com.pekinsoft.wizard.api.WizardSideBar Maven / Gradle / Ivy
Show all versions of application-framework-api Show documentation
/*
* Copyright (C) 2022 PekinSOFT Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* *****************************************************************************
* Project : ntos
* Class : WizardSideBar.java
* Author : Sean Carrick
* Created : Dec 16, 2022
* Modified : Dec 16, 2022
*
* Purpose: See class JavaDoc for explanation
*
* Revision History:
*
* WHEN BY REASON
* ------------ ------------------- -----------------------------------------
* Dec 16, 2022 Sean Carrick Initial creation.
* *****************************************************************************
*/
package com.pekinsoft.wizard.api;
import com.pekinsoft.utils.ColorUtils;
import com.pekinsoft.wizard.api.displayer.InstructionsPanel;
import com.pekinsoft.wizard.spi.Wizard;
import com.pekinsoft.wizard.spi.WizardObserver;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.IllegalComponentStateException;
import java.awt.Insets;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Locale;
import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.accessibility.AccessibleState;
import javax.accessibility.AccessibleStateSet;
import javax.accessibility.AccessibleText;
import javax.imageio.ImageIO;
import javax.swing.CellRendererPane;
import javax.swing.JEditorPane;
import javax.swing.UIManager;
import com.pekinsoft.framework.Application;
import com.pekinsoft.framework.ResourceMap;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/**
* The {@code WizardSideBar} class is an extension of the {@link
* javax.swing.JPanel JPanel} class that provides for painting of a background
* image and the step descriptions of a Wizard. Instances of this class should
* be added to a Wizard dialog in the {@link java.awt.BorderLayout#WEST
* BorderLayout.WEST} position.
*
* The background image on the sidebar panel can be changed in three ways:
*
* - Place a {@link java.awt.image.BufferedImage BufferedImage} into the
* {@link javax.swing.UIManager UIManager} with the property key
* "wizard.sidebar.background".
*
- Create a {@link java.lang.System System} property named
* "wizard.sidebar.background" with the value the path to the image, which may be a
* path to an image resource in your JAr file.
*
- As a last resort, you can try placing a resource entry named
* "wizard.sidebar.background" in your calling class' {@link
* com.pekinsoft.framework.ResourceMap ResourceMap} properties file.
*
*
* If a {@code BufferedImage} or its path is not found in any of these places,
* the default background image will be used. The default image is designed in
* such a way that it will still look nice, even if it is skewed by the size of
* the sidebar panel. This image is light-colored, yellow and gold, with hints
* of red and blue in it. If you desire to have a more customized sidebar image,
* we provide that ability as described below.
*
* To customize the sidebar image, you can place a custom wizard image in the
* same places as described above (i.e., the {@code UIManager}, {@code system}
* properties, or {@code ResourceMap} for you class), under the property key
* "wizard.sidebar.image". The image pointed to should be no larger than 180x180
* to look nice on the sidebar. This image will be painted only at its size and
* will never become skewed due to the sidebar being larger or smaller. This
* image is painted to the bottom-right corner of the sidebar, with the {@code x}
* location being the width of the sidebar minus the width of the image. The
* {@code y} location will be the height of the sidebar minus the height of the
* image. The supplied image should have a transparent background, with portable
* network graphics (PNG) files working the best.
*
* @author Sean Carrick
*/
public class WizardSideBar
extends javax.swing.JPanel
implements PropertyChangeListener, WizardObserver, InstructionsPanel {
public static final String PROP_WIZARD_SIDEBAR_IMAGE = "wizard.sidebar.image";
private static final long serialVersionUID = 4441673040479415503L;
private static final Logger logger = System.getLogger(WizardSideBar.class.getName());
private static final int MARGIN = 5;
private static final String PROP_WIZARD_SIDEBAR_BACKGROUND = "wizard.sidebar.background";
private static final ResourceMap resourceMap = Application
.getInstance()
.getContext()
.getResourceMap(WizardSideBar.class);
private final Wizard wizard;
private final BufferedImage background;
private final BufferedImage img;
private int currentStep = 0;
private boolean inSummaryPage = false;
/**
* Constructs a new {@code WizardSideBar} instance with the given
* properties.
*
* @param wizard the Wizard in which this side bar is being displayed
*/
public WizardSideBar(Wizard wizard) {
this(null, wizard);
Font f = UIManager.getFont ("Tree.font"); //NOI18N
if (f != null) {
setFont (f);
}
}
/**
* Get the wizard this panel is monitoring.
* @return
*/
protected final Wizard getWizard() {
return wizard;
}
public final Container getComponent() {
return this;
}
/**
* Overridden to start listening to the wizard when added to a container
*/
@Override
public void addNotify() {
super.addNotify();
wizard.addWizardObserver (this);
}
/**
* Overridden to stop listening to the wizard when removed from a container
*/
@Override
public void removeNotify() {
wizard.removeWizardObserver (this);
super.removeNotify();
}
public WizardSideBar(BufferedImage img, Wizard wizard) {
if (img == null) {
// In the event of classloader issues, also have a way to get the
//+ image from the UIManager - slightly more portable for large
//+ applications.
img = (BufferedImage) UIManager.get(PROP_WIZARD_SIDEBAR_IMAGE);
}
String imgStr = System.getProperty(PROP_WIZARD_SIDEBAR_IMAGE);
// Image has not been loaded and user wishes to supply their own image.
if (img == null && imgStr != null) {
// Get a URL, works for JArs.
ClassLoader cl = this.getClass().getClassLoader();
URL url = cl.getResource(imgStr);
if (url == null) {
// Let's try getting it from the Wizard's classloader.
url = wizard.getClass().getClassLoader().getResource(imgStr);
}
if (url == null) {
// One last attempt: Let's see if we can get it from the wizard's
//+ ResourceMap:
try {
url = new URL(Application
.getInstance()
.getContext()
.getResourceMap(wizard.getClass())
.getString(PROP_WIZARD_SIDEBAR_IMAGE));
} catch (MalformedURLException mue) {
}
}
// successfully parsed the URL
if (url != null) {
try {
img = ImageIO.read(url);
} catch (IOException ioe) {
logger.log(Level.WARNING, "Could not load wizard image {0}", ioe,
ioe.getMessage()); // NOI18N
System.setProperty(PROP_WIZARD_SIDEBAR_IMAGE, null); // NOI18N
img = null; // Error loading image, set to {@code null} to not use.
}
} else {
logger.log(Level.WARNING, "Bad URL for wizard image ", imgStr);
System.setProperty(PROP_WIZARD_SIDEBAR_IMAGE, null);
img = null;
}
}
BufferedImage background = null;
if (background == null) {
// In the event of classloader issues, also have a way to get the
//+ image from the UIManager - slightly more portable for large
//+ applications.
background = (BufferedImage) UIManager.get(PROP_WIZARD_SIDEBAR_BACKGROUND);
}
String bgStr = System.getProperty(PROP_WIZARD_SIDEBAR_BACKGROUND);
// Image has not been loaded and user wishes to supply their own image.
if (background == null && bgStr != null) {
// Get a URL, works for JArs.
ClassLoader cl = this.getClass().getClassLoader();
URL url = cl.getResource(bgStr);
if (url == null) {
// Let's try getting it from the Wizard's classloader.
url = wizard.getClass().getClassLoader().getResource(bgStr);
}
if (url == null) {
// One last attempt: Let's see if we can get it from the wizard's
//+ ResourceMap:
try {
url = new URL(Application
.getInstance()
.getContext()
.getResourceMap(wizard.getClass())
.getString(PROP_WIZARD_SIDEBAR_BACKGROUND));
} catch (MalformedURLException mue) {
}
}
// successfully parsed the URL
if (url != null) {
try {
background = ImageIO.read(url);
} catch (IOException ioe) {
logger.log(Level.WARNING, "Could not load wizard image {0}", ioe,
ioe.getMessage()); // NOI18N
System.setProperty(PROP_WIZARD_SIDEBAR_BACKGROUND, null); // NOI18N
background = null; // Error loading image, set to {@code null} to not use.
}
} else {
logger.log(Level.WARNING, "Bad URL for wizard image ", bgStr);
System.setProperty(PROP_WIZARD_SIDEBAR_BACKGROUND, null);
background = null;
}
}
this.img = img;
this.background = background;
this.wizard = wizard;
initComponents();
}
@Override
public boolean isOpaque() {
return background != null;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName() != null) {
switch (evt.getPropertyName()) {
case "stepsChanged":
currentStep = (int) evt.getNewValue();
break;
case "inSummaryPage":
break;
}
}
repaint();
}
public void setInSummaryPage(boolean inSummaryPage) {
boolean oldValue = this.inSummaryPage;
this.inSummaryPage = inSummaryPage;
firePropertyChange("inSummaryPage", oldValue, this.inSummaryPage);
repaint();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// //GEN-BEGIN:initComponents
private void initComponents() {
setMinimumSize(new java.awt.Dimension(198, 361));
setPreferredSize(new java.awt.Dimension(198, 361));
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 198, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 361, Short.MAX_VALUE)
);
}// //GEN-END:initComponents
private BufferedImage getImage() {
return img;
}
private BufferedImage getBgImage() {
return background;
}
/**
* Paints the background image for this component, or fills the background
* with a color if no image present.
*
* @param g A Graphic2D into which we can paint
* @param x The x coordinate of the area that should contain the image
* @param y The y coordinate of the area that should contain the image
* @param w The width of the area that should contain the image
* @param h The height of the area that should contain the image
*/
protected void paintBackground(Graphics2D g, int x, int y, int w, int h) {
BufferedImage image = getBgImage();
if (image != null) {
g.drawImage(image, x, y, w, h, this);
} else {
Color c = g.getColor();
g.setColor(Color.WHITE);
g.fillRect(x, y, w, h);
g.setColor(c);
}
}
/**
* Paints the custom image for this component, unless no image is present.
*
* @param g a {@code Graphics2D} object into which we can paint
*/
protected void paintImage(Graphics2D g) {
BufferedImage image = getImage();
int x = 0;
int y = 0;
int w = 0;
int h = 0;
if (image != null) {
x = getWidth() - image.getWidth();
y = getHeight() - image.getHeight();
w = image.getWidth();
h = image.getHeight();
}
if (image != null) {
g.drawImage(image, x, y, w, h, this);
}
}
String[] steps = new String[0];
@Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
Font f = getFont() != null ? getFont() : UIManager.getFont("controlFont"); //NOI18N
FontMetrics fm = g.getFontMetrics (f);
Insets ins = getInsets();
int dx = ins.left;
int dy = ins.top;
int w = getWidth() - (ins. left + ins.right);
int hh = getHeight() - (ins.top + ins.bottom);
paintBackground(g2d, dx, dy, w, hh);
paintImage(g2d);
color = g2d.getColor();
g2d.setColor(getGoodColor());
String currentStep = wizard.getCurrentStep();
if (!inSummaryPage) {
//Don't fetch step list if in summary page, there will
//only be the base ones
steps = wizard.getAllSteps();
}
String steps[] = this.steps;
if (inSummaryPage) {
String summaryStep = resourceMap.getString("sideBar.title"); //NOI18N
String[] nue = new String[steps.length + 1];
System.arraycopy(steps, 0, nue, 0, steps.length);
nue[nue.length - 1] = summaryStep;
steps = nue;
}
int y = fm.getMaxAscent() + ins.top + MARGIN;
int x = ins.left + MARGIN;
int h = fm.getMaxAscent() + fm.getMaxDescent() + 3;
Font boldFont = f.deriveFont (Font.BOLD);
g.setFont (boldFont);
g.drawString (resourceMap.getString("sideBar.title"), x, y); //NOI18N
int underlineY = ins.top + MARGIN + fm.getAscent() + 3;
g.drawLine (x, underlineY, x + (getWidth() - (x + ins.left + MARGIN)),
underlineY);
int bottom = getComponentCount() == 0 ? getHeight() - getInsets().bottom :
getHeight() - getInsets().bottom - getComponents()[0].getPreferredSize().height;
y += h + 10;
int first = 0;
int stop = steps.length;
String elipsis = resourceMap.getString("ellipsis"); //NOI18N
boolean wontFit = y + (h * (steps.length)) > getHeight();
if (wontFit) {
//try to center the current step
int availHeight = bottom - y;
int willFit = availHeight / h;
int currStepIndex = Arrays.asList (steps).indexOf(currentStep);
int rangeStart = Math.max (0, currStepIndex - (willFit / 2));
int rangeEnd = Math.min (rangeStart + willFit, steps.length);
if (rangeStart + willFit > steps.length) {
//Don't scroll off if there's room
rangeStart = steps.length - willFit;
rangeEnd = steps.length;
}
steps = (String[]) steps.clone();
if (rangeStart != 0) {
steps[rangeStart] = elipsis;
first = rangeStart;
}
if (rangeEnd != steps.length && rangeEnd > 0) {
steps[rangeEnd - 1] = elipsis;
stop = rangeEnd;
}
}
/*
if (wontFit) {
int currStepIndex = Arrays.asList (steps).indexOf(currentStep);
if (currStepIndex != -1) { //shouldn't happen
steps = (String[]) steps.clone();
first = Math.max (0, currStepIndex - 2);
if (first != 0) {
if (y + ((currStepIndex - first) * h) > getHeight()) {
//Best effort to keep current step on screen
first++;
}
if (first != currStepIndex) {
steps[first] = elipsis;
}
}
}
}
if (y + ((stop - first) * h) > bottom) {
int avail = bottom - y;
int willFit = avail / h;
int last = first + willFit - 1;
if (last < steps.length - 1) {
steps[last] = elipsis;
stop = last + 1;
}
}
*/
g.setFont (getFont());
g.setColor (getGoodColor());
for (int i=first; i < stop; i++) {
boolean isUndetermined = Wizard.UNDETERMINED_STEP.equals(steps[i]);
boolean canOnlyFinish = wizard.getForwardNavigationMode() ==
Wizard.MODE_CAN_FINISH;
if (isUndetermined && canOnlyFinish) {
break;
}
String curr;
if (!elipsis.equals(steps[i])) {
if (inSummaryPage && i == this.steps.length) {
curr = (i + 1) + ". " + steps[i];
} else {
curr = (i + 1) + ". " + (isUndetermined ?
resourceMap.getString("ellipsis") : //NOI18N
steps[i].equals(elipsis) ? elipsis :
wizard.getStepDescription(steps[i])); //NOI18N
}
} else {
curr = elipsis;
}
if (curr != null) {
boolean selected = (steps[i].equals (currentStep) && !inSummaryPage) ||
(inSummaryPage && i == steps.length - 1);
if (selected) {
g.setFont (boldFont);
}
int width = fm.stringWidth(curr);
while (width > getWidth() - (ins.left + ins.right) && curr.length() > 5) {
curr = curr.substring(0, curr.length() - 5) +
resourceMap.getString("ellipsis"); //NOI18N
}
g.drawString (curr, x, y);
if (selected) {
g.setFont (f);
}
y += h;
}
}
}
private Color color;
private Color getGoodColor() {
Color c = (UIManager.getColor("textText") == null)
? UIManager.getColor("controlText")
: UIManager.getColor("textText");
if (!ColorUtils.isDarkColor(c)) {
c = Color.darkGray;
}
return c;
}
private int historicWidth = Integer.MIN_VALUE;
@Override
public final Dimension getPreferredSize() {
Font f = getFont() != null ? getFont() // NOI18N
: UIManager.getFont("controlFont"); // NOI18N
Graphics g = getGraphics();
if (g == null) {
g = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics();
}
f = f.deriveFont(Font.BOLD);
FontMetrics fm = g.getFontMetrics(f);
Insets ins = getInsets();
int h = fm.getHeight();
String[] steps = wizard.getAllSteps();
int w = Integer.MIN_VALUE;
int i = 1;
for (String step : steps) {
String desc = (i++) + ". " + (Wizard.UNDETERMINED_STEP.equals(step)
? resourceMap.getString("ellipsis")
: wizard.getStepDescription(step));
if (desc != null) {
w = Math.max(w, fm.stringWidth(desc) + MARGIN);
}
}
if (Integer.MIN_VALUE == 2) {
w = 350;
}
BufferedImage img = getBgImage();
if (img != null) {
w = Math.max(w, img.getWidth());
}
// Make sure we can grow but not shrink.
w = Math.max(w, historicWidth);
historicWidth = w;
return new Dimension(w, ins.top + ins.bottom + ((h + 3) * steps.length));
}
@Override
public final Dimension getMinimumSize() {
return getPreferredSize();
}
public void stepsChanged(Wizard wizard) {
repaint();
}
public void navigabilityChanged(Wizard wizard) {
// do nothing
}
public void selectionChanged(Wizard wizard) {
repaint();
}
@Override
public final void doLayout() {
Component[] c = getComponents();
Insets ins = getInsets();
int y = getHeight() - (MARGIN + ins.bottom);
int x = MARGIN + ins.left;
int w = getWidth() - ((MARGIN * 2) + ins.left + ins.right);
if (w < 0) w = 0;
if (c.length > 0) {
for (int i = c.length; i >= 0; i--) {
Dimension d = c[i].getPreferredSize();
c[i].setBounds(x, y - d.height, w, d.height);
y -= d.height;
}
}
}
@Override
public final AccessibleContext getAccessibleContext() {
return new ACI(this);
}
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
private static final class ACI extends AccessibleContext {
private final Wizard wizard;
private final WizardSideBar panel;
public ACI(WizardSideBar pnl) {
this.wizard = pnl.wizard;
panel = pnl;
if (pnl.getParent() instanceof Accessible) {
setAccessibleParent((Accessible) pnl.getParent());
}
setAccessibleName(resourceMap.getString("ACN_WizardSideBar"));
setAccessibleDescription(resourceMap.getString("ACSD_WizardSideBar"));
}
JEditorPane pane;
public AccessibleText getAccessibleText() {
if (pane == null) {
// Cheat just a bit here - will do for now - the text is there,
//+ more or less where it should be, and a screen reader should
//+ be able to find it; exact bounds do not make much difference.
pane = new JEditorPane();
pane.setBounds(panel.getBounds());
pane.getAccessibleContext().getAccessibleText();
pane.setFont(panel.getFont());
CellRendererPane cell = new CellRendererPane();
cell.add(pane);
}
pane.setText(getText());
pane.selectAll();
pane.validate();
return pane.getAccessibleContext().getAccessibleText();
}
public String getText() {
StringBuilder sb = new StringBuilder();
String[] s = wizard.getAllSteps();
for (String a : s) {
sb.append(a).append("\n");
}
return sb.toString();
}
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.LIST;
}
@Override
public AccessibleStateSet getAccessibleStateSet() {
AccessibleState[] states = new AccessibleState[] {
AccessibleState.VISIBLE,
AccessibleState.OPAQUE,
AccessibleState.SHOWING,
AccessibleState.MULTI_LINE
};
return new AccessibleStateSet(states);
}
@Override
public int getAccessibleIndexInParent() {
return -1;
}
@Override
public int getAccessibleChildrenCount() {
return 0;
}
@Override
public Accessible getAccessibleChild(int i) {
throw new IndexOutOfBoundsException(i);
}
@Override
public Locale getLocale() throws IllegalComponentStateException {
return Locale.getDefault();
}
}
}