package com.vitco.manager.help; import com.jidesoft.docking.DockableFrame; import com.jidesoft.docking.FrameContainer; import com.jidesoft.plaf.basic.BasicJideTabbedPaneUI; import com.jidesoft.swing.JideButton; import com.jidesoft.swing.JideMenu; import com.jidesoft.swing.JideSplitButton; import com.jidesoft.swing.JideTabbedPane; import com.vitco.Main; import com.vitco.layout.frames.custom.FrameGenericJideButton; import com.vitco.manager.action.ActionManager; import com.vitco.manager.action.ComplexActionManager; import com.vitco.manager.lang.LangSelectorInterface; import com.vitco.settings.VitcoSettings; import com.vitco.util.misc.SaveResourceLoader; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.CubicCurve2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; /** * Help overlay class that displays the help on top * over the frame that the help was "clicked on". * */ public class FrameHelpOverlay extends JComponent { // the frame this overlay lives in private final JRootPane frame; // the action managers that are used to acquire the actions private final ActionManager actionManager; private ComplexActionManager complexActionManager; // language selector that is used to lookup the help texts private LangSelectorInterface langSelector; // custom rectangle class that can store string information private final static class CRectangle extends Rectangle { // constructor public CRectangle(int x, int y, int w, int h, String info) { super(x,y,w,h); this.info = info; } // retrieve the stored information private String info = null; public final String getInfo() { return info; } } // list of known rectangles (that can trigger a refresh) private final ArrayList<CRectangle> rects = new ArrayList<CRectangle>(); // currently active rectangle private CRectangle activeRect = null; // reference to this help overlay instance private final JComponent thisInstance = this; // handwriting font private static Font font = null; // close button image private static Image closeButton = null; // background information (this is stored to fake transparency without // actually needing to refresh the components that are "underneath") private static BufferedImage image = null; // the rectangle that we want to display our information into private Rectangle displayRect = null; // --------------- // static constructor static { // read handwriting font from file try { font = Font.createFont(Font.TRUETYPE_FONT, new SaveResourceLoader("resource/font/font.ttf").asInputStream()).deriveFont(Font.PLAIN, 18f); } catch (FontFormatException e) { // should never happen e.printStackTrace(); } catch (IOException e) { // should never happen e.printStackTrace(); } // read close button image closeButton = new SaveResourceLoader("resource/img/icons/close.png").asImage(); } // constructor public FrameHelpOverlay(final JRootPane frame, ActionManager actionManager, ComplexActionManager complexActionManager, LangSelectorInterface langSelector) { // set final parameters this.frame = frame; this.actionManager = actionManager; this.complexActionManager = complexActionManager; this.langSelector = langSelector; // set this to "not transparent" (we handle fake transparency our self) this.setOpaque(true); // register mouse events MouseAdapter adapter = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { super.mousePressed(e); // hide this instance when clicked setActive(false); } @Override public void mouseMoved(MouseEvent e) { super.mouseMoved(e); handleMoveEvent(e.getPoint()); } @Override public void mouseExited(MouseEvent e) { super.mouseExited(e); handleMoveEvent(new Point(-1,-1)); } }; this.addMouseMotionListener(adapter); this.addMouseListener(adapter); // register "hide when focus lost" this.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { super.focusLost(e); setActive(false); } }); // listen to show and resize actions this.addComponentListener(new ComponentAdapter () { @Override public void componentShown(ComponentEvent e) { super.componentShown(e); updateInternal(); // reset mouse position (highlighting) handleMoveEvent(new Point(-1,-1)); } @Override public void componentHidden(ComponentEvent e) { super.componentHidden(e); image = null; // free memory } @Override public void componentResized(ComponentEvent e) { super.componentResized(e); updateInternal(); } }); // register "hide when esc pressed" this.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { super.keyPressed(e); if (e.getKeyCode() == 27) { setActive(false); } } }); } // ==================== // Internal logic // ==================== // enable/disable this glasspane private boolean active = false; public final void setActive(boolean active) { if (this.active == active) { return; } this.active = active; frame.setGlassPane(this); this.setVisible(active); // this will trigger updating if (active) { // request focus if visible thisInstance.requestFocusInWindow(); // ensure we capture a fresh image image = null; } } public final boolean isActive() { return active; } // helper - repaint this overlay if rectangle of interest changes private void handleMoveEvent(Point point) { if (isActive()) { boolean found = false; // check if position is inside a rectangle for (CRectangle rect : rects) { if (rect.contains(point)) { if (!rect.equals(activeRect)) { activeRect = rect; thisInstance.repaint(); } found = true; break; } } // check if position is not inside any rectangle if (!found) { if (activeRect != null) { activeRect = null; thisInstance.repaint(); } } } } // helper - invalidate internal cache structure private void updateInternal() { if (isActive()) { // analyse rectangle structure rects.clear(); rAnalyze(frame.getContentPane(), 0, 0); // check for missing help information if (Main.isDebugMode()) { for (CRectangle rect : rects) { if (!langSelector.containsString("help_overlay_" + rect.getInfo())) { System.out.println("Error: Missing help for: " + "help_overlay_" + rect.getInfo()); } } } // determine the display rect (where the information is shown in) int left = thisInstance.getWidth()/2 - 250; displayRect = new Rectangle( Math.max(left, 20), 0, Math.min(left+500, thisInstance.getWidth()) - Math.max(left, 40), thisInstance.getHeight() ); // trigger a repaint thisInstance.repaint(); } } // helper - recursively find components and convert them // to rectangles with identifier text private void rAnalyze(Container cont, int x, int y) { // loop over all contained components for (int i = 0; i < cont.getComponentCount(); i++) { Component comp = cont.getComponent(i); if (comp.isVisible()) { // only handle visible components if (comp instanceof JideSplitButton) { // handle "complex" action buttons (popup component) for (Component complexAction : ((JideSplitButton)comp).getMenuComponents()) { String[] actionKeys = complexActionManager.getActionKeys(complexAction); if (actionKeys.length > 0) { int xn = x + comp.getX(); int yn = y + comp.getY(); int w = comp.getWidth(); int h = comp.getHeight(); // add to front rects.add(0, new CRectangle(xn, yn, w, h, "complex_action_" + actionKeys[0])); break; } } } else if (comp instanceof JideButton) { // handle "normal" action buttons for (ActionListener actionListener : ((AbstractButton)comp).getActionListeners()) { if (actionListener instanceof AbstractAction) { String[] actionKeys = actionManager.getActionKeys((AbstractAction)actionListener); if (actionKeys.length > 0) { int xn = x + comp.getX(); int yn = y + comp.getY(); int w = comp.getWidth(); int h = comp.getHeight(); // add to front rects.add(0, new CRectangle(xn, yn, w, h, "action_" + actionKeys[0])); break; } else { if (comp instanceof FrameGenericJideButton) { int xn = x + comp.getX(); int yn = y + comp.getY(); int w = comp.getWidth(); int h = comp.getHeight(); // add to front rects.add(0, new CRectangle(xn, yn, w, h, "generic_header_button_" + ((JideButton) comp).getToolTipText() .replace(" ", "_").toLowerCase()) ); } } } } } else if (comp instanceof DockableFrame) { // handle dockable sub-frames (i.e. the entire sub-frames) int xn = x + comp.getX() - 2; int yn = y + comp.getY() - 2; int w = comp.getWidth() + 4; int h = comp.getHeight() + 4; // append rects.add(new CRectangle(xn, yn, w, h, "window_" + ((DockableFrame)comp).getKey())); } else if (comp instanceof BasicJideTabbedPaneUI.ScrollableTabViewport) { // handle window tab bars (if multiple windows are stacked) Component tabbedPaneUncast = comp.getParent(); // check if parent is really a JideTabbedPane if (tabbedPaneUncast != null && tabbedPaneUncast instanceof JideTabbedPane) { JideTabbedPane tabbedPane = (JideTabbedPane)tabbedPaneUncast; Rectangle visibleRect = comp.getBounds(); // loop over all tabs for (int k = 0; k < tabbedPane.getTabCount(); k++) { // compute visible part of rectangle of this tab Rectangle rect = tabbedPane.getBoundsAt(k).intersection(visibleRect); // check if this tab is actually visible if (!rect.isEmpty()) { // check that this tab is actually visible if (tabbedPane instanceof FrameContainer) { // this is a "proper" dockable jide window // i.e. several dockable windows are grouped together String name = tabbedPane.getComponentAt(k).getName(); if (name != null) { int xn = x + rect.x; int yn = y + rect.y; int w = rect.width; int h = rect.height; // add to front rects.add(0, new CRectangle(xn, yn, w, h, "window_" + name)); } else { System.out.println("Error: No name detected for help identifier (#1)."); } } else { // this lives inside a dockable window (custom defined) // find the "proper" parent dockable frame Component parent = tabbedPane.getParent(); while (parent != null && !(parent instanceof DockableFrame)) { parent = parent.getParent(); } if (parent != null) { String name = "window_" + ((DockableFrame)parent).getKey() + "_tab_" + tabbedPane.getTitleAt(k).replace(" ", "_").toLowerCase(); int xn = x + rect.x; int yn = y + rect.y; int w = rect.width; int h = rect.height; // add to front rects.add(0, new CRectangle(xn, yn, w, h, name)); } else { System.out.println("Error: No name detected for help identifier (#2)."); } } } } } } else if (comp instanceof JideMenu) { // handle menu items (text menu) String name = comp.getName(); if (name != null && !name.equals("")) { int xn = x + comp.getX(); int yn = y + comp.getY(); int w = comp.getWidth(); int h = comp.getHeight(); // append rects.add(new CRectangle(xn, yn, w, h, "menu_item_" + name)); } } // -- recursively analyze the sub-components if (comp instanceof Container) { rAnalyze((Container) comp, x + comp.getX(), y + comp.getY()); } } } } // ==================== // Drawing logic // ==================== // Draw a string consisting of words with automatic line breaks. // Returns the dimensions that drawing this string takes (drawing can be prevented with the draw flag) public Rectangle drawString(Graphics g, String str, int x, int y, int width, boolean draw) { FontMetrics fm = g.getFontMetrics(); int lineHeight = fm.getHeight(); // get line hight int curX = x; int curY = y; int maxX = x; // replace line breaks (the "|" characters) str = str.replace("|","| "); for (String word : str.split(" ")) { // split into words boolean lineEnd = word.endsWith("|"); if (lineEnd) { // remove line break word = word.substring(0, word.length()-1); } int wordWidth = fm.stringWidth(word + (lineEnd ? "" : " ")); // get word width if (curX + wordWidth >= x + width) { // check if we need to do a line break curY += lineHeight; curX = x; } if (!word.equals("")) { maxX = Math.max(maxX, curX + wordWidth); if (draw) { g.drawString(word, curX, curY); } // add word width to current x curX += wordWidth; } if (lineEnd) { curY += lineHeight; curX = x; } } // return dimensions return new Rectangle(x, y, maxX-x, (curY + lineHeight)-y); } // helper - draw information to rectangle and also draw a connections curve (Bezier) // if a rectangle is hovered (selected) private void drawHelpContent(Graphics2D g2, String identifier) { if (displayRect != null) { // extract help identifier String help_text; if (langSelector.containsString(identifier)) { help_text = langSelector.getString(identifier); } else { help_text = langSelector.getString("help_overlay_no_help_available") + " (#" + identifier + ")"; } // g2.drawRoundRect(displayRect.x, displayRect.y, displayRect.width, displayRect.height, 4, 4); // debug g2 = (Graphics2D) g2.create(); // extract the dimension of the rect that drawing the string will take Rectangle rect = drawString(g2, help_text, displayRect.x, displayRect.y, displayRect.width, false); // compute the top left point for our info text int topX = displayRect.x + (displayRect.width - rect.width) / 2; int topY = displayRect.y + g2.getFontMetrics().getHeight()/2 + 30; // draw the Bezier curve (if activeRect is available) from activeRect to displayRect g2.setColor(VitcoSettings.HELP_OVERLAY_HIGHLIGHT_COLOR); drawBezier(g2, rect, topX, topY); // draw outline for text box g2.setStroke(new BasicStroke(2f)); g2.setColor(new Color(0,0,0,100)); g2.fillRoundRect(topX - 10, topY - 10, rect.width + 20, rect.height + 20, 15, 15); g2.setColor(VitcoSettings.HELP_OVERLAY_DEFAULT_COLOR); g2.drawRoundRect(topX - 10, topY - 10, rect.width + 20, rect.height + 20, 15, 15); // draw text into text box g2.setStroke(new BasicStroke(1f)); drawString(g2, help_text, topX, topY + g2.getFontMetrics().getHeight()/2, displayRect.width, true); // dispose graphics element g2.dispose(); } } // helper - draw Bezier curve private void drawBezier(Graphics2D g2, Rectangle2D rect, int topX, int topY) { if (activeRect != null) { g2.setStroke(new BasicStroke(2f)); // compute the two rectangles that we want to connect // with the bezier curve int r1_left = topX - 10; int r1_top = topY - 10; int r1_right = (int) (topX + rect.getWidth() + 10); int r1_bottom = (int) (topY + rect.getHeight() + 10); int r1_center_x = (r1_left + r1_right)/2; int r1_center_y = (r1_top + r1_bottom)/2; int r2_left = activeRect.x; int r2_top = activeRect.y; int r2_right = activeRect.x + activeRect.width; int r2_bottom = activeRect.y + activeRect.height; int r2_center_x = (r2_left + r2_right)/2; int r2_center_y = (r2_top + r2_bottom)/2; // define the points and control points according to // the rectangle position (making sure they are drawn // from connecting sides) int p1x, p1y, p2x, p2y, c1x, c1y, c2x, c2y; if (r1_center_x < r2_center_x) { if (r1_center_y < r2_center_y) { p1x = (r1_left + r1_right)/2; p1y = r1_bottom; p2x = r2_left; p2y = (r2_top + r2_bottom)/2; c1x = p1x; c1y = p1y + 100; c2x = p2x - 100; c2y = p2y; } else { p1x = r1_right; p1y = (r1_bottom + r1_top)/2; p2x = (r2_left + r2_right)/2; p2y = r2_bottom; c1x = p1x + 100; c1y = p1y; c2x = p2x; c2y = p2y + 100; } } else { if (r1_center_y < r2_center_y) { p1x = (r1_left + r1_right)/2; p1y = r1_bottom; p2x = r2_right; p2y = (r2_top + r2_bottom)/2; c1x = p1x; c1y = p1y + 100; c2x = p2x + 100; c2y = p2y; } else { p1x = r1_left; p1y = (r1_bottom + r1_top)/2; p2x = (r2_left + r2_right)/2; p2y = r2_bottom; c1x = p1x - 100; c1y = p1y; c2x = p2x; c2y = p2y + 100; } } // draw the actual curve CubicCurve2D cubicCurve = new CubicCurve2D.Double(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y); g2.draw(cubicCurve); } } // helper - draw known rectangles (and highlight selected) private void drawRects(Graphics2D g2) { for (CRectangle rect : rects) { if (rect.equals(activeRect)) { g2.setColor(VitcoSettings.HELP_OVERLAY_HIGHLIGHT_COLOR); g2.drawRoundRect(rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4, 4, 4); g2.setColor(VitcoSettings.HELP_OVERLAY_DEFAULT_COLOR); } else { g2.drawRoundRect(rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4, 4, 4); } } } // native paint method call @Override public void paint(Graphics g) { super.paint(g); // only draw information if this overlay is active // Note: this is necessary since the glasspane is also displayed when // the dockable frames are dragged if (isActive()) { // draw overlay Graphics2D g2 = (Graphics2D)g.create(); // capture background (for transparency) if outdated if (image == null || image.getWidth() != thisInstance.getWidth() || image.getHeight() != thisInstance.getHeight()) { image = new BufferedImage(thisInstance.getWidth(), thisInstance.getHeight(), BufferedImage.TYPE_INT_RGB); frame.getContentPane().paint(image.getGraphics()); // draw it a bit darker (to show that this is an overlay) Graphics gr = image.createGraphics(); gr.setColor(new Color(0, 0, 0, 100)); gr.fillRect(0, 0, this.getWidth(), this.getHeight()); gr.dispose(); } // draw static image (background) g2.drawImage(image, 0, 0, null); // prepare graphics settings g2.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (font != null) { g2.setFont(font); } g2.setColor(VitcoSettings.HELP_OVERLAY_DEFAULT_COLOR); g2.setStroke(new BasicStroke(2f)); // paint all known rectangles drawRects(g2); // paint the help content using the active rectangle if (activeRect != null) { // a rectangle is hovered (selected) drawHelpContent(g2, "help_overlay_" + activeRect.getInfo()); } else { // paint "root" of current help view if no sub-component is hovered Component comp = thisInstance.getRootPane().getParent(); if (comp instanceof DockableFrame) { // dockable frame is parent drawHelpContent(g2, "help_overlay_window_" + ((DockableFrame) comp).getKey()); } else { // the entire window is parent drawHelpContent(g2, "help_overlay_abtract_overview_information"); } } // draw the closing button into the top right corner (this is just to give ppl something to // click as clicking anywhere will close the overlay) if (closeButton != null) { g2.drawImage(closeButton, this.getWidth() - closeButton.getWidth(null) - 10, 10, null); } // free the graphics element that we created g2.dispose(); } } }