1 /*
   2  * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.apple.laf;
  27 
  28 import java.awt.*;
  29 import java.beans.PropertyVetoException;
  30 
  31 import javax.swing.*;
  32 import javax.swing.border.Border;
  33 import javax.swing.plaf.UIResource;
  34 
  35 import sun.swing.SwingUtilities2;
  36 
  37 import apple.laf.*;
  38 import apple.laf.JRSUIConstants.*;
  39 import apple.laf.JRSUIState.TitleBarHeightState;
  40 
  41 import com.apple.laf.AquaUtils.RecyclableSingleton;
  42 import com.apple.laf.AquaInternalFrameBorderMetrics;
  43 import java.awt.geom.AffineTransform;
  44 
  45 public class AquaInternalFrameBorder implements Border, UIResource {
  46     private static final int kCloseButton = 0;
  47     private static final int kIconButton = 1;
  48     private static final int kGrowButton = 2;
  49 
  50     private static final int sMaxIconWidth = 15;
  51     private static final int sMaxIconHeight = sMaxIconWidth;
  52     private static final int sAfterButtonPad = 11;
  53     private static final int sAfterIconPad = 5;
  54     private static final int sRightSideTitleClip = 0;
  55 
  56     private static final int kContentTester = 100; // For getting region insets
  57 
  58     private static final RecyclableSingleton<AquaInternalFrameBorder> documentWindowFrame = new RecyclableSingleton<AquaInternalFrameBorder>() {
  59         protected AquaInternalFrameBorder getInstance() {
  60             return new AquaInternalFrameBorder(WindowType.DOCUMENT);
  61         }
  62     };
  63     protected static AquaInternalFrameBorder window() {
  64         return documentWindowFrame.get();
  65     }
  66 
  67     private static final RecyclableSingleton<AquaInternalFrameBorder> utilityWindowFrame = new RecyclableSingleton<AquaInternalFrameBorder>() {
  68         protected AquaInternalFrameBorder getInstance() {
  69             return new AquaInternalFrameBorder(WindowType.UTILITY);
  70         }
  71     };
  72     protected static AquaInternalFrameBorder utility() {
  73         return utilityWindowFrame.get();
  74     }
  75 
  76     private static final RecyclableSingleton<AquaInternalFrameBorder> dialogWindowFrame = new RecyclableSingleton<AquaInternalFrameBorder>() {
  77         protected AquaInternalFrameBorder getInstance() {
  78             return new AquaInternalFrameBorder(WindowType.DOCUMENT);
  79         }
  80     };
  81     protected static AquaInternalFrameBorder dialog() {
  82         return dialogWindowFrame.get();
  83     }
  84 
  85     private final AquaInternalFrameBorderMetrics metrics;
  86 
  87     private final int fThisButtonSpan;
  88     private final int fThisLeftSideTotal;
  89 
  90     private final boolean fIsUtility;
  91 
  92     // Instance variables
  93     private final WindowType fWindowKind; // Which kind of window to draw
  94     private Insets fBorderInsets; // Cached insets object
  95 
  96     private Color selectedTextColor;
  97     private Color notSelectedTextColor;
  98 
  99     private Rectangle fInBounds; // Cached bounds rect object
 100 
 101     protected final AquaPainter<TitleBarHeightState> titleBarPainter = AquaPainter.create(JRSUIStateFactory.getTitleBar());
 102     protected final AquaPainter<JRSUIState> widgetPainter = AquaPainter.create(JRSUIState.getInstance());
 103 
 104     protected AquaInternalFrameBorder(final WindowType kind) {
 105         fWindowKind = kind;
 106 
 107         titleBarPainter.state.set(WindowClipCorners.YES);
 108         if (fWindowKind == WindowType.UTILITY) {
 109             fIsUtility = true;
 110             metrics = AquaInternalFrameBorderMetrics.getMetrics(true);
 111 
 112             widgetPainter.state.set(WindowType.UTILITY);
 113             titleBarPainter.state.set(WindowType.UTILITY);
 114         } else {
 115             fIsUtility = false;
 116             metrics = AquaInternalFrameBorderMetrics.getMetrics(false);
 117 
 118             widgetPainter.state.set(WindowType.DOCUMENT);
 119             titleBarPainter.state.set(WindowType.DOCUMENT);
 120         }
 121         titleBarPainter.state.setValue(metrics.titleBarHeight);
 122         titleBarPainter.state.set(WindowTitleBarSeparator.YES);
 123         widgetPainter.state.set(AlignmentVertical.CENTER);
 124 
 125         fThisButtonSpan = (metrics.buttonWidth * 3) + (metrics.buttonPadding * 2);
 126         fThisLeftSideTotal = metrics.leftSidePadding + fThisButtonSpan + sAfterButtonPad;
 127     }
 128 
 129     public void setColors(final Color inSelectedTextColor, final Color inNotSelectedTextColor) {
 130         selectedTextColor = inSelectedTextColor;
 131         notSelectedTextColor = inNotSelectedTextColor;
 132     }
 133 
 134     // Utility to lazy-init and fill in fInBounds
 135     protected void setInBounds(final int x, final int y, final int w, final int h) {
 136         if (fInBounds == null) fInBounds = new Rectangle();
 137 
 138         fInBounds.x = x;
 139         fInBounds.y = y;
 140         fInBounds.width = w;
 141         fInBounds.height = h;
 142     }
 143 
 144     // Border interface
 145     public boolean isBorderOpaque() {
 146         return false;
 147     }
 148 
 149     // Border interface
 150     public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int w, final int h) {
 151         // For expanded InternalFrames, the frame & component are the same object
 152         paintBorder((JInternalFrame)c, c, g, x, y, w, h);
 153     }
 154 
 155     protected void paintTitleContents(final Graphics g, final JInternalFrame frame, final int x, final int y, final int w, final int h) {
 156         final boolean isSelected = frame.isSelected();
 157         final Font f = g.getFont();
 158 
 159         g.setFont(metrics.font);
 160 
 161         // Center text vertically.
 162         final FontMetrics fm = g.getFontMetrics();
 163         final int baseline = (metrics.titleBarHeight + fm.getAscent() - fm.getLeading() - fm.getDescent()) / 2;
 164 
 165         // max button is the rightmost so use it
 166         final int usedWidth = fThisLeftSideTotal + sRightSideTitleClip;
 167         int iconWidth = getIconWidth(frame);
 168         if (iconWidth > 0) iconWidth += sAfterIconPad;
 169 
 170         final int totalWidth = w;
 171 
 172         // window title looks like: | 0 0 0(sAfterButtonPad)IconWidth Title(right pad) |
 173         final int availTextWidth = totalWidth - usedWidth - iconWidth - sAfterButtonPad;
 174 
 175         final String title = frame.getTitle();
 176 
 177         String text = title;
 178         int totalTextWidth = 0;
 179 
 180         int startXPosition = fThisLeftSideTotal;
 181         boolean wasTextShortened = false;
 182         // shorten the string to fit in the
 183         if (text != null && !text.isEmpty()) {
 184             totalTextWidth = SwingUtilities.computeStringWidth(fm, text);
 185             final String clipString = "\u2026";
 186             if (totalTextWidth > availTextWidth) {
 187                 wasTextShortened = true;
 188                 totalTextWidth = SwingUtilities.computeStringWidth(fm, clipString);
 189                 int nChars;
 190                 for (nChars = 0; nChars < text.length(); nChars++) {
 191                     final int nextCharWidth = fm.charWidth(text.charAt(nChars));
 192                     if ((totalTextWidth + nextCharWidth) > availTextWidth) {
 193                         break;
 194                     }
 195                     totalTextWidth += nextCharWidth;
 196                 }
 197                 text = text.substring(0, nChars) + clipString;
 198             }
 199 
 200             if (!wasTextShortened) {
 201                 // center it!
 202                 startXPosition = (totalWidth - (totalTextWidth + iconWidth)) / 2;
 203                 if (startXPosition < fThisLeftSideTotal) {
 204                     startXPosition = fThisLeftSideTotal;
 205                 }
 206             }
 207 
 208             if (isSelected || fIsUtility) {
 209                 g.setColor(Color.lightGray);
 210             } else {
 211                 g.setColor(Color.white);
 212             }
 213             SwingUtilities2.drawString(frame, g, text, x + startXPosition + iconWidth, y + baseline + 1);
 214 
 215             if (isSelected || fIsUtility) {
 216                 g.setColor(selectedTextColor);
 217             } else {
 218                 g.setColor(notSelectedTextColor);
 219             }
 220 
 221             SwingUtilities2.drawString(frame, g, text, x + startXPosition + iconWidth, y + baseline);
 222             g.setFont(f);
 223         }
 224 
 225         // sja fix x & y
 226         final int iconYPostion = (metrics.titleBarHeight - getIconHeight(frame)) / 2;
 227         paintTitleIcon(g, frame, x + startXPosition, y + iconYPostion);
 228     }
 229 
 230     public int getWhichButtonHit(final JInternalFrame frame, final int x, final int y) {
 231         int buttonHit = -1;
 232 
 233         final Insets i = frame.getInsets();
 234         int startX = i.left + metrics.leftSidePadding - 1;
 235         if (isInsideYButtonArea(i, y) && x >= startX) {
 236             if (x <= (startX + metrics.buttonWidth)) {
 237                 if (frame.isClosable()) {
 238                     buttonHit = kCloseButton;
 239                 }
 240             } else {
 241                 startX += metrics.buttonWidth + metrics.buttonPadding;
 242                 if (x >= startX && x <= (startX + metrics.buttonWidth)) {
 243                     if (frame.isIconifiable()) {
 244                         buttonHit = kIconButton;
 245                     }
 246                 } else {
 247                     startX += metrics.buttonWidth + metrics.buttonPadding;
 248                     if (x >= startX && x <= (startX + metrics.buttonWidth)) {
 249                         if (frame.isMaximizable()) {
 250                             buttonHit = kGrowButton;
 251                         }
 252                     }
 253                 }
 254             }
 255         }
 256 
 257         return buttonHit;
 258     }
 259 
 260     public void doButtonAction(final JInternalFrame frame, final int whichButton) {
 261         switch (whichButton) {
 262             case kCloseButton:
 263                 frame.doDefaultCloseAction();
 264                 break;
 265 
 266             case kIconButton:
 267                 if (frame.isIconifiable()) {
 268                     if (!frame.isIcon()) {
 269                         try {
 270                             frame.setIcon(true);
 271                         } catch(final PropertyVetoException e1) {}
 272                     } else {
 273                         try {
 274                             frame.setIcon(false);
 275                         } catch(final PropertyVetoException e1) {}
 276                     }
 277                 }
 278                 break;
 279 
 280             case kGrowButton:
 281                 if (frame.isMaximizable()) {
 282                     if (!frame.isMaximum()) {
 283                         try {
 284                             frame.setMaximum(true);
 285                         } catch(final PropertyVetoException e5) {}
 286                     } else {
 287                         try {
 288                             frame.setMaximum(false);
 289                         } catch(final PropertyVetoException e6) {}
 290                     }
 291                 }
 292                 break;
 293 
 294             default:
 295                 System.err.println("AquaInternalFrameBorder should never get here!!!!");
 296                 Thread.dumpStack();
 297                 break;
 298         }
 299     }
 300 
 301     public boolean isInsideYButtonArea(final Insets i, final int y) {
 302         final int startY = (i.top - metrics.titleBarHeight / 2) - (metrics.buttonHeight / 2) - 1;
 303         final int endY = startY + metrics.buttonHeight;
 304         return y >= startY && y <= endY;
 305     }
 306 
 307     public boolean getWithinRolloverArea(final Insets i, final int x, final int y) {
 308         final int startX = i.left + metrics.leftSidePadding;
 309         final int endX = startX + fThisButtonSpan;
 310         return isInsideYButtonArea(i, y) && x >= startX && x <= endX;
 311     }
 312 
 313     protected void paintTitleIcon(final Graphics g, final JInternalFrame frame,
 314             final int x, final int y) {
 315 
 316         Icon icon = frame.getFrameIcon();
 317         if (icon == null) {
 318             icon = UIManager.getIcon("InternalFrame.icon");
 319         }
 320 
 321         if (icon == null) {
 322             return;
 323         }
 324 
 325         if (icon.getIconWidth() > sMaxIconWidth
 326                 || icon.getIconHeight() > sMaxIconHeight) {
 327             final Graphics2D g2 = (Graphics2D) g;
 328             final AffineTransform savedAT = g2.getTransform();
 329             double xScaleFactor = (double) sMaxIconWidth / icon.getIconWidth();
 330             double yScaleFactor = (double) sMaxIconHeight / icon.getIconHeight();
 331 
 332             //Coordinates are after a translation hence relative origin shifts
 333             g2.translate(x, y);
 334 
 335             //scaling factor is needed to scale while maintaining aspect ratio
 336             double scaleMaintainAspectRatio = Math.min(xScaleFactor, yScaleFactor);
 337 
 338             //minimum value is taken to set to a maximum Icon Dimension
 339             g2.scale(scaleMaintainAspectRatio, scaleMaintainAspectRatio);
 340 
 341             icon.paintIcon(frame, g2, 0, 0);
 342             g2.setTransform(savedAT);
 343 
 344         } else {
 345             icon.paintIcon(frame, g, x, y);
 346         }
 347     }
 348 
 349     protected int getIconWidth(final JInternalFrame frame) {
 350         int width = 0;
 351 
 352         Icon icon = frame.getFrameIcon();
 353         if (icon == null) {
 354             icon = UIManager.getIcon("InternalFrame.icon");
 355         }
 356         if (icon != null) {
 357             width = Math.min(icon.getIconWidth(), sMaxIconWidth);
 358         }
 359 
 360         return width;
 361     }
 362 
 363     protected int getIconHeight(final JInternalFrame frame) {
 364         int height = 0;
 365 
 366         Icon icon = frame.getFrameIcon();
 367         if (icon == null) {
 368             icon = UIManager.getIcon("InternalFrame.icon");
 369         }
 370         if (icon != null) {
 371             height = Math.min(icon.getIconHeight(), sMaxIconHeight);
 372         }
 373 
 374         return height;
 375     }
 376 
 377     public void drawWindowTitle(final Graphics g, final JInternalFrame frame, final int inX, final int inY, final int inW, final int inH) {
 378         final int x = inX;
 379         final int y = inY;
 380         final int w = inW;
 381         int h = inH;
 382 
 383         h = metrics.titleBarHeight + inH;
 384 
 385         // paint the background
 386         titleBarPainter.state.set(frame.isSelected() ? State.ACTIVE : State.INACTIVE);
 387         titleBarPainter.paint(g, frame, x, y, w, h);
 388 
 389         // now the title and the icon
 390         paintTitleContents(g, frame, x, y, w, h);
 391 
 392         // finally the widgets
 393         drawAllWidgets(g, frame); // rollover is last attribute
 394     }
 395 
 396     // Component could be a JInternalFrame or a JDesktopIcon
 397     void paintBorder(final JInternalFrame frame, final Component c, final Graphics g, final int x, final int y, final int w, final int h) {
 398         if (fBorderInsets == null) getBorderInsets(c);
 399         // Set the contentRect - inset by border size
 400         setInBounds(x + fBorderInsets.left, y + fBorderInsets.top, w - (fBorderInsets.right + fBorderInsets.left), h - (fBorderInsets.top + fBorderInsets.bottom));
 401 
 402         // Set parameters
 403         setMetrics(frame, c);
 404 
 405         // Draw the frame
 406         drawWindowTitle(g, frame, x, y, w, h);
 407     }
 408 
 409     // defaults to false
 410     boolean isDirty(final JInternalFrame frame) {
 411         final Object dirty = frame.getClientProperty("windowModified");
 412         if (dirty == null || dirty == Boolean.FALSE) return false;
 413         return true;
 414     }
 415 
 416     // Border interface
 417     public Insets getBorderInsets(final Component c) {
 418         if (fBorderInsets == null) fBorderInsets = new Insets(0, 0, 0, 0);
 419 
 420         // Paranoia check
 421         if (!(c instanceof JInternalFrame)) return fBorderInsets;
 422 
 423         final JInternalFrame frame = (JInternalFrame)c;
 424 
 425         // Set the contentRect to an arbitrary value (in case the current real one is too small)
 426         setInBounds(0, 0, kContentTester, kContentTester);
 427 
 428         // Set parameters
 429         setMetrics(frame, c);
 430 
 431         fBorderInsets.left = 0;
 432         fBorderInsets.top = metrics.titleBarHeight;
 433         fBorderInsets.right = 0;
 434         fBorderInsets.bottom = 0;
 435 
 436         return fBorderInsets;
 437     }
 438 
 439     public void repaintButtonArea(final JInternalFrame frame) {
 440         final Insets i = frame.getInsets();
 441         final int x = i.left + metrics.leftSidePadding;
 442         final int y = i.top - metrics.titleBarHeight + 1;
 443         frame.repaint(x, y, fThisButtonSpan, metrics.titleBarHeight - 2);
 444     }
 445 
 446     // Draw all the widgets this frame supports
 447     void drawAllWidgets(final Graphics g, final JInternalFrame frame) {
 448         int x = metrics.leftSidePadding;
 449         int y = (metrics.titleBarHeight - metrics.buttonHeight) / 2 - metrics.titleBarHeight;
 450 
 451         final Insets insets = frame.getInsets();
 452         x += insets.left;
 453         y += insets.top + metrics.downShift;
 454 
 455         final AquaInternalFrameUI ui = (AquaInternalFrameUI)frame.getUI();
 456         final int buttonPressedIndex = ui.getWhichButtonPressed();
 457         final boolean overButton = ui.getMouseOverPressedButton();
 458         final boolean rollover = ui.getRollover();
 459 
 460         final boolean frameSelected = frame.isSelected() || fIsUtility;
 461         final boolean generalActive = rollover || frameSelected;
 462 
 463         final boolean dirty = isDirty(frame);
 464 
 465         paintButton(g, frame, x, y, kCloseButton, buttonPressedIndex, overButton, frame.isClosable(), generalActive, rollover, dirty);
 466 
 467         x += metrics.buttonPadding + metrics.buttonWidth;
 468         paintButton(g, frame, x, y, kIconButton, buttonPressedIndex, overButton, frame.isIconifiable(), generalActive, rollover, false);
 469 
 470         x += metrics.buttonPadding + metrics.buttonWidth;
 471         paintButton(g, frame, x, y, kGrowButton, buttonPressedIndex, overButton, frame.isMaximizable(), generalActive, rollover, false);
 472     }
 473 
 474     public void paintButton(final Graphics g, final JInternalFrame frame, final int x, final int y, final int buttonType, final int buttonPressedIndex, final boolean overButton, final boolean enabled, final boolean active, final boolean anyRollover, final boolean dirty) {
 475         widgetPainter.state.set(getWidget(frame, buttonType));
 476         widgetPainter.state.set(getState(buttonPressedIndex == buttonType && overButton, anyRollover, active, enabled));
 477         widgetPainter.state.set(dirty ? BooleanValue.YES : BooleanValue.NO);
 478         widgetPainter.paint(g, frame, x, y, metrics.buttonWidth, metrics.buttonHeight);
 479     }
 480 
 481     static Widget getWidget(final JInternalFrame frame, final int buttonType) {
 482         switch (buttonType) {
 483             case kIconButton: return Widget.TITLE_BAR_COLLAPSE_BOX;
 484             case kGrowButton: return Widget.TITLE_BAR_ZOOM_BOX;
 485         }
 486 
 487         return Widget.TITLE_BAR_CLOSE_BOX;
 488     }
 489 
 490     static State getState(final boolean pressed, final boolean rollover, final boolean active, final boolean enabled) {
 491         if (!enabled) return State.DISABLED;
 492         if (!active) return State.INACTIVE;
 493         if (pressed) return State.PRESSED;
 494         if (rollover) return State.ROLLOVER;
 495         return State.ACTIVE;
 496     }
 497 
 498     protected void setMetrics(final JInternalFrame frame, final Component window) {
 499         final String title = frame.getTitle();
 500         final FontMetrics fm = frame.getFontMetrics(UIManager.getFont("InternalFrame.titleFont"));
 501         int titleWidth = 0;
 502         int titleHeight = fm.getAscent();
 503         if (title != null) {
 504             titleWidth = SwingUtilities.computeStringWidth(fm, title);
 505         }
 506         // Icon space
 507         final Icon icon = frame.getFrameIcon();
 508         if (icon != null) {
 509             titleWidth += icon.getIconWidth();
 510             titleHeight = Math.max(titleHeight, icon.getIconHeight());
 511         }
 512     }
 513 
 514     protected int getTitleHeight() {
 515         return metrics.titleBarHeight;
 516     }
 517 }