package gui; import gui.forms.GUIMain; import gui.listeners.PaneMenuListener; import util.Constants; import util.Utils; import util.settings.Settings; import javax.accessibility.AccessibleComponent; import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.image.BufferedImage; import java.lang.reflect.Field; import java.util.ArrayList; /** * Created by Nick on 1/4/14. * <p> * Credit http://stackoverflow.com/questions/60269/how-to-implement-draggable-tab-using-java-swing * <p> * Modified by me. */ public class DraggableTabbedPane extends JTabbedPane { public boolean dragging = false; private Image tabImage = null; private Point currentMouseLocation = null; private int draggedTabIndex = 0; private TabType toPlace = null; public DraggableTabbedPane() { super(); addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent e) { currentMouseLocation = e.getPoint(); if (!dragging) { // Gets the tab index based on the mouse position int tabNumber = getUI().tabForCoordinate(DraggableTabbedPane.this, e.getX(), e.getY()); if (tabNumber == 0 || tabNumber == getTabCount() - 1) return; if (tabNumber > 0) { draggedTabIndex = tabNumber; Rectangle bounds = getUI().getTabBounds(DraggableTabbedPane.this, tabNumber); // Paint the tabbed pane to a buffer Image totalImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics totalGraphics = totalImage.getGraphics(); totalGraphics.setClip(bounds); // Don't be double buffered when painting to a static image. setDoubleBuffered(false); paintComponent(totalGraphics); // Paint just the dragged tab to the buffer tabImage = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB); Graphics graphics = tabImage.getGraphics(); graphics.drawImage(totalImage, 0, 0, bounds.width, bounds.height, bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height, DraggableTabbedPane.this); dragging = true; repaint(); } } else { TabType tt = getTabType(e); if (tt.getType() != TabTypeEnum.TAB_NEITHER) { toPlace = tt; } else { toPlace = null; } // Need to repaint repaint(); } super.mouseDragged(e); } }); addMouseListener(new MouseAdapter() { public void mouseReleased(MouseEvent e) { int tabNumber = getUI().tabForCoordinate(DraggableTabbedPane.this, e.getX(), e.getY()); if (dragging) { if (tabNumber > 0 && tabNumber != draggedTabIndex && tabNumber != getTabCount() - 1) { int indexWillPlace = draggedTabIndex; if (toPlace.getType() == TabTypeEnum.TAB_MOVE_RIGHT) { if (tabNumber > draggedTabIndex) { indexWillPlace = tabNumber; } else { indexWillPlace = tabNumber + 1; } } else if (toPlace.getType() == TabTypeEnum.TAB_MOVE_LEFT) { if (tabNumber > draggedTabIndex) { indexWillPlace = tabNumber - 1; } else { indexWillPlace = tabNumber; } } else if (toPlace.getType() == TabTypeEnum.TAB_COMBINED) { indexWillPlace = tabNumber; } if (indexWillPlace != draggedTabIndex) { if (toPlace.getType() == TabTypeEnum.TAB_COMBINED) { //there's four cases to catch ChatPane cp = Utils.getChatPane(draggedTabIndex); if (cp != null) { ChatPane willPlace = Utils.getChatPane(indexWillPlace); if (willPlace != null) { //single -> single //creating a combined chat pane for the first time CombinedChatPane combinedChatPane = CombinedChatPane.createCombinedChatPane(cp, willPlace); if (combinedChatPane != null) { if (!e.isControlDown()) { if (draggedTabIndex > indexWillPlace) { removeTabAt(draggedTabIndex); removeTabAt(indexWillPlace); } else { removeTabAt(indexWillPlace); removeTabAt(draggedTabIndex); } } insertTab(combinedChatPane.getTabTitle(), null, combinedChatPane.getScrollPane(), null, combinedChatPane.getIndex()); setSelectedIndex(combinedChatPane.getIndex()); GUIMain.combinedChatPanes.add(combinedChatPane); } } else { CombinedChatPane willPlaceCombined = Utils.getCombinedChatPane(indexWillPlace); if (willPlaceCombined != null) { //single -> combined //adding to an already existing combined pane if (willPlaceCombined.addChatPane(cp)) { //successfully added to the pane removeTabAt(draggedTabIndex); setSelectedIndex(willPlaceCombined.getIndex()); } } } } else { CombinedChatPane ccp = Utils.getCombinedChatPane(draggedTabIndex); if (ccp != null) { ChatPane willPlace = Utils.getChatPane(indexWillPlace); if (willPlace != null) { //combined -> single //we'll convert it back to single -> combined if (ccp.addChatPane(willPlace)) { if (!e.isControlDown()) { removeTabAt(indexWillPlace); } setSelectedIndex(ccp.getIndex()); } } else { CombinedChatPane willPlaceCombined = Utils.getCombinedChatPane(indexWillPlace); if (willPlaceCombined != null) { //combined -> combined if (ccp.addChatPane(willPlaceCombined.getPanes())) { if (e.isControlDown()) { removeTabAt(willPlaceCombined.getIndex()); //we'll have to get rid of the other one GUIMain.combinedChatPanes.remove(willPlaceCombined); } setSelectedIndex(ccp.getIndex()); } } } } } } else { //dragging and moving the tabs Component comp = getComponentAt(draggedTabIndex); String title = getTitleAt(draggedTabIndex); removeTabAt(draggedTabIndex); insertTab(title, null, comp, null, indexWillPlace); setSelectedIndex(indexWillPlace); } } } } if (SwingUtilities.isRightMouseButton(e)) { if (tabNumber > 0) { ChatPane detected = Utils.getChatPane(tabNumber); CombinedChatPane detectedCombo = Utils.getCombinedChatPane(tabNumber); boolean first = (detected != null); boolean second = (detectedCombo != null); if (first || second) { JPopupMenu popupMenu = new JPopupMenu(); PaneMenuListener listener = Constants.listenerPaneMenu; JMenuItem menuItem = new JMenuItem("Pop-out chat"); menuItem.addActionListener(listener); popupMenu.add(menuItem); if (Settings.showTabPulses.getValue()) { menuItem = new JMenuItem("Toggle Tab Pulsing " + (first ? (detected.shouldPulseLoc() ? "OFF" : "ON") : (detectedCombo.shouldPulseLoc() ? "OFF" : "ON"))); menuItem.addActionListener(listener); popupMenu.add(menuItem); } if (first) { menuItem = new JMenuItem("Go to " + detected.getChannel() + "'s channel"); menuItem.addActionListener(listener); popupMenu.add(menuItem); menuItem = new JMenuItem("View viewer list"); menuItem.addActionListener(listener); popupMenu.add(menuItem); menuItem = new JMenuItem("Remove tab"); menuItem.addActionListener(listener); popupMenu.add(menuItem); } else { menuItem = new JMenuItem("Disband tab"); menuItem.addActionListener(listener); popupMenu.add(menuItem); menuItem = new JMenuItem("Rename tab"); menuItem.addActionListener(Constants.tabListener); popupMenu.add(menuItem); JMenu panels = new JMenu("Set Active Panel..."); JCheckBoxMenuItem streamCheck; streamCheck = new JCheckBoxMenuItem("All"); streamCheck.addActionListener(listener); if (detectedCombo.getActiveChannel().equalsIgnoreCase("All")) streamCheck.setState(true); panels.add(streamCheck); panels.add(new JPopupMenu.Separator()); String[] streams = detectedCombo.getChannels(); for (String stream : streams) { streamCheck = new JCheckBoxMenuItem(stream); streamCheck.addActionListener(listener); if (detectedCombo.getActiveChannel().equalsIgnoreCase(stream)) streamCheck.setState(true); panels.add(streamCheck); } popupMenu.add(panels); } menuItem = new JMenuItem("Clear Chat"); menuItem.addActionListener(listener); popupMenu.add(menuItem); popupMenu.show((DraggableTabbedPane) e.getSource(), e.getX(), e.getY()); } } } updateIndexes(); tabImage = null; toPlace = null; dragging = false; repaint(); } }); } public void updateIndexes() { if (!GUIMain.chatPanes.isEmpty()) { for (int i = 0; i < getTabCount(); i++) { String title = getTitleAt(i); ChatPane toUpdate = GUIMain.chatPanes.get(title); if (toUpdate != null) toUpdate.setIndex(i); } } if (!GUIMain.combinedChatPanes.isEmpty()) { for (int i = 0; i < getTabCount(); i++) { String title = getTitleAt(i); for (CombinedChatPane cp : GUIMain.combinedChatPanes) { if (cp.getTabTitle().equalsIgnoreCase(title)) { cp.setIndex(i); break; } } } } } public void scrollDownPanes() { if (!GUIMain.chatPanes.isEmpty()) { ChatPane[] panes = GUIMain.chatPanes.values().toArray(new ChatPane[GUIMain.chatPanes.size()]); for (ChatPane p : panes) { p.scrollToBottom(); } } if (!GUIMain.combinedChatPanes.isEmpty()) { GUIMain.combinedChatPanes.forEach(gui.CombinedChatPane::scrollToBottom); } } public AccessibleComponent getPage(int index) { try { Field pages = JTabbedPane.class.getDeclaredField("pages"); pages.setAccessible(true); Object p = pages.get(this); return (AccessibleComponent) ((ArrayList) p).get(index); } catch (Exception e) { e.printStackTrace(); return null; } } @Override public void setTitleAt(int index, String title) { CombinedChatPane pane = Utils.getCombinedChatPane(index); if (pane != null && !pane.getTabTitle().equalsIgnoreCase(title)) pane.setCustomTitle(title); super.setTitleAt(index, title); } private final RenderingHints antialiasing = new RenderingHints( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHints(antialiasing); if (toPlace != null && toPlace.getType() != TabTypeEnum.TAB_NEITHER) { g2.setColor(Color.orange); Polygon toFill = getFillShape(toPlace); g2.fillPolygon(toFill); g2.drawRect((int) toPlace.getRectangle().getX(), (int) toPlace.getRectangle().getY(), (int) toPlace.getRectangle().getWidth(), (int) toPlace.getRectangle().getHeight()); } // Are we dragging? if (dragging && currentMouseLocation != null && tabImage != null) { // Draw the dragged tab g2.drawImage(tabImage, currentMouseLocation.x, currentMouseLocation.y, this); } } /** * So we're going to apply some geometry/trig here. * <p> * Each tabType rectangle will be dynamic in size. We need * to create the appropriate shape for each type, heeding to * the dynamic size of the rectangle. * <p> * We want a filled ">" or "<" triangle to show it's moving, and a filled "+" * to show it's going to be combined. * <p> * For the triangles, we create an equilateral triangle around the center point, * easy if you inscribe the triangle on a circle with a defined radius, * which is half the distance from the center of the rectangle to the left/right bound. * ________ * | | * | x | * | * x | * | x | * |________| * <p> * The trig part comes in for the two points on the same x, but different y. The y will be calculated with * sin (60) = x/radius or radius * sin(60), with the x point being negated if it's pointing to the right. * <p> * For the "+" shape, we need to create it by doing fourth blocks. * _________ * | _ | * | _| |_ | * | |_ * _| | * | |_| | * |_________| * <p> * We can calculate the points by doing (the distance from the center to left/right bound / 4) * as our initial x/y point, then start rotating (and applying - signs) and adding the points * on the outer edge. * * @param tabType The tab to determine which shape to make. * @return The shape. */ private Polygon getFillShape(TabType tabType) { Polygon p = new Polygon(); Rectangle r = tabType.getRectangle(); int centerRectX = (int) r.getCenterX(); int centerRectY = (int) r.getCenterY(); if (tabType.getType() == TabTypeEnum.TAB_MOVE_RIGHT) { int distX = (int) (r.getX() + r.getWidth()) - centerRectX; int distY = (int) (r.getY() + r.getHeight()) - centerRectY; Point first = new Point(centerRectX + (distX), centerRectY); p.addPoint(first.x, first.y); Point second; int x2 = (centerRectX - (distX / 2)); int y2 = (centerRectY + (distY / 2)); int y3 = (centerRectY - (distY / 2)); second = new Point(x2, y2); p.addPoint(second.x, second.y); p.addPoint(second.x, y3); } else if (tabType.getType() == TabTypeEnum.TAB_MOVE_LEFT) { int distX = (int) (r.getX() + r.getWidth()) - centerRectX; int distY = (int) (r.getY() + r.getHeight()) - centerRectY; Point first = new Point((int) r.getX(), centerRectY); p.addPoint(first.x, first.y); Point second; int x2 = (centerRectX + distX); int y2 = (centerRectY + (distY / 2)); int y3 = (centerRectY - (distY / 2)); second = new Point(x2, y2); p.addPoint(second.x, second.y); p.addPoint(second.x, y3); } else {//combine int distX = (int) (r.getX() + r.getWidth()) - centerRectX; int distY = (int) (r.getY() + r.getHeight()) - centerRectY; int outerX = (int) (distX / 1.25); int innerX = outerX / 2; int outerY = (int) (distY / 1.25); int innerY = outerY / 2; //bottom p.addPoint(centerRectX - innerX, centerRectY - innerY); p.addPoint(centerRectX - innerX, centerRectY - outerY); p.addPoint(centerRectX + innerX, centerRectY - outerY); p.addPoint(centerRectX + innerX, centerRectY - innerY); //right p.addPoint(centerRectX + outerX, centerRectY - innerY); p.addPoint(centerRectX + outerX, centerRectY + innerY); //top p.addPoint(centerRectX + innerX, centerRectY + innerY); p.addPoint(centerRectX + innerX, centerRectY + outerY); p.addPoint(centerRectX - innerX, centerRectY + outerY); p.addPoint(centerRectX - innerX, centerRectY + innerY); //left p.addPoint(centerRectX - outerX, centerRectY + innerY); p.addPoint(centerRectX - outerX, centerRectY - innerY); } return p; } /** * Overrides the TabbedPane in order to prevent the "+" tab from getting * selected and shown. */ @Override protected void fireStateChanged() { ChatPane cp = Utils.getChatPane(getSelectedIndex()); CombinedChatPane ccp = Utils.getCombinedChatPane(getSelectedIndex()); if (cp != null) GUIMain.updateTitle(cp.getViewerCountString()); if (ccp != null) { String activeChan = ccp.getActiveChannel(); GUIMain.updateTitle(activeChan.equalsIgnoreCase("all") ? null : GUIMain.chatPanes.get(activeChan).getViewerCountString()); } if (getSelectedIndex() == getTabCount() - 1) return; super.fireStateChanged(); } /** * Right so. * We have this rectangle. * ------------------- * | | * | | * ------------------- * but how do we cut it up into thirds? By cutting it into halves, and * using the halves as midpoints. * ------------------- * | | | * | | | * ------------------- * now it's in half, just cut it in half again. * ------------------- * | | | | * | | | | * ------------------- * repeat for the right side, and when finished just cut out the middle line * (or make a new rectangle with those coordinates). * ------------------- * | | | | * | | | | * ------------------- * <p> * Now we can check the point of the mouse event and see if it will * create a new combined tab, or just move the tab to the left/right of the * tab. * * @param e The mouse event (gives the Point) of the mouse. * @return The Tab Type that the tab will create. */ public TabType getTabType(MouseEvent e) { int tabNumber = getUI().tabForCoordinate(DraggableTabbedPane.this, e.getX(), e.getY()); if (tabNumber == draggedTabIndex || tabNumber <= 0 || tabNumber == getTabCount() - 1) return new TabType(null, TabTypeEnum.TAB_NEITHER); Rectangle r = getUI().getTabBounds(DraggableTabbedPane.this, tabNumber);//base tab rectangle. Rectangle leftHalf = new Rectangle((int) r.getX(), (int) r.getY(), (int) (r.getWidth() / 2), (int) r.getHeight()); Rectangle leftMove = new Rectangle((int) leftHalf.getX(), (int) leftHalf.getY(), (int) (leftHalf.getWidth() / 2), (int) leftHalf.getHeight()); Rectangle rightHalf = new Rectangle((int) (r.getX() + leftHalf.getWidth()), (int) r.getY(), (int) leftHalf.getWidth(), (int) leftHalf.getHeight()); //same width as the other half Rectangle rightMove = new Rectangle((int) (rightHalf.getX() + leftMove.getWidth()), (int) rightHalf.getY(), (int) (leftMove.getWidth()), (int) leftHalf.getHeight()); Rectangle combine = new Rectangle((int) (r.getX() + leftMove.getWidth()), (int) r.getY(), (int) leftHalf.getWidth(), (int) r.getHeight()); Point p = e.getPoint(); if (leftMove.contains(p)) { return new TabType(leftMove, TabTypeEnum.TAB_MOVE_LEFT); } else if (rightMove.contains(p)) { return new TabType(rightMove, TabTypeEnum.TAB_MOVE_RIGHT); } else if (combine.contains(p)) { return new TabType(combine, TabTypeEnum.TAB_COMBINED); } else return new TabType(null, TabTypeEnum.TAB_NEITHER); } enum TabTypeEnum { TAB_COMBINED, TAB_MOVE_LEFT, TAB_MOVE_RIGHT, TAB_NEITHER } private class TabType { Rectangle rectangle; TabTypeEnum type; TabType(Rectangle r, TabTypeEnum type) { rectangle = r; this.type = type; } public Rectangle getRectangle() { return rectangle; } public TabTypeEnum getType() { return type; } } }