/*
* Copyright (c) 2014 Seth J. Morabito <web@loomcom.com>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.loomcom.symon;
import com.loomcom.symon.devices.Memory;
import com.loomcom.symon.exceptions.FifoUnderrunException;
import com.loomcom.symon.exceptions.MemoryAccessException;
import com.loomcom.symon.exceptions.MemoryRangeException;
import com.loomcom.symon.exceptions.SymonException;
import com.loomcom.symon.machines.Machine;
import com.loomcom.symon.ui.*;
import com.loomcom.symon.ui.Console;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Symon Simulator Interface and Control.
* <p/>
* This class provides a control and I/O system for the simulated 6502 system.
* It includes the simulated CPU itself, as well as 32KB of RAM, 16KB of ROM,
* and a simulated ACIA for serial I/O. The ACIA is attached to a dumb terminal
* with a basic 80x25 character display.
*/
public class Simulator {
// UI constants
private static final int DEFAULT_FONT_SIZE = 12;
private static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, DEFAULT_FONT_SIZE);
private static final int CONSOLE_BORDER_WIDTH = 10;
// Since it is very expensive to update the UI with Swing's Event Dispatch Thread, we can't afford
// to refresh the status view on every simulated clock cycle. Instead, we will only refresh the status view
// after this number of steps when running normally.
//
// Since we're simulating a 1MHz 6502 here, we have a 1 us delay between steps. Setting this to 20000
// should give us a status update about every 100 ms.
//
// TODO: Work around the event dispatch thread with custom painting code instead of relying on Swing.
//
private static final int MAX_STEPS_BETWEEN_UPDATES = 20000;
private final static Logger logger = Logger.getLogger(Simulator.class.getName());
// The simulated machine
private Machine machine;
// Number of CPU steps between CRT repaints.
// TODO: Dynamically refresh the value at runtime based on performance figures to reach ~ 30fps.
private long stepsBetweenCrtcRefreshes = 2500;
// A counter to keep track of the number of UI updates that have been
// requested
private int stepsSinceLastUpdate = 0;
private int stepsSinceLastCrtcRefresh = 0;
// The number of steps to run per click of the "Step" button
private int stepsPerClick = 1;
/**
* The Main Window is the primary control point for the simulator.
* It is in charge of the menu, and sub-windows. It also shows the
* CPU status at all times.
*/
private JFrame mainWindow;
/**
* The Trace Window shows the most recent 50,000 CPU states.
*/
private TraceLog traceLog;
/**
* The Memory Window shows the contents of one page of memory.
*/
private MemoryWindow memoryWindow;
private VideoWindow videoWindow;
private SimulatorMenu menuBar;
private RunLoop runLoop;
private Console console;
private StatusPanel statusPane;
private JButton runStopButton;
private JButton stepButton;
private JButton softResetButton;
private JButton hardResetButton;
private JComboBox<String> stepCountBox;
private JFileChooser fileChooser;
private PreferencesDialog preferences;
private final Object commandMonitorObject = new Object();
private MAIN_CMD command = MAIN_CMD.NONE;
public static enum MAIN_CMD {
NONE,
SELECTMACHINE
}
/**
* The list of step counts that will appear in the "Step" drop-down.
*/
private static final String[] STEPS = {"1", "5", "10", "20", "50", "100"};
public Simulator(Class machineClass) throws Exception {
this.machine = (Machine) machineClass.getConstructors()[0].newInstance();
}
/**
* Display the main simulator UI.
*/
public void createAndShowUi() throws IOException {
mainWindow = new JFrame();
mainWindow.setTitle("6502 Simulator - " + machine.getName());
mainWindow.setResizable(false);
mainWindow.getContentPane().setLayout(new BorderLayout());
// UI components used for I/O.
this.console = new com.loomcom.symon.ui.Console(80, 25, DEFAULT_FONT);
this.statusPane = new StatusPanel(machine);
console.setBorderWidth(CONSOLE_BORDER_WIDTH);
// File Chooser
fileChooser = new JFileChooser(System.getProperty("user.dir"));
preferences = new PreferencesDialog(mainWindow, true);
// Panel for Console and Buttons
JPanel consoleContainer = new JPanel();
JPanel buttonContainer = new JPanel();
consoleContainer.setLayout(new BorderLayout());
consoleContainer.setBorder(new EmptyBorder(10, 10, 10, 0));
buttonContainer.setLayout(new FlowLayout());
runStopButton = new JButton("Run");
stepButton = new JButton("Step");
softResetButton = new JButton("Soft Reset");
hardResetButton = new JButton("Hard Reset");
stepCountBox = new JComboBox<String>(STEPS);
stepCountBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
try {
JComboBox cb = (JComboBox) actionEvent.getSource();
stepsPerClick = Integer.parseInt((String) cb.getSelectedItem());
} catch (NumberFormatException ex) {
stepsPerClick = 1;
stepCountBox.setSelectedIndex(0);
}
}
});
buttonContainer.add(runStopButton);
buttonContainer.add(stepButton);
buttonContainer.add(stepCountBox);
buttonContainer.add(softResetButton);
buttonContainer.add(hardResetButton);
// Left side - console
consoleContainer.add(console, BorderLayout.CENTER);
mainWindow.getContentPane().add(consoleContainer, BorderLayout.LINE_START);
// Right side - status pane
mainWindow.getContentPane().add(statusPane, BorderLayout.LINE_END);
// Bottom - buttons.
mainWindow.getContentPane().add(buttonContainer, BorderLayout.PAGE_END);
runStopButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
if (runLoop != null && runLoop.isRunning()) {
handleStop();
} else {
handleStart();
}
}
});
stepButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
handleStep(stepsPerClick);
}
});
softResetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
// If this was a CTRL-click, do a hard reset.
handleReset(false);
}
});
hardResetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
// If this was a CTRL-click, do a hard reset.
handleReset(true);
}
});
// Prepare the log window
traceLog = new TraceLog();
// Prepare the memory window
memoryWindow = new MemoryWindow(machine.getBus());
// Composite Video and 6545 CRTC
if(machine.getCrtc() != null) {
videoWindow = new VideoWindow(machine.getCrtc(), 2, 2);
}
mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// The Menu. This comes last, because it relies on other components having
// already been initialized.
menuBar = new SimulatorMenu();
mainWindow.setJMenuBar(menuBar);
mainWindow.pack();
mainWindow.setVisible(true);
console.requestFocus();
handleReset(false);
}
public MAIN_CMD waitForCommand() {
synchronized(commandMonitorObject) {
try {
commandMonitorObject.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
return command;
}
private void handleStart() {
// Shift focus to the console.
console.requestFocus();
// Spin up the new run loop
runLoop = new RunLoop();
runLoop.start();
traceLog.simulatorDidStart();
}
private void handleStop() {
runLoop.requestStop();
runLoop.interrupt();
runLoop = null;
}
/*
* Perform a reset.
*/
private void handleReset(boolean isColdReset) {
if (runLoop != null && runLoop.isRunning()) {
runLoop.requestStop();
runLoop.interrupt();
runLoop = null;
}
try {
logger.log(Level.INFO, "Reset requested. Resetting CPU.");
// Reset CPU
machine.getCpu().reset();
// Clear the console.
console.reset();
// Reset the trace log.
traceLog.reset();
// If we're doing a cold reset, clear the memory.
if (isColdReset) {
Memory mem = machine.getRam();
if (mem != null) {
mem.fill(0);
}
}
// Update status.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Now update the state
statusPane.updateState();
memoryWindow.updateState();
}
});
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Exception during simulator reset: " + ex.getMessage());
}
}
/**
* Step the requested number of times, and immediately refresh the UI.
*/
private void handleStep(int numSteps) {
try {
for (int i = 0; i < numSteps; i++) {
step();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (traceLog.isVisible()) {
traceLog.refresh();
}
statusPane.updateState();
memoryWindow.updateState();
}
});
} catch (SymonException ex) {
logger.log(Level.SEVERE, "Exception during simulator step: " + ex.getMessage());
ex.printStackTrace();
}
}
/**
* Perform a single step of the simulated system.
*/
private void step() throws MemoryAccessException {
machine.getCpu().step();
traceLog.append(machine.getCpu().getCpuState());
// Read from the ACIA and immediately update the console if there's
// output ready.
if (machine.getAcia() != null && machine.getAcia().hasTxChar()) {
// This is thread-safe
console.print(Character.toString((char) machine.getAcia().txRead()));
console.repaint();
}
// If a key has been pressed, fill the ACIA.
try {
if (machine.getAcia() != null && console.hasInput()) {
machine.getAcia().rxWrite((int) console.readInputChar());
}
} catch (FifoUnderrunException ex) {
logger.severe("Console type-ahead buffer underrun!");
}
if (videoWindow != null && stepsSinceLastCrtcRefresh++ > stepsBetweenCrtcRefreshes) {
stepsSinceLastCrtcRefresh = 0;
if (videoWindow.isVisible()) {
videoWindow.repaint();
}
}
// This is a very expensive update, and we're doing it without
// a delay, so we don't want to overwhelm the Swing event processing thread
// with requests. Limit the number of ui updates that can be performed.
if (stepsSinceLastUpdate++ > MAX_STEPS_BETWEEN_UPDATES) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Now update the state
statusPane.updateState();
memoryWindow.updateState();
}
});
stepsSinceLastUpdate = 0;
}
}
/**
* Load a program into memory at the simulatorDidStart address.
*/
private void loadProgram(byte[] program, int startAddress) throws MemoryAccessException {
int addr = startAddress, i;
for (i = 0; i < program.length; i++) {
machine.getBus().write(addr++, program[i] & 0xff);
}
logger.log(Level.INFO, "Loaded " + i + " bytes at address 0x" +
Integer.toString(startAddress, 16));
// After loading, be sure to reset and
// Reset (but don't clear memory, naturally)
machine.getCpu().reset();
// Reset the stack program counter
machine.getCpu().setProgramCounter(preferences.getProgramStartAddress());
// Immediately update the UI.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Now update the state
statusPane.updateState();
memoryWindow.updateState();
}
});
}
/**
* The main run thread.
*/
class RunLoop extends Thread {
private boolean isRunning = false;
public boolean isRunning() {
return isRunning;
}
public void requestStop() {
isRunning = false;
}
public void run() {
logger.log(Level.INFO, "Starting main run loop.");
isRunning = true;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Don't allow step while the simulator is running
stepButton.setEnabled(false);
stepCountBox.setEnabled(false);
menuBar.simulatorDidStart();
// Toggle the state of the run button
runStopButton.setText("Stop");
}
});
try {
do {
step();
} while (shouldContinue());
} catch (SymonException ex) {
logger.log(Level.SEVERE, "Exception in main simulator run thread. Exiting run.");
ex.printStackTrace();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
statusPane.updateState();
memoryWindow.updateState();
runStopButton.setText("Run");
stepButton.setEnabled(true);
stepCountBox.setEnabled(true);
if (traceLog.isVisible()) {
traceLog.refresh();
}
menuBar.simulatorDidStop();
traceLog.simulatorDidStop();
}
});
isRunning = false;
}
/**
* Returns true if the run loop should proceed to the next step.
*
* @return True if the run loop should proceed to the next step.
*/
private boolean shouldContinue() {
return isRunning && !(preferences.getHaltOnBreak() && machine.getCpu().getInstruction() == 0x00);
}
}
class LoadProgramAction extends AbstractAction {
public LoadProgramAction() {
super("Load Program...", null);
putValue(SHORT_DESCRIPTION, "Load a program into memory");
putValue(MNEMONIC_KEY, KeyEvent.VK_L);
}
public void actionPerformed(ActionEvent actionEvent) {
try {
int retVal = fileChooser.showOpenDialog(mainWindow);
if (retVal == JFileChooser.APPROVE_OPTION) {
File f = fileChooser.getSelectedFile();
if (f.canRead()) {
long fileSize = f.length();
if (fileSize > machine.getMemorySize()) {
throw new IOException("File will not fit in " +
"available memory ($" +
Integer.toString(machine.getMemorySize(), 16) +
" bytes)");
} else {
byte[] program = new byte[(int) fileSize];
int i = 0;
FileInputStream fis = new FileInputStream(f);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
while (dis.available() != 0) {
program[i++] = dis.readByte();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
console.reset();
}
});
// Now load the program at the starting address.
loadProgram(program, preferences.getProgramStartAddress());
// TODO: "Don't Show Again" checkbox
JOptionPane.showMessageDialog(mainWindow,
"Loaded Successfully At " +
String.format("$%04X", preferences.getProgramStartAddress()),
"OK",
JOptionPane.PLAIN_MESSAGE);
}
}
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "Unable to read program file: " + ex.getMessage());
JOptionPane.showMessageDialog(mainWindow, ex.getMessage(), "Failure", JOptionPane.ERROR_MESSAGE);
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Memory access error loading program: " + ex.getMessage());
JOptionPane.showMessageDialog(mainWindow, ex.getMessage(), "Failure", JOptionPane.ERROR_MESSAGE);
}
}
}
class LoadRomAction extends AbstractAction {
public LoadRomAction() {
super("Load ROM...", null);
putValue(SHORT_DESCRIPTION, "Load a ROM image");
putValue(MNEMONIC_KEY, KeyEvent.VK_R);
}
public void actionPerformed(ActionEvent actionEvent) {
try {
int retVal = fileChooser.showOpenDialog(mainWindow);
if (retVal == JFileChooser.APPROVE_OPTION) {
File romFile = fileChooser.getSelectedFile();
if (romFile.canRead()) {
long fileSize = romFile.length();
if (fileSize != machine.getRomSize()) {
throw new IOException("ROM file must be exactly " + String.valueOf(machine.getRomSize()) + " bytes.");
} else {
// Load the new ROM image
Memory rom = Memory.makeROM(machine.getRomBase(), machine.getRomBase() + machine.getRomSize() - 1, romFile);
machine.setRom(rom);
// Now, reset
machine.getCpu().reset();
logger.log(Level.INFO, "ROM File `" + romFile.getName() + "' loaded at " +
String.format("0x%04X", machine.getRomBase()));
// TODO: "Don't Show Again" checkbox
JOptionPane.showMessageDialog(mainWindow,
"Loaded Successfully At " +
String.format("$%04X", machine.getRomBase()),
"OK",
JOptionPane.PLAIN_MESSAGE);
}
}
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "Unable to read ROM file: " + ex.getMessage());
JOptionPane.showMessageDialog(mainWindow, ex.getMessage(), "Failure", JOptionPane.ERROR_MESSAGE);
} catch (MemoryRangeException ex) {
logger.log(Level.SEVERE, "Memory range error while loading ROM file: " + ex.getMessage());
JOptionPane.showMessageDialog(mainWindow, ex.getMessage(), "Failure", JOptionPane.ERROR_MESSAGE);
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Memory access error while loading ROM file: " + ex.getMessage());
JOptionPane.showMessageDialog(mainWindow, ex.getMessage(), "Failure", JOptionPane.ERROR_MESSAGE);
}
}
}
class ShowPrefsAction extends AbstractAction {
public ShowPrefsAction() {
super("Preferences...", null);
putValue(SHORT_DESCRIPTION, "Show Preferences Dialog");
putValue(MNEMONIC_KEY, KeyEvent.VK_P);
}
public void actionPerformed(ActionEvent actionEvent) {
preferences.getDialog().setVisible(true);
}
}
class SelectMachineAction extends AbstractAction {
public SelectMachineAction() {
super("Switch emulated machine...", null);
putValue(SHORT_DESCRIPTION, "Select the type of the machine to be emulated");
putValue(MNEMONIC_KEY, KeyEvent.VK_M);
}
public void actionPerformed(ActionEvent actionEvent) {
if(runLoop != null) {
runLoop.requestStop();
}
memoryWindow.dispose();
traceLog.dispose();
if(videoWindow != null) {
videoWindow.dispose();
}
mainWindow.dispose();
command = MAIN_CMD.SELECTMACHINE;
synchronized(commandMonitorObject) {
commandMonitorObject.notifyAll();
}
}
}
class QuitAction extends AbstractAction {
public QuitAction() {
super("Quit", null);
putValue(SHORT_DESCRIPTION, "Exit the Simulator");
putValue(MNEMONIC_KEY, KeyEvent.VK_Q);
}
public void actionPerformed(ActionEvent actionEvent) {
if (runLoop != null && runLoop.isRunning()) {
runLoop.requestStop();
runLoop.interrupt();
}
System.exit(0);
}
}
class SetFontAction extends AbstractAction {
private int size;
public SetFontAction(int size) {
super(Integer.toString(size) + " pt", null);
this.size = size;
putValue(SHORT_DESCRIPTION, "Set font to " + Integer.toString(size) + "pt.");
}
public void actionPerformed(ActionEvent actionEvent) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
console.setFont(new Font("Monospaced", Font.PLAIN, size));
mainWindow.pack();
}
});
}
}
class ToggleTraceWindowAction extends AbstractAction {
public ToggleTraceWindowAction() {
super("Trace Log", null);
putValue(SHORT_DESCRIPTION, "Show or Hide the Trace Log Window");
}
public void actionPerformed(ActionEvent actionEvent) {
synchronized (traceLog) {
if (traceLog.isVisible()) {
traceLog.setVisible(false);
} else {
traceLog.refresh();
traceLog.setVisible(true);
}
}
}
}
class ToggleMemoryWindowAction extends AbstractAction {
public ToggleMemoryWindowAction() {
super("Memory Window", null);
putValue(SHORT_DESCRIPTION, "Show or Hide the Memory Window");
}
public void actionPerformed(ActionEvent actionEvent) {
synchronized (memoryWindow) {
if (memoryWindow.isVisible()) {
memoryWindow.setVisible(false);
} else {
memoryWindow.setVisible(true);
}
}
}
}
class ToggleVideoWindowAction extends AbstractAction {
public ToggleVideoWindowAction() {
super("Video Window", null);
putValue(SHORT_DESCRIPTION, "Show or Hide the Video Window");
}
public void actionPerformed(ActionEvent actionEvent) {
synchronized (videoWindow) {
if (videoWindow.isVisible()) {
videoWindow.setVisible(false);
} else {
videoWindow.setVisible(true);
}
}
}
}
class SimulatorMenu extends JMenuBar {
// Menu Items
private JMenuItem loadProgramItem;
private JMenuItem loadRomItem;
/**
* Create a new SimulatorMenu instance.
*/
public SimulatorMenu() {
initMenu();
}
/**
* Disable menu items that should not be available during simulator execution.
*/
public void simulatorDidStart() {
loadProgramItem.setEnabled(false);
if (loadRomItem != null) {
loadRomItem.setEnabled(false);
}
}
/**
* Enable menu items that should be available while the simulator is stopped.
*/
public void simulatorDidStop() {
loadProgramItem.setEnabled(true);
if (loadRomItem != null) {
loadRomItem.setEnabled(true);
}
}
private void initMenu() {
/*
* File Menu
*/
JMenu fileMenu = new JMenu("File");
loadProgramItem = new JMenuItem(new LoadProgramAction());
fileMenu.add(loadProgramItem);
// Simple Machine does not implement a ROM, so it makes no sense to
// offer a ROM load option.
if (machine.getRom() != null) {
loadRomItem = new JMenuItem(new LoadRomAction());
fileMenu.add(loadRomItem);
}
JMenuItem prefsItem = new JMenuItem(new ShowPrefsAction());
fileMenu.add(prefsItem);
JMenuItem selectMachineItem = new JMenuItem(new SelectMachineAction());
fileMenu.add(selectMachineItem);
JMenuItem quitItem = new JMenuItem(new QuitAction());
fileMenu.add(quitItem);
add(fileMenu);
/*
* View Menu
*/
JMenu viewMenu = new JMenu("View");
JMenu fontSubMenu = new JMenu("Console Font Size");
ButtonGroup fontSizeGroup = new ButtonGroup();
makeFontSizeMenuItem(10, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(11, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(12, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(13, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(14, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(15, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(16, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(17, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(18, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(19, fontSubMenu, fontSizeGroup);
makeFontSizeMenuItem(20, fontSubMenu, fontSizeGroup);
viewMenu.add(fontSubMenu);
final JCheckBoxMenuItem showTraceLog = new JCheckBoxMenuItem(new ToggleTraceWindowAction());
// Un-check the menu item if the user closes the window directly
traceLog.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
showTraceLog.setSelected(false);
}
});
viewMenu.add(showTraceLog);
final JCheckBoxMenuItem showMemoryTable = new JCheckBoxMenuItem(new ToggleMemoryWindowAction());
// Un-check the menu item if the user closes the window directly
memoryWindow.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
showMemoryTable.setSelected(false);
}
});
viewMenu.add(showMemoryTable);
if(videoWindow != null) {
final JCheckBoxMenuItem showVideoWindow = new JCheckBoxMenuItem(new ToggleVideoWindowAction());
videoWindow.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
showVideoWindow.setSelected(false);
}
});
viewMenu.add(showVideoWindow);
}
add(viewMenu);
}
private void makeFontSizeMenuItem(int size, JMenu fontSubMenu, ButtonGroup group) {
Action action = new SetFontAction(size);
JCheckBoxMenuItem item = new JCheckBoxMenuItem(action);
item.setSelected(size == DEFAULT_FONT_SIZE);
fontSubMenu.add(item);
group.add(item);
}
}
}