package edu.hws.eck.mdbfx;
import javafx.scene.control.MenuBar;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Alert;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.stage.FileChooser;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.Optional;
import org.w3c.dom.*;
import java.io.*;
import java.net.URL;
/**
* This class defines a MenuBar for use with a MandelbrotPane. This is a large
* and complex class because it includes many nested classes and event handlers that
* do the work of carrying out menu commands. However, most of the complexity is in
* the private part of the class, and the class has only a small public interface.
*/
public class Menus extends MenuBar {
/**
* This is the list of example files for the Examples menu. The items in the
* menu are the strings from this array. For a string str in the array,
* a resource file name is constructed as: "edu/hws/edu/mdbfx/examples" + str + ".mdb".
* The resulting names must be names of resource files that are accessible
* to the program. The files should be settings files for the Mandelbrot
* Viewer program. When the user selects an item from the Examples menu,
* the corresponding file is loaded and applied to the display.
*/
private static final String[] SETTINGS_FILE_LIST = {
"settings1", "settings2", "settings3", "settings4", "settings5",
"settings6", "settings7", "settings8", "settings9", "settings10",
"settings11", "settings12"
};
/**
* Constructor creates the menu bar containing commands that apply to a MandelbrotPane.
* The menu bar contains File, MaxIterations, Palette, PaletteLength, and Example menus.
* @param owner the MandelbrotPane that will be managed by this menu bar. This
* variable is saved for later access to the MandelbrotPane.
*/
public Menus(MandelbrotPane owner) {
this.owner = owner;
paletteManager = new PaletteManager(); // Defines the Palette menu.
paletteLengthManager = new PaletteLengthManager(); // Defines the PaletteLength menu.
maxIterationsManager = new MaxIterationsManager(); // Defines the MaxIterations menu.
// Create menus and add them to the menu bar.
Menu fileMenu = new Menu(I18n.tr("menu.file"));
Menu controlMenu = new Menu(I18n.tr("menu.control"));
Menu maxIterationsMenu = new Menu(I18n.tr("menu.maxIterations"));
Menu paletteMenu = new Menu(I18n.tr("menu.palette"));
Menu paletteLengthMenu = new Menu(I18n.tr("menu.paletteLength"));
Menu exampleMenu = new Menu(I18n.tr("menu.examples"));
getMenus().addAll(fileMenu,controlMenu,maxIterationsMenu,paletteMenu,
paletteLengthMenu,exampleMenu);
// Add items to the File menu.
MenuItem saveParams = new MenuItem(I18n.tr("command.save"));
saveParams.setOnAction( e -> doSaveParams() );
saveParams.setAccelerator(KeyCombination.valueOf("shortcut+S"));
MenuItem openParams = new MenuItem(I18n.tr("command.open"));
openParams.setOnAction( e -> doOpenParams() );
openParams.setAccelerator(KeyCombination.valueOf("shortcut+O"));
MenuItem saveImage = new MenuItem(I18n.tr("command.saveImage"));
saveImage.setOnAction( e -> doSaveImage() );
saveImage.setAccelerator(KeyCombination.valueOf("shortcut+shift+S"));
MenuItem close = new MenuItem(I18n.tr("command.quit"));
close.setOnAction( e -> Platform.exit() );
close.setAccelerator(KeyCombination.valueOf("shortcut+Q"));
fileMenu.getItems().addAll(saveParams,openParams,
new SeparatorMenuItem(), saveImage, new SeparatorMenuItem(), close);
// Add items to the Control menu.
MenuItem allDefaults = new MenuItem(I18n.tr("command.restoreAllDefaults"));
allDefaults.setOnAction( e -> doAllDefaults() );
allDefaults.setAccelerator(KeyCombination.valueOf("shortcut+shift+R"));
MenuItem defaultLimits = new MenuItem(I18n.tr("command.defaultLimits"));
defaultLimits.setOnAction( e -> doDefaultLimits() );
defaultLimits.setAccelerator(KeyCombination.valueOf("shortcut+R"));
MenuItem undoChangeOfLimits = new MenuItem(I18n.tr("Restore Previous Limits"));
undoChangeOfLimits.setOnAction( e -> doUndoChangeOfLimits() );
undoChangeOfLimits.setAccelerator(KeyCombination.valueOf("shortcut+U"));
undoChangeOfLimits.setDisable(true);
MenuItem showLimits = new MenuItem(I18n.tr("command.showLimits"));
showLimits.setOnAction( e -> doShowLimits() );
showLimits.setAccelerator(KeyCombination.valueOf("shortcut+L"));
MenuItem setLimits = new MenuItem(I18n.tr("command.enterLimits"));
setLimits.setOnAction( e -> doSetLimits() );
setLimits.setAccelerator(KeyCombination.valueOf("shortcut+shift+L"));
MenuItem setImageSize = new MenuItem(I18n.tr("command.enterImageSize"));
setImageSize.setOnAction( e -> doSetImageSize() );
setImageSize.setAccelerator(KeyCombination.valueOf("shortcut+I"));
controlMenu.getItems().addAll( allDefaults, new SeparatorMenuItem(), defaultLimits, undoChangeOfLimits,
showLimits, setLimits, new SeparatorMenuItem(), setImageSize);
// Add items to the other three menus. These are created by the "manager" objects.
// and by the fillExampleMenu() method.
paletteMenu.getItems().addAll(paletteManager.items);
paletteLengthMenu.getItems().addAll(paletteLengthManager.items);
maxIterationsMenu.getItems().addAll(maxIterationsManager.items);
fillExampleMenu(exampleMenu);
// Some commands are disabled when a computation is in progress in the display.
saveImage.disableProperty().bind(owner.getDisplay().workingProperty());
saveParams.disableProperty().bind(owner.getDisplay().workingProperty());
openParams.disableProperty().bind(owner.getDisplay().workingProperty());
setLimits.disableProperty().bind(owner.getDisplay().workingProperty());
setImageSize.disableProperty().bind(owner.getDisplay().workingProperty());
owner.limitsProperty().addListener( (o,oldVal,newVal) -> {
// Save old value of limitsProperty for use in "Restore Previous Limits".
previousLimits = oldVal;
undoChangeOfLimits.setDisable( previousLimits == null );
});
} // end constructor
/**
* If one of the save or open commands has been used to save or load a file, then
* fileDialogProperty will be the directory that contained the saved or opened file.
* This method returns an absolute path name for that selected directory. It is used
* by Main.java to find out the selected directory when the program ends.
* The directory is saved in user preferences and is restored the next time
* the program is run.
*/
public String getSelectedDirectoryInFileChooser() {
if (fileDialogDirectory == null)
return null;
else
return fileDialogDirectory.getAbsolutePath();
}
/**
* This sets the selected directory for the file dialog. This method
* is called by Main.java when the program starts, to restore the directory
* that was saved the last time the program was run (by the same user).
* @param path absolute path name to the directory; if this is
* not the path name of an actual directory, then the property
* is not set.
*/
public void setSelectedDirectoryInFileChooser(String path) {
File dir = new File(path);
if (dir.isDirectory()) {
fileDialogDirectory = dir;
}
}
/**
* Produces an XML representation of the current settings. This is used by the
* doOpenParams() method to restore the setting of the program based on the contents
* of an XML file. It is also used for the Examples menu. Currently, the image size
* is NOT adjusted to the value in the file; the same picture that was saved is shown,
* but possibly at a different size. No changes are made if an error occurs while
* processing the file. An exception is thrown in that case.
*/
public void retrieveSettingsFromXML(Document xmlDoc) {
Element docElement = xmlDoc.getDocumentElement();
String docName = docElement.getTagName();
if (! docName.equalsIgnoreCase("mandelbrot_settings"))
throw new IllegalArgumentException(I18n.tr("xml.error.wrongType",docName));
String version = docElement.getAttribute("version");
if ( ! version.equalsIgnoreCase("edu.hws.eck.mdb/1.0"))
throw new IllegalArgumentException(I18n.tr("xml.error.wrongSettingsVersion"));
NodeList nodes = docElement.getChildNodes();
int ct = nodes.getLength();
/* Default values will be used if no value is found in the file, */
int paletteItemNum = 0;
int paletteType = MandelbrotPane.PALETTE_SPECTRUM;
Color c1 = null, c2 = null; // for gradient palettes
int paletteLength = 0;
int maxIterations = 250;
double[] limits = new double[] { -2.5,1.1,-1.35,1.35 };
for (int i = 0; i < ct; i++) {
Node node = nodes.item(i);
if (node instanceof Element) {
String name = ((Element)node).getTagName();
String value = ((Element)node).getAttribute("value");
try {
if (name.equalsIgnoreCase("palettetype")) {
Object[] paletteData = paletteManager.getValueFromString(value);
paletteItemNum = (Integer)paletteData[0];
paletteType = (Integer)paletteData[1];
if (paletteType == MandelbrotPane.PALETTE_GRADIENT) {
c1 = (Color)paletteData[2];
c2 = (Color)paletteData[3];
}
}
else if (name.equalsIgnoreCase("palettelength"))
paletteLength = paletteLengthManager.getValueFromString(value);
else if (name.equalsIgnoreCase("maxiterations"))
maxIterations = maxIterationsManager.getValueFromString(value);
else if (name.equalsIgnoreCase("limits")) {
String[] limitStrings = value.split(",");
double xmin = Double.parseDouble(limitStrings[0]);
double xmax = Double.parseDouble(limitStrings[1]);
double ymin = Double.parseDouble(limitStrings[2]);
double ymax = Double.parseDouble(limitStrings[3]);
if (xmin >= xmax || ymin >= ymax)
throw new IllegalArgumentException();
limits = new double[] { xmin, xmax, ymin, ymax };
}
}
catch (Exception e) {
throw new IllegalArgumentException(I18n.tr("xml.error.illegalSettingsValue",name,value));
}
}
}
owner.setParams(maxIterations,paletteType,c1,c2,paletteLength,limits);
paletteManager.setItemNum(paletteItemNum);
paletteLengthManager.setValue(paletteLength);
maxIterationsManager.setValue(maxIterations);
}
/**
* This is used by the Save Params action to create an XML representation of
* the current settings. (It is public but is not currently used outside this class.)
*/
public String currentSettingsAsXML() {
StringBuffer buffer = new StringBuffer();
buffer.append("\n");
buffer.append("\n");
double[] limits = owner.getRequestedLimits();
String limitString = limits[0] + "," + limits[1] + "," + limits[2] + "," + limits[3];
buffer.append("\n");
String sizeString = owner.getDisplay().getWidth() + "," + owner.getDisplay().getHeight();
buffer.append("\n");
buffer.append("\n");
buffer.append("\n");
buffer.append("\n");
buffer.append("\n");
return buffer.toString();
}
//------------------ Everything after this point is private --------------------------
private MandelbrotPane owner; // From the parameter to the constructor.
private PaletteManager paletteManager; // Manages Palette menu; defined by nested class below.
private PaletteLengthManager paletteLengthManager; // Manages PaletteLength menu; defined by nested class below.
private MaxIterationsManager maxIterationsManager; // Manages MaxIterations menu; defined by nested class below.
private File fileDialogDirectory; // Save selected directory from fileDialog.
private double[] previousLimits; // For the Restore Previous Limits command.
/**
* A little utility method that makes strings out of the xy-limits on the display,
* where the lengths of the string is adjusted depending on the distance between
* xmax and xmin. The idea is to try to avoid more digits after the decimal
* points than makes sense. If it succeeds, the coordinates that are shown for xmin
* and xmax should differ only in their last four or five digits and the same should
* also be true for ymin and ymax.
* @return An array of 4 strings representing the values of xmin, xmax, ymin, ymax.
*/
private String[] makeScaledLimitStrings() {
double[] limits = owner.getLimits();
double xmin = limits[0];
double xmax = limits[1];
double ymin = limits[2];
double ymax = limits[3];
double diff = xmax - xmin;
if (diff == 0)
return new String[] { ""+xmin, ""+xmax, ""+ymin, ""+ymax };
int scale = 4;
if (diff > 0) {
while (diff < 1) {
scale++;
diff *= 10;
}
}
String fmt = "%1." + scale + "f";
String[] str = new String[4];
str[0] = String.format(fmt,xmin);
str[1] = String.format(fmt,xmax);
str[2] = String.format(fmt,ymin);
str[3] = String.format(fmt,ymax);
return str;
}
/**
* Save an XML file representing the current settings of the program.
* The XML file is in the format that is loaded by doOpenParams.
*/
private void doSaveParams() {
FileChooser fileDialog = new FileChooser();
fileDialog.setInitialFileName(I18n.tr("files.saveparams.defaultFileName"));
if (fileDialogDirectory == null)
fileDialog.setInitialDirectory(new File(System.getProperty("user.home")));
else
fileDialog.setInitialDirectory(fileDialogDirectory);
fileDialog.setTitle(I18n.tr("files.saveparams.title"));
File selectedFile = fileDialog.showSaveDialog(owner.getScene().getWindow());
if (selectedFile == null)
return;
PrintWriter out;
try {
FileOutputStream stream = new FileOutputStream(selectedFile);
out = new PrintWriter( stream );
}
catch (Exception e) {
error(I18n.tr("files.saveparams.error.cannotOpen",
selectedFile.getName(), e.toString()));
return;
}
try {
out.print(currentSettingsAsXML());
out.close();
try {
File dir = selectedFile.getParentFile();
if (dir.isDirectory())
fileDialogDirectory = dir;
}
catch (Exception e) {
}
}
catch (Exception e) {
error(I18n.tr("files.saveparams.error.cannotWrite",
selectedFile.getName(), e.toString()));
}
}
/**
* Open an XML file and, if it is successfully parsed, restore the
* image to the settings that were stored in the file.
*/
private void doOpenParams() {
FileChooser fileDialog = new FileChooser();
fileDialog.setTitle(I18n.tr("files.openparams.title"));
if (fileDialogDirectory == null)
fileDialog.setInitialDirectory(new File(System.getProperty("user.home")));
else
fileDialog.setInitialDirectory(fileDialogDirectory);
File selectedFile = fileDialog.showOpenDialog(owner.getScene().getWindow());
if (selectedFile == null)
return; // User canceled or clicked the dialog's close box.
Document xmldoc;
try {
DocumentBuilder docReader = DocumentBuilderFactory.newInstance().newDocumentBuilder();
xmldoc = docReader.parse(selectedFile);
}
catch (Exception e) {
error(I18n.tr("files.openparams.error.notXML", selectedFile.getName(), e.toString()));
return;
}
try {
retrieveSettingsFromXML(xmldoc);
try {
File dir = selectedFile.getParentFile();
if (dir.isDirectory())
fileDialogDirectory = dir;
}
catch (Exception e) {
}
}
catch (Exception e) {
error(I18n.tr("files.openparams.error.notParamsFile", selectedFile.getName(), e.getMessage()));
}
}
/**
* Save the current image as a PNG file. Only the PNG file format is available.
*/
public void doSaveImage() {
FileChooser fileDialog = new FileChooser();
fileDialog.setInitialFileName(I18n.tr("files.saveimage.defaultFileName"));
if (fileDialogDirectory == null)
fileDialog.setInitialDirectory(new File(System.getProperty("user.home")));
else
fileDialog.setInitialDirectory(fileDialogDirectory);
fileDialog.setTitle(I18n.tr("files.saveimage.title"));
File selectedFile = fileDialog.showSaveDialog(owner.getScene().getWindow());
if ( selectedFile == null )
return; // User did not select a file.
try {
Image canvasImage = owner.getDisplay().snapshot(null,null);
BufferedImage image = SwingFXUtils.fromFXImage(canvasImage,null);
String filename = selectedFile.getName().toLowerCase();
if ( ! filename.endsWith(".png")) {
error(I18n.tr("file.saveimage.pngOnly"));
return;
}
boolean hasFormat = ImageIO.write(image,"PNG",selectedFile);
if ( ! hasFormat ) { // (this should never happen)
error(I18n.tr("files.saveimage.noPNG"));
}
try {
File dir = selectedFile.getParentFile();
if (dir.isDirectory())
fileDialogDirectory = dir;
}
catch (Exception e) {
}
}
catch (Exception e) {
error(I18n.tr("files.saveimage.cantwrite", selectedFile.getName(), e.toString()));
}
}
/**
* Restores limits to their default values, showing the entire Mandelbrot Set.
*/
private void doDefaultLimits() {
owner.setLimits(-2.5,1.1,-1.35,1.35);
}
/**
* Restores default limits, palette, and maxIterations.
*/
private void doAllDefaults() {
owner.defaults();
paletteManager.setItemNum(0); // Change menus to match default settings
paletteLengthManager.setValue(0);
maxIterationsManager.setValue(250);
}
/**
* Restores previous xy-limits on MandelbrotPanel. The previous limits are
* obtained from an event that is emitted by an observable property of the display whenever
* the limits change. The change event handler stores the old limits in the previousLimits
* instance variable.
*/
private void doUndoChangeOfLimits() {
if (previousLimits != null)
owner.setLimits(previousLimits[0],previousLimits[1],
previousLimits[2],previousLimits[3]);
}
/**
* Puts up a message alert that contains the current range of xy-values
* that is shown in the MandelbrotPane.
*/
private void doShowLimits() {
String[] limits = makeScaledLimitStrings();
Alert alert = new Alert(Alert.AlertType.INFORMATION,
I18n.tr("dialog.showLimits",limits[0],limits[1],limits[2],limits[3]));
alert.setHeaderText(null);
alert.setGraphic(null);
alert.showAndWait();
}
/**
* Puts up a dialog box of type SetImageSizeDialog (another class defined in
* this package). The dialog box lets the user enter new values for the
* width and height of the image. If the user does not cancel, then the
* new width and height are applied to the image. The dialog box ensures
* that the returned values, if any, are legal
*/
private void doSetImageSize() {
int oldWidth = (int)owner.getDisplay().getWidth();
int oldHeight = (int)owner.getDisplay().getHeight();
int[] newSize = SetImageSizeDialog.showDialog(new int[] {oldWidth, oldHeight});
if (newSize == null) // user canceled the dialog
return;
owner.setImageSize(newSize[0], newSize[1]);
}
/**
* Puts up a dialog box of type SetLimitsDialog (another class defined in
* this package). The dialog box lets the user enter new values for xmin,
* xmax, ymin, and ymax (the limits of the range of xy-values shown in the
* MandelbrotPane). If the user does not cancel, the new limits are
* applied to the display.
*/
private void doSetLimits() {
String[] limits = makeScaledLimitStrings();
double[] newLimits = SetLimitsDialog.showDialog(limits);
if (newLimits != null) // User canceled the dialog
owner.setLimits(newLimits[0],newLimits[1],newLimits[2],newLimits[3]);
}
/**
* Adds names of settings files to the examples menu. See the global variable
* SETTINGS_FILE_LIST for more info. Installs event handlers on each menu
* item to load the corresponding settings file.
*/
private void fillExampleMenu(Menu menu) {
for (int i = 0; i < SETTINGS_FILE_LIST.length; i++) {
final String str = SETTINGS_FILE_LIST[i];
MenuItem item = new MenuItem(str);
item.setOnAction( e -> loadExampleFile(str) );
menu.getItems().add(item);
}
}
/**
* Reads settings from an XML resource file.
*/
private void loadExampleFile(String resourceName) { // Tries to load one of the examples.
resourceName = "edu/hws/eck/mdbfx/examples/" + resourceName + ".mdb";
ClassLoader cl = getClass().getClassLoader();
URL resourceURL = cl.getResource(resourceName);
if (resourceURL != null) {
try {
InputStream stream = resourceURL.openStream();
DocumentBuilder docReader = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document xmldoc = docReader.parse(stream);
retrieveSettingsFromXML(xmldoc);
}
catch (Exception e) {
error("Internal Error. Couldn't load example\n" + e);
}
}
else {
error("Internal Error. Couldn't find file.");
}
}
/**
* Show an error alert with the given message.
*/
private void error(String message) {
Alert alert = new Alert(Alert.AlertType.ERROR, message);
alert.setHeaderText(null);
alert.showAndWait();
}
/**
* Defines the object that manages the Palette menu.
*/
private class PaletteManager {
RadioMenuItem[] items; // Array contains all the items that are in the Palette menu.
int selectedItem = 0; // Index in the items array of the item that is currently selected.
private String[] valueStrings = {"Spectrum","PaleSpectrum","Grayscale","CyclicGrayscale",
"BlackToRed","RedToCyan","OrangeToBlue"}; // Names for commands in XML settings file.
PaletteManager() {
// Constructor creates the items and adds them to a ToggleGroup. Also this
// object adds itself as an ActionListener to each item so it can carry out
// the command when the user selects one of the items.
items = new RadioMenuItem[8];
items[0] = new RadioMenuItem(I18n.tr("command.palette.spectrum"));
items[1] = new RadioMenuItem(I18n.tr("command.palette.paleSpectrum"));
items[2] = new RadioMenuItem(I18n.tr("command.palette.grayscale"));
items[3] = new RadioMenuItem(I18n.tr("command.palette.cyclicGrayscale"));
items[4] = new RadioMenuItem(I18n.tr("command.palette.gradientBlackToRed"));
items[5] = new RadioMenuItem(I18n.tr("command.palette.gradientRedToCyan"));
items[6] = new RadioMenuItem(I18n.tr("command.palette.gradientOrangeToBlue"));
items[7] = new RadioMenuItem(I18n.tr("command.palette.customGradient"));
ToggleGroup grp = new ToggleGroup();
for (int i = 0; i < items.length; i++) {
items[i].setToggleGroup(grp);
items[i].setOnAction( e -> applySelection() );
}
items[selectedItem].setSelected(true);
}
String valueAsString() {
// Converts the setting of this menu to a string that can be saved
// in an XML file. This is used by the currentSettingAsXML() method,
// which is used in turn by the SaveParams command.
if (selectedItem < valueStrings.length)
return valueStrings[selectedItem];
else {
Color c1 = owner.getGradientPaletteColor1();
Color c2 = owner.getGradientPaletteColor2();
if (c1 == null || c2 == null)
return valueStrings[0]; // Should not happen!
return "Custom/" + c1.getRed() + "," + c1.getGreen() + "," + c1.getBlue() + "/"
+ c2.getRed() + "," + c2.getGreen() + "," + c2.getBlue();
}
}
void setItemNum(int itemNum) {
// Select specifed item number in the menu.
selectedItem = itemNum;
items[itemNum].setSelected(true);
}
Object[] getValueFromString(String str) {
// Takes a string from an XML file (which originally came from the
// previous method when the file was saved) and restores the setting
// represented by that string. This is called by the retrieveSettingsFromXML()
// method, which is called in turn by the Open Params command.
for (int i = 0; i < 6; i++) {
if (valueStrings[i].equalsIgnoreCase(str)) {
switch(i) {
case 0: return new Object[] { 0, MandelbrotPane.PALETTE_SPECTRUM };
case 1: return new Object[] { 1, MandelbrotPane.PALETTE_PALE_SPECTRUM };
case 2: return new Object[] { 2, MandelbrotPane.PALETTE_GRAYSCALE };
case 3: return new Object[] { 3, MandelbrotPane.PALETTE_CYCLIC_GRAYSCALE };
case 4: return new Object[] { 4, MandelbrotPane.PALETTE_GRADIENT, Color.BLACK, Color.RED };
case 5: return new Object[] { 5, MandelbrotPane.PALETTE_GRADIENT, Color.RED, Color.CYAN };
case 6: return new Object[] { 6, MandelbrotPane.PALETTE_GRADIENT, Color.rgb(255,130,20), Color.BLUE };
}
}
}
String[] tokens = str.split("[/,]");
if ( ! tokens[0].equalsIgnoreCase("custom"))
throw new IllegalArgumentException();
Color c1 = Color.color( Double.parseDouble(tokens[1]),
Double.parseDouble(tokens[2]), Double.parseDouble(tokens[3]) );
Color c2 =Color.color( Double.parseDouble(tokens[4]),
Double.parseDouble(tokens[5]), Double.parseDouble(tokens[6]) );
return new Object[] { 7, MandelbrotPane.PALETTE_GRADIENT, c1, c2 };
}
private void applySelection() {
// Sets the palette in the MandelbrotPane to match the
// currently selected item in the menu.
if (items[0].isSelected()) {
owner.setPaletteType(MandelbrotPane.PALETTE_SPECTRUM);
selectedItem = 0;
}
else if (items[1].isSelected()) {
owner.setPaletteType(MandelbrotPane.PALETTE_PALE_SPECTRUM);
selectedItem = 1;
}
else if (items[2].isSelected()) {
owner.setPaletteType(MandelbrotPane.PALETTE_GRAYSCALE);
selectedItem = 2;
}
else if (items[3].isSelected()) {
owner.setPaletteType(MandelbrotPane.PALETTE_CYCLIC_GRAYSCALE);
selectedItem = 3;
}
else if (items[4].isSelected()) {
owner.setGradientPalette(Color.BLACK, Color.RED);
selectedItem = 4;
}
else if (items[5].isSelected()) {
owner.setGradientPalette(Color.RED, Color.CYAN);
selectedItem = 5;
}
else if (items[6].isSelected()) {
owner.setGradientPalette( Color.rgb(255,130,20), Color.rgb(0,0,255));
selectedItem = 6;
}
else { // The setting is for a custom gradient. NOTE that this case never occurs when
// this method is called from the setValueFromString() method; it only occurs
// when called from actionPerformed() in response to a user action. The
// command is "Custom gradient", and the response is to show two Color
// dialog boxes where the user can pick the start and end color for the
// gradient. Note that if the user CANCELS, then the state of the
// MandelbrotPane is not changed, and the menu must be reset to show
// the selection that was in place before the user action so that the
// menu will properly reflect the state of the display. This is the
// main reason why I keep the current selectedItem in an instance variable.
Color c1 = Color.GREEN;
Color c2 = Color.YELLOW;
if (owner.getPaletteType() == MandelbrotPane.PALETTE_GRADIENT) {
// If the display is already using a gradient palette, then the colors
// from that gradient will be used as the initially selected colors in
// the color chooser dialog boxes.
c1 = owner.getGradientPaletteColor1();
c2 = owner.getGradientPaletteColor2();
}
c1 = colorChooser(c1, I18n.tr("dialog.selectGradient1"));
if (c1 == null) {
items[selectedItem].setSelected(true); // Restore previous selection in menu.
return; // (The menu selection has been changed by the user
} // action, but the value of selectedItem has not changed.)
c2 = colorChooser(c2, I18n.tr("dialog.selectGradient2"));
if (c2 == null) {
items[selectedItem].setSelected(true);
return;
}
owner.setGradientPalette(c1, c2);
selectedItem = 7;
}
}
} // end nested class PaletteManager
/**
* Defines the object that manages the PaletteLength menu. Similar in
* structure to PaletteManager; see above.
*/
private class PaletteLengthManager {
int[] standardLengths = { 25, 50, 100, 250, 500, 1000, 2000, 5000, 10000 };
int selectedItem = 0;
RadioMenuItem[] items;
PaletteLengthManager() {
items = new RadioMenuItem[ 2 + standardLengths.length ];
items[0] = new RadioMenuItem(I18n.tr("command.palette.lengthTracksMaxIterations"));
for (int i = 0; i < standardLengths.length; i++)
items[i+1] = new RadioMenuItem(
I18n.tr("command.palette.length", ""+standardLengths[i]));
items[items.length-1] = new RadioMenuItem(I18n.tr("command.palette.customLength"));
ToggleGroup grp = new ToggleGroup();
for (int i = 0; i < items.length; i++) {
items[i].setToggleGroup(grp);
items[i].setOnAction(e -> itemSelected());
}
items[selectedItem].setSelected(true);
}
String valueAsString() {
return "" + owner.getPaletteLength();
}
void setValue(int value) {
if (value == 0) {
selectedItem = 0;
items[0].setSelected(true);
return;
}
for (int i = 0; i < standardLengths.length; i++) {
if (value == standardLengths[i]) {
selectedItem = i+1;
items[i+1].setSelected(true);
return;
}
}
items[items.length-1].setSelected(true);
selectedItem = items.length - 1;
}
int getValueFromString(String str) {
int length = Integer.parseInt(str);
if (length == 0) {
return 0;
}
if (length < 2 || length > 500000)
throw new IllegalArgumentException();
return length;
}
public void itemSelected() {
if (items[0].isSelected()) {
owner.setPaletteLength(0);
selectedItem = 0;
}
else if (items[items.length-1].isSelected()) {
TextInputDialog dialog = new TextInputDialog();
dialog.setHeaderText(I18n.tr("command.palette.customLengthQuestion",
owner.getPaletteLength()));
Optional resp = dialog.showAndWait();
if (!resp.isPresent() || resp.get() == null || resp.get().trim().length() == 0) {
items[selectedItem].setSelected(true);
return;
}
try {
int length = Integer.parseInt(resp.get().trim());
if (length < 2)
throw new NumberFormatException();
if (length > 500000)
throw new NumberFormatException();
owner.setPaletteLength(length);
selectedItem = items.length - 1;
}
catch (NumberFormatException e) {
error(I18n.tr("command.palette.customLengthError",resp.get().trim()));
items[selectedItem].setSelected(true);
return;
}
}
else {
for (int i = 0; i < standardLengths.length; i++) {
if (items[i+1].isSelected()) {
owner.setPaletteLength(standardLengths[i]);
selectedItem = i+1;
break;
}
}
}
}
} // end nested class PaletteLengthManager
/**
* Defines the object that manages the MaxIterations menu. Similar in
* structure to PaletteManager; see above.
*/
private class MaxIterationsManager{
int[] standardValues = { 50, 100, 250, 500, 1000, 2000, 5000, 20000, 50000, 100000 };
int selectedItem = 2;
RadioMenuItem[] items;
MaxIterationsManager() {
items = new RadioMenuItem[ 1 + standardValues.length ];
for (int i = 0; i < standardValues.length; i++)
items[i] = new RadioMenuItem(
I18n.tr("command.maxiterations", ""+standardValues[i]));
items[items.length-1] = new RadioMenuItem(I18n.tr("command.maxiterations.custom"));
ToggleGroup grp = new ToggleGroup();
for (int i = 0; i < items.length; i++) {
items[i].setToggleGroup(grp);
items[i].setOnAction( e -> itemSelected() );
}
items[selectedItem].setSelected(true);
}
String valueAsString() {
return "" + owner.getMaxIterations();
}
void setValue(int value) {
for (int i = 0; i < standardValues.length; i++) {
if (value == standardValues[i]) {
selectedItem = i;
items[i].setSelected(true);
return;
}
}
items[items.length-1].setSelected(true);
selectedItem = items.length - 1;
}
int getValueFromString(String str) {
int length = Integer.parseInt(str);
if (length < 2 || length > 500000)
throw new IllegalArgumentException();
return length;
}
public void itemSelected() {
if (items[items.length-1].isSelected()) {
TextInputDialog dialog = new TextInputDialog();
dialog.setHeaderText(I18n.tr("command.maxiterations.customQuestion",owner.getMaxIterations()));
Optional resp = dialog.showAndWait();
if (!resp.isPresent() || resp.get() == null || resp.get().trim().length() == 0) {
items[selectedItem].setSelected(true);
return;
}
try {
int value = Integer.parseInt(resp.get().trim());
if (value < 2)
throw new NumberFormatException();
if (value > 500000)
throw new NumberFormatException();
owner.setMaxIterations(value);
selectedItem = items.length - 1;
}
catch (NumberFormatException e) {
error(I18n.tr("command.maxiterations.customError",resp.get().trim()));
items[selectedItem].setSelected(true);
return;
}
}
else {
for (int i = 0; i < standardValues.length; i++) {
if (items[i].isSelected()) {
owner.setMaxIterations(standardValues[i]);
selectedItem = i;
break;
}
}
}
}
} // end nested class MaxIterationsManager
//---------- implementing a ColorChooser dialog box (from SimpleDialogs.java) ------------------------
/**
* This component shows six sliders that the user can manipulate
* to set the red, green, blue, hue, brightness, and saturation components
* of a color. A color patch shows the selected color, and there are
* six labels that show the numerical values of all the components.
*/
private static class ColorChooserPane extends GridPane {
private Slider hueSlider, brightnessSlider, saturationSlider, // Sliders to control color components.
redSlider, greenSlider, blueSlider;
private Label hueLabel, brightnessLabel, saturationLabel, // For displaying color component values.
redLabel, greenLabel, blueLabel;
private Pane colorPatch; // Color patch for displaying the color.
private Color currentColor;
public ColorChooserPane(Color initialColor) {
/* Create Sliders with possible values from 0 to 1, or 0 to 360 for hue. */
hueSlider = new Slider(0,360,0);
saturationSlider = new Slider(0,1,1);
brightnessSlider = new Slider(0,1,1);
redSlider = new Slider(0,1,1);
greenSlider = new Slider(0,1,0);
blueSlider = new Slider(0,1,0);
/* Set up listeners to respond when a slider value is changed. */
hueSlider.valueProperty().addListener( e -> newColor(hueSlider) );
saturationSlider.valueProperty().addListener( e -> newColor(saturationSlider) );
brightnessSlider.valueProperty().addListener( e -> newColor(brightnessSlider) );
redSlider.valueProperty().addListener( e -> newColor(redSlider) );
greenSlider.valueProperty().addListener( e -> newColor(greenSlider) );
blueSlider.valueProperty().addListener( e -> newColor(blueSlider) );
/* Create Labels showing current RGB and HSB values. */
hueLabel = makeText(String.format(" Hue = %1.3f", 0.0));
saturationLabel = makeText(String.format(" Saturation = %1.3f", 1.0));
brightnessLabel = makeText(String.format(" Brightness = %1.3f", 1.0));
redLabel = makeText(String.format(" Red = %1.3f", 1.0));
greenLabel = makeText(String.format(" Green = %1.3f", 1.0));
blueLabel = makeText(String.format(" Blue = %1.3f", 1.0));
/* Create an object to show the currently selected color. */
colorPatch = new Pane();
colorPatch.setStyle("-fx-background-color:red; -fx-border-color:black; -fx-border-width:2px");
/* Lay out the components */
GridPane root = this;
ColumnConstraints c1 = new ColumnConstraints();
c1.setPercentWidth(33);
ColumnConstraints c2 = new ColumnConstraints();
c2.setPercentWidth(34);
ColumnConstraints c3 = new ColumnConstraints();
c3.setPercentWidth(33);
root.getColumnConstraints().addAll(c1, c2, c3);
root.add(hueSlider, 0, 0);
root.add(saturationSlider, 0, 1);
root.add(brightnessSlider, 0, 2);
root.add(redSlider, 0, 3);
root.add(greenSlider, 0, 4);
root.add(blueSlider, 0, 5);
root.add(hueLabel, 1, 0);
root.add(saturationLabel, 1, 1);
root.add(brightnessLabel, 1, 2);
root.add(redLabel, 1, 3);
root.add(greenLabel, 1, 4);
root.add(blueLabel, 1, 5);
root.add(colorPatch, 2, 0, 1, 6); // occupies 6 rows!
root.setStyle("-fx-padding:5px; -fx-border-color:darkblue; -fx-border-width:2px; -fx-background-color:#DDF");
setColor(initialColor == null? Color.BLACK : initialColor);
}
public Color getColor() {
return currentColor;
}
public void setColor(Color color) {
if (color == null)
return;
hueSlider.setValue(color.getHue());
brightnessSlider.setValue(color.getBrightness());
saturationSlider.setValue(color.getSaturation());
redSlider.setValue(color.getRed());
greenSlider.setValue(color.getGreen());
blueSlider.setValue(color.getBlue());
String colorString = String.format("#%02x%02x%02x", (int)(255*color.getRed()),
(int)(255*color.getGreen()), (int)(255*color.getBlue()) );
colorPatch.setStyle("-fx-border-color:black; -fx-border-width:2px; -fx-background-color:" + colorString);
hueLabel.setText(String.format(I18n.tr("dialog.hue") + " = %1.3f", color.getHue()));
saturationLabel.setText(String.format(I18n.tr("dialog.saturation") + " = %1.3f", color.getSaturation()));
brightnessLabel.setText(String.format(I18n.tr("dialog.brightness") + " = %1.3f", color.getBrightness()));
redLabel.setText(String.format(I18n.tr("dialog.red") + " = %1.3f", color.getRed()));
greenLabel.setText(String.format(I18n.tr("dialog.green") + " = %1.3f", color.getGreen()));
blueLabel.setText(String.format(I18n.tr("dialog.blue") + " = %1.3f", color.getBlue()));
currentColor = color;
}
private Label makeText(String message) {
// Make a label to show a given message shown in bold, with some padding
// between the text and the border of the label.
Label text = new Label(message);
text.setStyle(" -fx-padding: 6px 10px 6px 10px; -fx-font-weight:bold");
return text;
}
private void newColor(Slider whichSlider) {
// Adjust the GUI to a new color value, when one of the sliders has changed.
if ( ! whichSlider.isValueChanging() ) {
return; // Don't respond to change if it was set programmatically;
// only respond if it was set by user dragging the slider.
}
Color color;
if (whichSlider == redSlider || whichSlider == greenSlider || whichSlider == blueSlider) {
color = Color.color(redSlider.getValue(), greenSlider.getValue(), blueSlider.getValue());
hueSlider.setValue(color.getHue());
brightnessSlider.setValue(color.getBrightness());
saturationSlider.setValue(color.getSaturation());
}
else {
color = Color.hsb(hueSlider.getValue(), saturationSlider.getValue(), brightnessSlider.getValue());
redSlider.setValue(color.getRed());
greenSlider.setValue(color.getGreen());
blueSlider.setValue(color.getBlue());
}
currentColor = color;
String colorString = String.format("#%02x%02x%02x", (int)(255*color.getRed()),
(int)(255*color.getGreen()), (int)(255*color.getBlue()) );
colorPatch.setStyle("-fx-border-color:black; -fx-border-width:2px; -fx-background-color:" + colorString);
hueLabel.setText(String.format(I18n.tr("dialog.hue") + " = %1.3f", color.getHue()));
saturationLabel.setText(String.format(I18n.tr("dialog.saturation") + " = %1.3f", color.getSaturation()));
brightnessLabel.setText(String.format(I18n.tr("dialog.brightness") + " = %1.3f", color.getBrightness()));
redLabel.setText(String.format(I18n.tr("dialog.red") + " = %1.3f", color.getRed()));
greenLabel.setText(String.format(I18n.tr("dialog.green") + " = %1.3f", color.getGreen()));
blueLabel.setText(String.format(I18n.tr("dialog.blue") + " = %1.3f", color.getBlue()));
}
} // end class SimpleColorChooser
/**
* Shows a dialog box containing a simple color chooser pane that the user
* can manipulate to select a color. The dialog box has an OK button and
* a "Cancel" button.
* @param initialColor the color that is initially selected in the dialog.
* If the value is null, the initial color is black.
* @param headerText Text to be shown in the dialog above the color chooser
* pane. Can be null. For multi-line text, the \n character should
* be included in the string to separate the lines.
* @return null if the user cancels the dialog, or the color that is selected
* in the color chooser pane if the user dismisses the dialog box by
* clicking the "OK" button.
*/
private static Color colorChooser( Color initialColor, String headerText ) {
ColorChooserPane chooser = new ColorChooserPane(initialColor);
Dialog dialog = new Dialog<>();
dialog.setTitle(I18n.tr("dialog.colorpicker"));
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialog.getDialogPane().setContent(chooser);
dialog.setHeaderText(headerText);
Optional result = dialog.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK )
return chooser.getColor();
else
return null;
}
} // end class Menus