Files
puzzles/nestedvm.c
Simon Tatham e4d05c36d9 Generate special fake keypresses from menu options.
This fixes an amusing UI bug that I think can currently only come up
in the unpublished puzzle 'Group', but there's no reason why other
puzzles _couldn't_ do the thing that triggers the bug, if they wanted
to.

Group has unusual keyboard handling, in that sometimes (when a cell is
selected for input and the key in question is valid for the current
puzzle size) the game's interpret_move function will eat keystrokes
like 'n' and 'u' that would otherwise trigger special UI events like
New Game or Undo.

The bug is that fake keypress events generated from the GUI menus
looked enough like those keystrokes that interpret_move would eat
those too. So if you start, say, a 16x16 Group puzzle, select an empty
cell, and then choose 'new game' from the menu, Group will enter 'n'
into the cell instead of starting a new game!

I've fixed this by inventing a new set of special keystroke values
called things like UI_NEWGAME and UI_UNDO, and having the GUI menus in
all my front ends generate those in place of 'n' and 'u'. So now the
midend can tell the difference between 'n' on the keyboard and New
Game from the menu, and so Group can treat them differently too. In
fact, out of sheer overcaution, midend.c will spot keystrokes in this
range and not even _pass_ them to the game back end, so Group
shouldn't be able to override these special events even by mistake.

One fiddly consequence is that in gtk.c I've had to rethink the menu
accelerator system. I was adding visible menu accelerators to a few
menu items, so that (for example) 'U' and 'R' showed up to the right
of Undo and Redo in the menu. Of course this had the side effect of
making them real functioning accelerators from GTK's point of view,
which activate the menu item in the same way as usual, causing it to
send whatever keystroke the menu item generates. In other words,
whenever I entered 'n' into a cell in a large Group game, this was the
route followed by even a normal 'n' originated from a real keystroke -
it activated the New Game menu item by mistake, which would then send
'n' by mistake instead of starting a new game!

Those mistakes cancelled each other out, but now I've fixed the
latter, I've had to fix the former too or else the GTK front end would
now undo all of this good work, by _always_ translating 'n' on the
keyboard to UI_NEWGAME, even if the puzzle would have wanted to treat
a real press of 'n' differently. So I've fixed _that_ in turn by
putting those menu accelerators in a GtkAccelGroup that is never
actually enabled on the main window, so the accelerator keys will be
displayed in the menu but not processed by GTK's keyboard handling.

(Also, while I was redoing this code, I've removed the logic in
add_menu_item_with_key that reverse-engineered an ASCII value into
Control and Shift modifiers plus a base key, because the only
arguments to that function were fixed at compile time anyway so it's
easier to just write the results of that conversion directly into the
call sites; and I've added the GTK_ACCEL_LOCKED flag, in recognition
of the fact that _because_ these accelerators are processed by a weird
mechanism, they cannot be dynamically reconfigured by users and
actually work afterwards.)
2017-09-20 18:01:52 +01:00

472 lines
11 KiB
C

/*
* nestedvm.c: NestedVM front end for my puzzle collection.
*/
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <time.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <sys/time.h>
#include "puzzles.h"
extern void _pause();
extern int _call_java(int cmd, int arg1, int arg2, int arg3);
void fatal(char *fmt, ...)
{
va_list ap;
fprintf(stderr, "fatal error: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
struct frontend {
// TODO kill unneeded members!
midend *me;
int timer_active;
struct timeval last_time;
config_item *cfg;
int cfg_which, cfgret;
int ox, oy, w, h;
};
static frontend *_fe;
void get_random_seed(void **randseed, int *randseedsize)
{
struct timeval *tvp = snew(struct timeval);
gettimeofday(tvp, NULL);
*randseed = (void *)tvp;
*randseedsize = sizeof(struct timeval);
}
void frontend_default_colour(frontend *fe, float *output)
{
output[0] = output[1]= output[2] = 0.8f;
}
void nestedvm_status_bar(void *handle, char *text)
{
_call_java(4,0,(int)text,0);
}
void nestedvm_start_draw(void *handle)
{
frontend *fe = (frontend *)handle;
_call_java(5, 0, fe->w, fe->h);
_call_java(4, 1, fe->ox, fe->oy);
}
void nestedvm_clip(void *handle, int x, int y, int w, int h)
{
frontend *fe = (frontend *)handle;
_call_java(5, w, h, 0);
_call_java(4, 3, x + fe->ox, y + fe->oy);
}
void nestedvm_unclip(void *handle)
{
frontend *fe = (frontend *)handle;
_call_java(4, 4, fe->ox, fe->oy);
}
void nestedvm_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
int align, int colour, char *text)
{
frontend *fe = (frontend *)handle;
_call_java(5, x + fe->ox, y + fe->oy,
(fonttype == FONT_FIXED ? 0x10 : 0x0) | align);
_call_java(7, fontsize, colour, (int)text);
}
void nestedvm_draw_rect(void *handle, int x, int y, int w, int h, int colour)
{
frontend *fe = (frontend *)handle;
_call_java(5, w, h, colour);
_call_java(4, 5, x + fe->ox, y + fe->oy);
}
void nestedvm_draw_line(void *handle, int x1, int y1, int x2, int y2,
int colour)
{
frontend *fe = (frontend *)handle;
_call_java(5, x2 + fe->ox, y2 + fe->oy, colour);
_call_java(4, 6, x1 + fe->ox, y1 + fe->oy);
}
void nestedvm_draw_poly(void *handle, int *coords, int npoints,
int fillcolour, int outlinecolour)
{
frontend *fe = (frontend *)handle;
int i;
_call_java(4, 7, npoints, 0);
for (i = 0; i < npoints; i++) {
_call_java(6, i, coords[i*2] + fe->ox, coords[i*2+1] + fe->oy);
}
_call_java(4, 8, outlinecolour, fillcolour);
}
void nestedvm_draw_circle(void *handle, int cx, int cy, int radius,
int fillcolour, int outlinecolour)
{
frontend *fe = (frontend *)handle;
_call_java(5, cx+fe->ox, cy+fe->oy, radius);
_call_java(4, 9, outlinecolour, fillcolour);
}
struct blitter {
int handle, w, h, x, y;
};
blitter *nestedvm_blitter_new(void *handle, int w, int h)
{
blitter *bl = snew(blitter);
bl->handle = -1;
bl->w = w;
bl->h = h;
return bl;
}
void nestedvm_blitter_free(void *handle, blitter *bl)
{
if (bl->handle != -1)
_call_java(4, 11, bl->handle, 0);
sfree(bl);
}
void nestedvm_blitter_save(void *handle, blitter *bl, int x, int y)
{
frontend *fe = (frontend *)handle;
if (bl->handle == -1)
bl->handle = _call_java(4,10,bl->w, bl->h);
bl->x = x;
bl->y = y;
_call_java(8, bl->handle, x + fe->ox, y + fe->oy);
}
void nestedvm_blitter_load(void *handle, blitter *bl, int x, int y)
{
frontend *fe = (frontend *)handle;
assert(bl->handle != -1);
if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
x = bl->x;
y = bl->y;
}
_call_java(9, bl->handle, x + fe->ox, y + fe->oy);
}
void nestedvm_end_draw(void *handle)
{
_call_java(4,2,0,0);
}
char *nestedvm_text_fallback(void *handle, const char *const *strings,
int nstrings)
{
/*
* We assume Java can cope with any UTF-8 likely to be emitted
* by a puzzle.
*/
return dupstr(strings[0]);
}
const struct drawing_api nestedvm_drawing = {
nestedvm_draw_text,
nestedvm_draw_rect,
nestedvm_draw_line,
nestedvm_draw_poly,
nestedvm_draw_circle,
NULL, // draw_update,
nestedvm_clip,
nestedvm_unclip,
nestedvm_start_draw,
nestedvm_end_draw,
nestedvm_status_bar,
nestedvm_blitter_new,
nestedvm_blitter_free,
nestedvm_blitter_save,
nestedvm_blitter_load,
NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
NULL, NULL, /* line_width, line_dotted */
nestedvm_text_fallback,
};
int jcallback_key_event(int x, int y, int keyval)
{
frontend *fe = (frontend *)_fe;
if (fe->ox == -1)
return 1;
if (keyval >= 0 &&
!midend_process_key(fe->me, x - fe->ox, y - fe->oy, keyval))
return 42;
return 1;
}
int jcallback_resize(int width, int height)
{
frontend *fe = (frontend *)_fe;
int x, y;
x = width;
y = height;
midend_size(fe->me, &x, &y, TRUE);
fe->ox = (width - x) / 2;
fe->oy = (height - y) / 2;
fe->w = x;
fe->h = y;
midend_force_redraw(fe->me);
return 0;
}
int jcallback_timer_func()
{
frontend *fe = (frontend *)_fe;
if (fe->timer_active) {
struct timeval now;
float elapsed;
gettimeofday(&now, NULL);
elapsed = ((now.tv_usec - fe->last_time.tv_usec) * 0.000001F +
(now.tv_sec - fe->last_time.tv_sec));
midend_timer(fe->me, elapsed); /* may clear timer_active */
fe->last_time = now;
}
return fe->timer_active;
}
void deactivate_timer(frontend *fe)
{
if (fe->timer_active)
_call_java(4, 13, 0, 0);
fe->timer_active = FALSE;
}
void activate_timer(frontend *fe)
{
if (!fe->timer_active) {
_call_java(4, 12, 0, 0);
gettimeofday(&fe->last_time, NULL);
}
fe->timer_active = TRUE;
}
void jcallback_config_ok()
{
frontend *fe = (frontend *)_fe;
char *err;
err = midend_set_config(fe->me, fe->cfg_which, fe->cfg);
if (err)
_call_java(2, (int) "Error", (int)err, 1);
else {
fe->cfgret = TRUE;
}
}
void jcallback_config_set_string(int item_ptr, int char_ptr) {
config_item *i = (config_item *)item_ptr;
char* newval = (char*) char_ptr;
sfree(i->sval);
i->sval = dupstr(newval);
free(newval);
}
void jcallback_config_set_boolean(int item_ptr, int selected) {
config_item *i = (config_item *)item_ptr;
i->ival = selected != 0 ? TRUE : FALSE;
}
void jcallback_config_set_choice(int item_ptr, int selected) {
config_item *i = (config_item *)item_ptr;
i->ival = selected;
}
static int get_config(frontend *fe, int which)
{
char *title;
config_item *i;
fe->cfg = midend_get_config(fe->me, which, &title);
fe->cfg_which = which;
fe->cfgret = FALSE;
_call_java(10, (int)title, 0, 0);
for (i = fe->cfg; i->type != C_END; i++) {
_call_java(5, (int)i, i->type, (int)i->name);
_call_java(11, (int)i->sval, i->ival, 0);
}
_call_java(12,0,0,0);
free_cfg(fe->cfg);
return fe->cfgret;
}
int jcallback_newgame_event(void)
{
frontend *fe = (frontend *)_fe;
if (!midend_process_key(fe->me, 0, 0, UI_NEWGAME))
return 42;
return 0;
}
int jcallback_undo_event(void)
{
frontend *fe = (frontend *)_fe;
if (!midend_process_key(fe->me, 0, 0, UI_UNDO))
return 42;
return 0;
}
int jcallback_redo_event(void)
{
frontend *fe = (frontend *)_fe;
if (!midend_process_key(fe->me, 0, 0, UI_REDO))
return 42;
return 0;
}
int jcallback_quit_event(void)
{
frontend *fe = (frontend *)_fe;
if (!midend_process_key(fe->me, 0, 0, UI_QUIT))
return 42;
return 0;
}
static void resize_fe(frontend *fe)
{
int x, y;
x = INT_MAX;
y = INT_MAX;
midend_size(fe->me, &x, &y, FALSE);
_call_java(3, x, y, 0);
}
int jcallback_preset_event(int ptr_game_params)
{
frontend *fe = (frontend *)_fe;
game_params *params =
(game_params *)ptr_game_params;
midend_set_params(fe->me, params);
midend_new_game(fe->me);
resize_fe(fe);
_call_java(13, midend_which_preset(fe->me), 0, 0);
return 0;
}
int jcallback_solve_event()
{
frontend *fe = (frontend *)_fe;
char *msg;
msg = midend_solve(fe->me);
if (msg)
_call_java(2, (int) "Error", (int)msg, 1);
return 0;
}
int jcallback_restart_event()
{
frontend *fe = (frontend *)_fe;
midend_restart_game(fe->me);
return 0;
}
int jcallback_config_event(int which)
{
frontend *fe = (frontend *)_fe;
_call_java(13, midend_which_preset(fe->me), 0, 0);
if (!get_config(fe, which))
return 0;
midend_new_game(fe->me);
resize_fe(fe);
_call_java(13, midend_which_preset(fe->me), 0, 0);
return 0;
}
int jcallback_about_event()
{
char titlebuf[256];
char textbuf[1024];
sprintf(titlebuf, "About %.200s", thegame.name);
sprintf(textbuf,
"%.200s\n\n"
"from Simon Tatham's Portable Puzzle Collection\n\n"
"%.500s", thegame.name, ver);
_call_java(2, (int)&titlebuf, (int)&textbuf, 0);
return 0;
}
void preset_menu_populate(struct preset_menu *menu, int menuid)
{
int i;
for (i = 0; i < menu->n_entries; i++) {
struct preset_menu_entry *entry = &menu->entries[i];
if (entry->params) {
_call_java(5, (int)entry->params, 0, 0);
_call_java(1, (int)entry->title, menuid, entry->id);
} else {
_call_java(5, 0, 0, 0);
_call_java(1, (int)entry->title, menuid, entry->id);
preset_menu_populate(entry->submenu, entry->id);
}
}
}
int main(int argc, char **argv)
{
int i, n;
float* colours;
_fe = snew(frontend);
_fe->timer_active = FALSE;
_fe->me = midend_new(_fe, &thegame, &nestedvm_drawing, _fe);
if (argc > 1)
midend_game_id(_fe->me, argv[1]); /* ignore failure */
midend_new_game(_fe->me);
{
struct preset_menu *menu;
int nids, topmenu;
menu = midend_get_presets(_fe->me, &nids);
topmenu = _call_java(1, 0, nids, 0);
preset_menu_populate(menu, topmenu);
}
colours = midend_colours(_fe->me, &n);
_fe->ox = -1;
_call_java(0, (int)thegame.name,
(thegame.can_configure ? 1 : 0) |
(midend_wants_statusbar(_fe->me) ? 2 : 0) |
(thegame.can_solve ? 4 : 0), n);
for (i = 0; i < n; i++) {
_call_java(1024+ i,
(int)(colours[i*3] * 0xFF),
(int)(colours[i*3+1] * 0xFF),
(int)(colours[i*3+2] * 0xFF));
}
resize_fe(_fe);
_call_java(13, midend_which_preset(_fe->me), 0, 0);
// Now pause the vm. The VM will be call()ed when
// an input event occurs.
_pause();
// shut down when the VM is resumed.
deactivate_timer(_fe);
midend_free(_fe->me);
return 0;
}