/* * Copyright ©1998-2020 by Richard A. Wilkes. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, version 2.0. If a copy of the MPL was not distributed with * this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, version 2.0. */ package com.trollworks.gcs.ui.widget.dock; import com.trollworks.gcs.menu.file.CloseHandler; import com.trollworks.gcs.ui.UIUtilities; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.LayoutManager; import java.util.ArrayList; import java.util.List; import javax.swing.JPanel; /** * All {@link Dockable}s are wrapped in a {@link DockContainer} when placed within a {@link Dock}. */ public class DockContainer extends JPanel implements DockLayoutNode, LayoutManager { private Dock mDock; private DockHeader mHeader; private List<Dockable> mDockables = new ArrayList<>(); private int mCurrent; private boolean mActive; /** * Creates a new {@link DockContainer} for the specified {@link Dockable}. * * @param dock The {@link Dock} that owns this {@link DockContainer}. * @param dockable The {@link Dockable} to wrap. */ public DockContainer(Dock dock, Dockable dockable) { mDock = dock; setLayout(this); setOpaque(true); setBackground(Color.WHITE); mHeader = new DockHeader(this); add(mHeader); add(dockable); mDockables.add(dockable); mHeader.addTab(dockable, 0); setMinimumSize(new Dimension(0, 0)); } /** @return The {@link Dock} this {@link DockContainer} resides in. */ public Dock getDock() { return mDock; } /** @return The current list of {@link Dockable}s in this {@link DockContainer}. */ public List<Dockable> getDockables() { return mDockables; } /** @param dockable The {@link Dockable} whose title and icon should be updated. */ public void updateTitle(Dockable dockable) { int index = mDockables.indexOf(dockable); if (index != -1) { mHeader.updateTitle(index); } } /** @param dockable The {@link Dockable} to stack into this {@link DockContainer}. */ public void stack(Dockable dockable) { stack(dockable, -1); } /** * @param dockable The {@link Dockable} to stack into this {@link DockContainer}. * @param index The position within this container to place it. Values out of range will * result in the {@link Dockable} being placed at the end. */ public void stack(Dockable dockable, int index) { DockContainer dc = dockable.getDockContainer(); if (dc != null) { if (dc == this && mDockables.size() == 1) { setCurrentDockable(dockable); acquireFocus(); return; } dc.close(dockable); } if (index < 0 || index >= mDockables.size()) { mDockables.add(dockable); } else { mDockables.add(index, dockable); } add(dockable); mHeader.addTab(dockable, mDockables.indexOf(dockable)); setCurrentDockable(dockable); acquireFocus(); } /** Transfers focus to this container if it doesn't already have the focus. */ public void acquireFocus() { Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); Component content = getCurrentDockable(); while (focusOwner != null && focusOwner != content) { focusOwner = focusOwner.getParent(); } if (focusOwner == null) { EventQueue.invokeLater(() -> transferFocus()); } } /** @return The {@link DockHeader} for this {@link DockContainer}. */ public DockHeader getHeader() { return mHeader; } /** * Calls the owning {@link Dock}'s {@link Dock#maximize(DockContainer)} method with this {@link * DockContainer} as the argument. */ public void maximize() { mDock.maximize(this); } /** Calls the owning {@link Dock}'s {@link Dock#restore()} method. */ public void restore() { mDock.restore(); } /** @return The current tab index. */ public int getCurrentTabIndex() { return mCurrent; } /** @return The current {@link Dockable}. */ public Dockable getCurrentDockable() { return mCurrent >= 0 && mCurrent < mDockables.size() ? mDockables.get(mCurrent) : null; } /** @param dockable The {@link Dockable} to make current. */ public void setCurrentDockable(Dockable dockable) { int index = mDockables.indexOf(dockable); if (index != -1) { int wasCurrent = mCurrent; mCurrent = index; for (Dockable one : mDockables) { one.setVisible(dockable == one); } mHeader.revalidate(); repaint(); acquireFocus(); if (mActive && wasCurrent != mCurrent) { dockable.activated(); } } } protected void setCurrentDockable(Dockable dockable, Component focusOn) { int index = mDockables.indexOf(dockable); if (index != -1) { int wasCurrent = mCurrent; mCurrent = index; for (Dockable one : mDockables) { one.setVisible(dockable == one); } mHeader.revalidate(); repaint(); focusOn.requestFocus(); if (mActive && wasCurrent != mCurrent) { dockable.activated(); } } } @Override public String toString() { StringBuilder buffer = new StringBuilder(); if (getParent() == null) { buffer.append("FLOATING "); } buffer.append("Dock Container [x:"); buffer.append(getX()); buffer.append(" y:"); buffer.append(getY()); buffer.append(" w:"); buffer.append(getWidth()); buffer.append(" h:"); buffer.append(getHeight()); int count = mDockables.size(); for (int i = 0; i < count; i++) { buffer.append(' '); if (i == mCurrent) { buffer.append('*'); } buffer.append('d'); buffer.append(i); buffer.append(':'); buffer.append(mDockables.get(i).getTitle()); } buffer.append("]"); return buffer.toString(); } /** * Attempt to close a {@link Dockable} within this {@link DockContainer}. This only has an * affect if the {@link Dockable} is contained by this {@link DockContainer} and implements the * {@link CloseHandler} interface. Note that the {@link CloseHandler} must call this {@link * DockContainer}'s {@link #close(Dockable)} method to actually close the tab. */ public void attemptClose(Dockable dockable) { if (dockable instanceof CloseHandler) { if (mDockables.contains(dockable)) { CloseHandler closeable = (CloseHandler) dockable; if (closeable.mayAttemptClose()) { closeable.attemptClose(); } } } } /** * Closes the specified {@link Dockable}. If the last {@link Dockable} within this {@link * DockContainer} is closed, then this {@link DockContainer} is also removed from the {@link * Dock}. */ public void close(Dockable dockable) { int index = mDockables.indexOf(dockable); if (index != -1) { remove(dockable); mDockables.remove(dockable); mHeader.close(dockable); if (mDockables.isEmpty()) { restore(); mDock.remove(this); mDock.revalidate(); mDock.repaint(); mDock = null; } else { if (index > 0) { index--; } setCurrentDockable(mDockables.get(index)); } } } /** * @return {@code true} if this {@link DockContainer} or one of its children has the keyboard * focus. */ public boolean isActive() { return mActive; } /** Called by the {@link Dock} to update the active highlight. */ void updateActiveHighlight() { boolean active = UIUtilities.getSelfOrAncestorOfType(KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(), DockContainer.class) == this; if (mActive != active) { mActive = active; mHeader.repaint(); if (mActive) { Dockable dockable = getCurrentDockable(); if (dockable != null) { dockable.activated(); } } } } @Override public void addLayoutComponent(String name, Component comp) { // Unused } @Override public void removeLayoutComponent(Component comp) { // Unused } @Override public Dimension preferredLayoutSize(Container parent) { Dimension size = mHeader.getPreferredSize(); int width = size.width; int height = size.height; if (!mDockables.isEmpty()) { size = getCurrentDockable().getPreferredSize(); if (width < size.width) { width = size.width; } height += size.height; } Insets insets = parent.getInsets(); return new Dimension(insets.left + width + insets.right, insets.top + height + insets.bottom); } @Override public Dimension minimumLayoutSize(Container parent) { Dimension size = mHeader.getMinimumSize(); int width = size.width; int height = size.height; Dockable current = getCurrentDockable(); if (current != null) { size = current.getMinimumSize(); if (width < size.width) { width = size.width; } height += size.height; } Insets insets = parent.getInsets(); return new Dimension(insets.left + width + insets.right, insets.top + height + insets.bottom); } @Override public void layoutContainer(Container parent) { Insets insets = parent.getInsets(); int height = mHeader.getPreferredSize().height; int width = parent.getWidth() - (insets.left + insets.right); mHeader.setBounds(insets.left, insets.top, width, height); Dockable current = getCurrentDockable(); if (current != null) { int remaining = getHeight() - (insets.top + height); if (remaining < 0) { remaining = 0; } current.setBounds(insets.left, insets.top + height, width, remaining); } } }