Files
puzzles/PuzzleApplet.java
Simon Tatham a7dc17c425 Rework the preset menu system to permit submenus.
To do this, I've completely replaced the API between mid-end and front
end, so any downstream front end maintainers will have to do some
rewriting of their own (sorry). I've done the necessary work in all
five of the front ends I keep in-tree here - Windows, GTK, OS X,
Javascript/Emscripten, and Java/NestedVM - and I've done it in various
different styles (as each front end found most convenient), so that
should provide a variety of sample code to show downstreams how, if
they should need it.

I've left in the old puzzle back-end API function to return a flat
list of presets, so for the moment, all the puzzle backends are
unchanged apart from an extra null pointer appearing in their
top-level game structure. In a future commit I'll actually use the new
feature in a puzzle; perhaps in the further future it might make sense
to migrate all the puzzles to the new API and stop providing back ends
with two alternative ways of doing things, but this seemed like enough
upheaval for one day.
2017-04-26 21:51:23 +01:00

651 lines
26 KiB
Java

/*
* PuzzleApplet.java: NestedVM applet for the puzzle collection
*/
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.*;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.Timer;
import java.util.List;
import org.ibex.nestedvm.Runtime;
public class PuzzleApplet extends JApplet implements Runtime.CallJavaCB {
private static final long serialVersionUID = 1L;
private static final int CFG_SETTINGS = 0, CFG_SEED = 1, CFG_DESC = 2,
LEFT_BUTTON = 0x0200, MIDDLE_BUTTON = 0x201, RIGHT_BUTTON = 0x202,
LEFT_DRAG = 0x203, MIDDLE_DRAG = 0x204, RIGHT_DRAG = 0x205,
LEFT_RELEASE = 0x206, CURSOR_UP = 0x209, CURSOR_DOWN = 0x20a,
CURSOR_LEFT = 0x20b, CURSOR_RIGHT = 0x20c, MOD_CTRL = 0x1000,
MOD_SHFT = 0x2000, MOD_NUM_KEYPAD = 0x4000, ALIGN_VCENTRE = 0x100,
ALIGN_HCENTRE = 0x001, ALIGN_HRIGHT = 0x002, C_STRING = 0,
C_CHOICES = 1, C_BOOLEAN = 2;
private JFrame mainWindow;
private JMenu typeMenu;
private JMenuItem[] typeMenuItems;
private int customMenuItemIndex;
private JMenuItem solveCommand;
private Color[] colors;
private JLabel statusBar;
private PuzzlePanel pp;
private Runtime runtime;
private String[] puzzle_args;
private Graphics2D gg;
private Timer timer;
private int xarg1, xarg2, xarg3;
private int[] xPoints, yPoints;
private BufferedImage[] blitters = new BufferedImage[512];
private ConfigDialog dlg;
static {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void init() {
try {
Container cp = getContentPane();
cp.setLayout(new BorderLayout());
runtime = (Runtime) Class.forName("PuzzleEngine").newInstance();
runtime.setCallJavaCB(this);
JMenuBar menubar = new JMenuBar();
JMenu jm;
menubar.add(jm = new JMenu("Game"));
addMenuItemWithKey(jm, "New", 'n');
addMenuItemCallback(jm, "Restart", "jcallback_restart_event");
addMenuItemCallback(jm, "Specific...", "jcallback_config_event", CFG_DESC);
addMenuItemCallback(jm, "Random Seed...", "jcallback_config_event", CFG_SEED);
jm.addSeparator();
addMenuItemWithKey(jm, "Undo", 'u');
addMenuItemWithKey(jm, "Redo", 'r');
jm.addSeparator();
solveCommand = addMenuItemCallback(jm, "Solve", "jcallback_solve_event");
solveCommand.setEnabled(false);
if (mainWindow != null) {
jm.addSeparator();
addMenuItemWithKey(jm, "Exit", 'q');
}
menubar.add(typeMenu = new JMenu("Type"));
typeMenu.setVisible(false);
menubar.add(jm = new JMenu("Help"));
addMenuItemCallback(jm, "About", "jcallback_about_event");
setJMenuBar(menubar);
cp.add(pp = new PuzzlePanel(), BorderLayout.CENTER);
pp.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
int key = -1;
int shift = e.isShiftDown() ? MOD_SHFT : 0;
int ctrl = e.isControlDown() ? MOD_CTRL : 0;
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
case KeyEvent.VK_KP_LEFT:
key = shift | ctrl | CURSOR_LEFT;
break;
case KeyEvent.VK_RIGHT:
case KeyEvent.VK_KP_RIGHT:
key = shift | ctrl | CURSOR_RIGHT;
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_KP_UP:
key = shift | ctrl | CURSOR_UP;
break;
case KeyEvent.VK_DOWN:
case KeyEvent.VK_KP_DOWN:
key = shift | ctrl | CURSOR_DOWN;
break;
case KeyEvent.VK_PAGE_UP:
key = shift | ctrl | MOD_NUM_KEYPAD | '9';
break;
case KeyEvent.VK_PAGE_DOWN:
key = shift | ctrl | MOD_NUM_KEYPAD | '3';
break;
case KeyEvent.VK_HOME:
key = shift | ctrl | MOD_NUM_KEYPAD | '7';
break;
case KeyEvent.VK_END:
key = shift | ctrl | MOD_NUM_KEYPAD | '1';
break;
default:
if (e.getKeyCode() >= KeyEvent.VK_NUMPAD0 && e.getKeyCode() <=KeyEvent.VK_NUMPAD9) {
key = MOD_NUM_KEYPAD | (e.getKeyCode() - KeyEvent.VK_NUMPAD0+'0');
}
break;
}
if (key != -1) {
runtimeCall("jcallback_key_event", new int[] {0, 0, key});
}
}
public void keyTyped(KeyEvent e) {
runtimeCall("jcallback_key_event", new int[] {0, 0, e.getKeyChar()});
}
});
pp.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
mousePressedReleased(e, true);
}
public void mousePressed(MouseEvent e) {
pp.requestFocus();
mousePressedReleased(e, false);
}
private void mousePressedReleased(MouseEvent e, boolean released) {
int button;
if ((e.getModifiers() & (InputEvent.BUTTON2_MASK | InputEvent.SHIFT_MASK)) != 0)
button = MIDDLE_BUTTON;
else if ((e.getModifiers() & (InputEvent.BUTTON3_MASK | InputEvent.ALT_MASK)) != 0)
button = RIGHT_BUTTON;
else if ((e.getModifiers() & (InputEvent.BUTTON1_MASK)) != 0)
button = LEFT_BUTTON;
else
return;
if (released)
button += LEFT_RELEASE - LEFT_BUTTON;
runtimeCall("jcallback_key_event", new int[] {e.getX(), e.getY(), button});
}
});
pp.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
int button;
if ((e.getModifiers() & (InputEvent.BUTTON2_MASK | InputEvent.SHIFT_MASK)) != 0)
button = MIDDLE_DRAG;
else if ((e.getModifiers() & (InputEvent.BUTTON3_MASK | InputEvent.ALT_MASK)) != 0)
button = RIGHT_DRAG;
else
button = LEFT_DRAG;
runtimeCall("jcallback_key_event", new int[] {e.getX(), e.getY(), button});
}
});
pp.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
handleResized();
}
});
pp.setFocusable(true);
pp.requestFocus();
timer = new Timer(20, new ActionListener() {
public void actionPerformed(ActionEvent e) {
runtimeCall("jcallback_timer_func", new int[0]);
}
});
String gameid;
try {
gameid = getParameter("game_id");
} catch (java.lang.NullPointerException ex) {
gameid = null;
}
if (gameid == null) {
puzzle_args = null;
} else {
puzzle_args = new String[2];
puzzle_args[0] = "puzzle";
puzzle_args[1] = gameid;
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
runtime.start(puzzle_args);
runtime.execute();
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void destroy() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
runtime.execute();
if (mainWindow != null) {
mainWindow.dispose();
System.exit(0);
}
}
});
}
protected void handleResized() {
pp.createBackBuffer(pp.getWidth(), pp.getHeight(), colors[0]);
runtimeCall("jcallback_resize", new int[] {pp.getWidth(), pp.getHeight()});
}
private void addMenuItemWithKey(JMenu jm, String name, int key) {
addMenuItemCallback(jm, name, "jcallback_menu_key_event", key);
}
private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback, final int arg) {
return addMenuItemCallback(jm, name, callback, new int[] {arg}, false);
}
private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback) {
return addMenuItemCallback(jm, name, callback, new int[0], false);
}
private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback, final int[] args, boolean checkbox) {
JMenuItem jmi;
if (checkbox)
jm.add(jmi = new JCheckBoxMenuItem(name));
else
jm.add(jmi = new JMenuItem(name));
jmi.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
runtimeCall(callback, args);
}
});
return jmi;
}
protected void runtimeCall(String func, int[] args) {
if (runtimeCallWithResult(func, args) == 42 && mainWindow != null) {
destroy();
}
}
protected int runtimeCallWithResult(String func, int[] args) {
try {
return runtime.call(func, args);
} catch (Runtime.CallException ex) {
ex.printStackTrace();
return 42;
}
}
private void buildConfigureMenuItem() {
if (typeMenu.isVisible()) {
typeMenu.addSeparator();
} else {
typeMenu.setVisible(true);
}
typeMenuItems[customMenuItemIndex] =
addMenuItemCallback(typeMenu, "Custom...",
"jcallback_config_event",
new int[] {CFG_SETTINGS}, true);
}
private void addTypeItem
(JMenu targetMenu, String name, int newId, final int ptrGameParams) {
typeMenu.setVisible(true);
typeMenuItems[newId] =
addMenuItemCallback(targetMenu, name,
"jcallback_preset_event",
new int[] {ptrGameParams}, true);
}
private void addTypeSubmenu
(JMenu targetMenu, String name, int newId) {
JMenu newMenu = new JMenu(name);
newMenu.setVisible(true);
typeMenuItems[newId] = newMenu;
targetMenu.add(newMenu);
}
public int call(int cmd, int arg1, int arg2, int arg3) {
try {
switch(cmd) {
case 0: // initialize
if (mainWindow != null) mainWindow.setTitle(runtime.cstring(arg1));
if ((arg2 & 1) != 0) buildConfigureMenuItem();
if ((arg2 & 2) != 0) addStatusBar();
if ((arg2 & 4) != 0) solveCommand.setEnabled(true);
colors = new Color[arg3];
return 0;
case 1: // configure Type menu
if (arg1 == 0) {
// preliminary setup
typeMenuItems = new JMenuItem[arg2 + 2];
typeMenuItems[arg2] = typeMenu;
customMenuItemIndex = arg2 + 1;
return arg2;
} else if (xarg1 != 0) {
addTypeItem((JMenu)typeMenuItems[arg2],
runtime.cstring(arg1), arg3, xarg1);
} else {
addTypeSubmenu((JMenu)typeMenuItems[arg2],
runtime.cstring(arg1), arg3);
}
return 0;
case 2: // MessageBox
JOptionPane.showMessageDialog(this, runtime.cstring(arg2), runtime.cstring(arg1), arg3 == 0 ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.ERROR_MESSAGE);
return 0;
case 3: // Resize
pp.setPreferredSize(new Dimension(arg1, arg2));
if (mainWindow != null) mainWindow.pack();
handleResized();
if (mainWindow != null) mainWindow.setVisible(true);
return 0;
case 4: // drawing tasks
switch(arg1) {
case 0:
String text = runtime.cstring(arg2);
if (text.equals("")) text = " ";
statusBar.setText(text);
break;
case 1:
gg = pp.backBuffer.createGraphics();
if (arg2 != 0 || arg3 != 0 ||
arg2 + xarg2 != getWidth() ||
arg3 + xarg3 != getHeight()) {
int left = arg2, right = arg2 + xarg2;
int top = arg3, bottom = arg3 + xarg3;
int width = getWidth(), height = getHeight();
gg.setColor(colors != null ? colors[0] : Color.black);
gg.fillRect(0, 0, left, height);
gg.fillRect(right, 0, width-right, height);
gg.fillRect(0, 0, width, top);
gg.fillRect(0, bottom, width, height-bottom);
gg.setClip(left, top, right-left, bottom-top);
}
break;
case 2: gg.dispose(); pp.repaint(); break;
case 3: gg.setClip(arg2, arg3, xarg1, xarg2); break;
case 4:
if (arg2 == 0 && arg3 == 0) {
gg.setClip(0, 0, getWidth(), getHeight());
} else {
gg.setClip(arg2, arg3, getWidth()-2*arg2, getHeight()-2*arg3);
}
break;
case 5:
gg.setColor(colors[xarg3]);
gg.fillRect(arg2, arg3, xarg1, xarg2);
break;
case 6:
gg.setColor(colors[xarg3]);
gg.drawLine(arg2, arg3, xarg1, xarg2);
break;
case 7:
xPoints = new int[arg2];
yPoints = new int[arg2];
break;
case 8:
if (arg3 != -1) {
gg.setColor(colors[arg3]);
gg.fillPolygon(xPoints, yPoints, xPoints.length);
}
gg.setColor(colors[arg2]);
gg.drawPolygon(xPoints, yPoints, xPoints.length);
break;
case 9:
if (arg3 != -1) {
gg.setColor(colors[arg3]);
gg.fillOval(xarg1-xarg3, xarg2-xarg3, xarg3*2, xarg3*2);
}
gg.setColor(colors[arg2]);
gg.drawOval(xarg1-xarg3, xarg2-xarg3, xarg3*2, xarg3*2);
break;
case 10:
for(int i=0; i<blitters.length; i++) {
if (blitters[i] == null) {
blitters[i] = new BufferedImage(arg2, arg3, BufferedImage.TYPE_3BYTE_BGR);
return i;
}
}
throw new RuntimeException("No free blitter found!");
case 11: blitters[arg2] = null; break;
case 12:
timer.start(); break;
case 13:
timer.stop(); break;
}
return 0;
case 5: // more arguments
xarg1 = arg1;
xarg2 = arg2;
xarg3 = arg3;
return 0;
case 6: // polygon vertex
xPoints[arg1]=arg2;
yPoints[arg1]=arg3;
return 0;
case 7: // string
gg.setColor(colors[arg2]);
{
String text = runtime.utfstring(arg3);
Font ft = new Font((xarg3 & 0x10) != 0 ? "Monospaced" : "Dialog",
Font.PLAIN, 100);
int height100 = this.getFontMetrics(ft).getHeight();
ft = ft.deriveFont(arg1 * 100 / (float)height100);
FontMetrics fm = this.getFontMetrics(ft);
int asc = fm.getAscent(), desc = fm.getDescent();
if ((xarg3 & ALIGN_VCENTRE) != 0)
xarg2 += asc - (asc+desc)/2;
int wid = fm.stringWidth(text);
if ((xarg3 & ALIGN_HCENTRE) != 0)
xarg1 -= wid / 2;
else if ((xarg3 & ALIGN_HRIGHT) != 0)
xarg1 -= wid;
gg.setFont(ft);
gg.drawString(text, xarg1, xarg2);
}
return 0;
case 8: // blitter_save
Graphics g2 = blitters[arg1].createGraphics();
g2.drawImage(pp.backBuffer, 0, 0, blitters[arg1].getWidth(), blitters[arg1].getHeight(),
arg2, arg3, arg2 + blitters[arg1].getWidth(), arg3 + blitters[arg1].getHeight(), this);
g2.dispose();
return 0;
case 9: // blitter_load
gg.drawImage(blitters[arg1], arg2, arg3, this);
return 0;
case 10: // dialog_init
dlg= new ConfigDialog(this, runtime.cstring(arg1));
return 0;
case 11: // dialog_add_control
{
int sval_ptr = arg1;
int ival = arg2;
int ptr = xarg1;
int type=xarg2;
String name = runtime.cstring(xarg3);
switch(type) {
case C_STRING:
dlg.addTextBox(ptr, name, runtime.cstring(sval_ptr));
break;
case C_BOOLEAN:
dlg.addCheckBox(ptr, name, ival != 0);
break;
case C_CHOICES:
dlg.addComboBox(ptr, name, runtime.cstring(sval_ptr), ival);
}
}
return 0;
case 12:
dlg.finish();
dlg = null;
return 0;
case 13: // tick a menu item
if (arg1 < 0) arg1 = customMenuItemIndex;
for (int i = 0; i < typeMenuItems.length; i++) {
if (typeMenuItems[i] instanceof JCheckBoxMenuItem) {
((JCheckBoxMenuItem)typeMenuItems[i]).setSelected
(arg1 == i);
}
}
return 0;
default:
if (cmd >= 1024 && cmd < 2048) { // palette
colors[cmd-1024] = new Color(arg1, arg2, arg3);
}
if (cmd == 1024) {
pp.setBackground(colors[0]);
if (statusBar != null) statusBar.setBackground(colors[0]);
this.setBackground(colors[0]);
}
return 0;
}
} catch (Throwable ex) {
ex.printStackTrace();
System.exit(-1);
return 0;
}
}
private void addStatusBar() {
statusBar = new JLabel("test");
statusBar.setBorder(new BevelBorder(BevelBorder.LOWERED));
getContentPane().add(BorderLayout.SOUTH,statusBar);
}
// Standalone runner
public static void main(String[] args) {
final PuzzleApplet a = new PuzzleApplet();
JFrame jf = new JFrame("Loading...");
jf.getContentPane().setLayout(new BorderLayout());
jf.getContentPane().add(a, BorderLayout.CENTER);
a.mainWindow=jf;
a.init();
a.start();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
a.stop();
a.destroy();
}
});
jf.setVisible(true);
}
public static class PuzzlePanel extends JPanel {
private static final long serialVersionUID = 1L;
protected BufferedImage backBuffer;
public PuzzlePanel() {
setPreferredSize(new Dimension(100,100));
createBackBuffer(100,100, Color.black);
}
public void createBackBuffer(int w, int h, Color bg) {
if (w > 0 && h > 0) {
backBuffer = new BufferedImage(w,h, BufferedImage.TYPE_3BYTE_BGR);
Graphics g = backBuffer.createGraphics();
g.setColor(bg);
g.fillRect(0, 0, w, h);
g.dispose();
}
}
protected void paintComponent(Graphics g) {
g.drawImage(backBuffer, 0, 0, this);
}
}
public static class ConfigComponent {
public int type;
public int configItemPointer;
public JComponent component;
public ConfigComponent(int type, int configItemPointer, JComponent component) {
this.type = type;
this.configItemPointer = configItemPointer;
this.component = component;
}
}
public class ConfigDialog extends JDialog {
private GridBagConstraints gbcLeft = new GridBagConstraints(
GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE, 1, 1,
0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0);
private GridBagConstraints gbcRight = new GridBagConstraints(
GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE,
GridBagConstraints.REMAINDER, 1, 1.0, 0,
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
new Insets(5, 5, 5, 5), 0, 0);
private GridBagConstraints gbcBottom = new GridBagConstraints(
GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE,
GridBagConstraints.REMAINDER, GridBagConstraints.REMAINDER,
1.0, 1.0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, new Insets(5, 5, 5, 5), 0, 0);
private static final long serialVersionUID = 1L;
private List components = new ArrayList();
public ConfigDialog(JApplet parent, String title) {
super(JOptionPane.getFrameForComponent(parent), title, true);
getContentPane().setLayout(new GridBagLayout());
}
public void addTextBox(int ptr, String name, String value) {
getContentPane().add(new JLabel(name), gbcLeft);
JComponent c = new JTextField(value, 25);
getContentPane().add(c, gbcRight);
components.add(new ConfigComponent(C_STRING, ptr, c));
}
public void addCheckBox(int ptr, String name, boolean selected) {
JComponent c = new JCheckBox(name, selected);
getContentPane().add(c, gbcRight);
components.add(new ConfigComponent(C_BOOLEAN, ptr, c));
}
public void addComboBox(int ptr, String name, String values, int selected) {
getContentPane().add(new JLabel(name), gbcLeft);
StringTokenizer st = new StringTokenizer(values.substring(1), values.substring(0,1));
JComboBox c = new JComboBox();
c.setEditable(false);
while(st.hasMoreTokens())
c.addItem(st.nextToken());
c.setSelectedIndex(selected);
getContentPane().add(c, gbcRight);
components.add(new ConfigComponent(C_CHOICES, ptr, c));
}
public void finish() {
JPanel buttons = new JPanel(new GridLayout(1, 2, 5, 5));
getContentPane().add(buttons, gbcBottom);
JButton b;
buttons.add(b=new JButton("OK"));
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
save();
dispose();
}
});
getRootPane().setDefaultButton(b);
buttons.add(b=new JButton("Cancel"));
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
dispose();
}
});
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private void save() {
for (int i = 0; i < components.size(); i++) {
ConfigComponent cc = (ConfigComponent) components.get(i);
switch(cc.type) {
case C_STRING:
JTextField jtf = (JTextField)cc.component;
runtimeCall("jcallback_config_set_string", new int[] {cc.configItemPointer, runtime.strdup(jtf.getText())});
break;
case C_BOOLEAN:
JCheckBox jcb = (JCheckBox)cc.component;
runtimeCall("jcallback_config_set_boolean", new int[] {cc.configItemPointer, jcb.isSelected()?1:0});
break;
case C_CHOICES:
JComboBox jcm = (JComboBox)cc.component;
runtimeCall("jcallback_config_set_choice", new int[] {cc.configItemPointer, jcm.getSelectedIndex()});
break;
}
}
runtimeCall("jcallback_config_ok", new int[0]);
}
}
}