shithub: puzzles

Download patch

ref: a7431c0b7ce232f296ebcd70172ca64e58300105
parent: c6b1d4472b2f339c54c9c9de06c6ebef2a92dba9
author: Simon Tatham <[email protected]>
date: Sat Sep 6 05:27:56 EDT 2008

New infrastructure feature. Games are now permitted to be
_conditionally_ able to format the current puzzle as text to be sent
to the clipboard. For instance, if a game were to support playing on
a square grid and on other kinds of grid such as hexagonal, then it
might reasonably feel that only the former could be sensibly
rendered in ASCII art; so it can now arrange for the "Copy" menu
item to be greyed out depending on the game_params.

To do this I've introduced a new backend function
(can_format_as_text_now()), and renamed the existing static backend
field "can_format_as_text" to "can_format_as_text_ever". The latter
will cause compile errors for anyone maintaining a third-party front
end; if any such person is reading this, I apologise to them for the
inconvenience, but I did do it deliberately so that they'd know to
update their front end.

As yet, no checked-in game actually uses this feature; all current
games can still either copy always or copy never.

[originally from svn r8161]

--- a/blackbox.c
+++ b/blackbox.c
@@ -463,6 +463,11 @@
     return dupstr("S");
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1413,7 +1418,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/bridges.c
+++ b/bridges.c
@@ -147,6 +147,11 @@
     }
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     int x, y, len, nl;
@@ -2644,7 +2649,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/cube.c
+++ b/cube.c
@@ -985,6 +985,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1715,7 +1720,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/devel.but
+++ b/devel.but
@@ -1365,9 +1365,9 @@
 
 \H{backend-misc} Miscellaneous
 
-\S{backend-can-format-as-text} \c{can_format_as_text}
+\S{backend-can-format-as-text-ever} \c{can_format_as_text_ever}
 
-\c int can_format_as_text;
+\c int can_format_as_text_ever;
 
 This boolean field is \cw{TRUE} if the game supports formatting a
 game state as ASCII text (typically ASCII art) for copying to the
@@ -1374,9 +1374,42 @@
 clipboard and pasting into other applications. If it is \cw{FALSE},
 front ends will not offer the \q{Copy} command at all.
 
-If this field is \cw{FALSE}, the function \cw{text_format()}
-(\k{backend-text-format}) is not expected to do anything at all.
+If this field is \cw{TRUE}, the game does not necessarily have to
+support text formatting for \e{all} games: e.g. a game which can be
+played on a square grid or a triangular one might only support copy
+and paste for the former, because triangular grids in ASCII art are
+just too difficult.
 
+If this field is \cw{FALSE}, the functions
+\cw{can_format_as_text_now()} (\k{backend-can-format-as-text-now})
+and \cw{text_format()} (\k{backend-text-format}) are never called.
+
+\S{backend-can-format-as-text-now} \c{can_format_as_text_now()}
+
+\c int (*can_format_as_text_now)(game_params *params);
+
+This function is passed a \c{game_params} and returns a boolean,
+which is \cw{TRUE} if the game can support ASCII text output for
+this particular game type. If it returns \cw{FALSE}, front ends will
+grey out or otherwise disable the \q{Copy} command.
+
+Games may enable and disable the copy-and-paste function for
+different game \e{parameters}, but are currently constrained to
+return the same answer from this function for all game \e{states}
+sharing the same parameters. In other words, the \q{Copy} function
+may enable or disable itself when the player changes game preset,
+but will never change during play of a single game or when another
+game of exactly the same type is generated.
+
+This function should not take into account aspects of the game
+parameters which are not encoded by \cw{encode_params()}
+(\k{backend-encode-params}) when the \c{full} parameter is set to
+\cw{FALSE}. Such parameters will not necessarily match up between a
+call to this function and a subsequent call to \cw{text_format()}
+itself. (For instance, game \e{difficulty} should not affect whether
+the game can be copied to the clipboard. Only the actual visible
+\e{shape} of the game can affect that.)
+
 \S{backend-text-format} \cw{text_format()}
 
 \c char *(*text_format)(game_state *state);
@@ -1386,9 +1419,11 @@
 state. It is used to implement the \q{Copy} operation in many front
 ends.
 
-This function should only be called if the back end field
-\c{can_format_as_text} (\k{backend-can-format-as-text}) is
-\cw{TRUE}.
+This function will only ever be called if the back end field
+\c{can_format_as_text_ever} (\k{backend-can-format-as-text-ever}) is
+\cw{TRUE} \e{and} the function \cw{can_format_as_text_now()}
+(\k{backend-can-format-as-text-now}) has returned \cw{TRUE} for the
+currently selected game parameters.
 
 The returned string may contain line endings (and will probably want
 to), using the normal C internal \cq{\\n} convention. For
@@ -2852,6 +2887,16 @@
 \cq{params:description}) describing the game currently active in the
 mid-end. The returned string is dynamically allocated.
 
+\H{midend-can-format-as-text-now} \cw{midend_can_format_as_text_now()}
+
+\c int midend_can_format_as_text_now(midend *me);
+
+Returns \cw{TRUE} if the game code is capable of formatting puzzles
+of the currently selected game type as ASCII.
+
+If this returns \cw{FALSE}, then \cw{midend_text_format()}
+(\k{midend-text-format}) will return \cw{NULL}.
+
 \H{midend-text-format} \cw{midend_text_format()}
 
 \c char *midend_text_format(midend *me);
@@ -2860,8 +2905,9 @@
 copying to the clipboard. The returned string is dynamically
 allocated.
 
-You should not call this function if the game's
-\c{can_format_as_text} flag is \cw{FALSE}.
+If the game's \c{can_format_as_text_ever} flag is \cw{FALSE}, or if
+its \cw{can_format_as_text_now()} function returns \cw{FALSE}, then
+this function will return \cw{NULL}.
 
 If the returned string contains multiple lines (which is likely), it
 will use the normal C line ending convention (\cw{\\n} only). On
@@ -2964,8 +3010,8 @@
 \b fetching the \c{name} field to use in window titles and similar
 
 \b reading the \c{can_configure}, \c{can_solve} and
-\c{can_format_as_text} fields to decide whether to add those items
-to the menu bar or equivalent
+\c{can_format_as_text_ever} fields to decide whether to add those
+items to the menu bar or equivalent
 
 \b reading the \c{winhelp_topic} field (Windows only)
 
--- a/dominosa.c
+++ b/dominosa.c
@@ -1171,6 +1171,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1755,7 +1760,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/fifteen.c
+++ b/fifteen.c
@@ -383,6 +383,11 @@
     return dupstr("S");
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     char *ret, *p, buf[80];
@@ -858,7 +863,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/filling.c
+++ b/filling.c
@@ -275,6 +275,11 @@
     return repr;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     const int w = state->shared->params.w;
@@ -1650,7 +1655,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/flip.c
+++ b/flip.c
@@ -853,6 +853,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1281,7 +1286,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/galaxies.c
+++ b/galaxies.c
@@ -335,6 +335,11 @@
 
 #define IS_VERTICAL_EDGE(x) ((x % 2) == 0)
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     int maxlen = (state->sx+1)*state->sy, x, y;
@@ -3425,7 +3430,7 @@
 #else
     TRUE, solve_game,
 #endif
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/gtk.c
+++ b/gtk.c
@@ -79,7 +79,7 @@
  * GTK front end to puzzles.
  */
 
-static void update_preset_tick(frontend *fe);
+static void changed_preset(frontend *fe);
 
 struct font {
 #ifdef USE_PANGO
@@ -127,6 +127,7 @@
     int npresets;
     GtkWidget **preset_bullets;
     GtkWidget *preset_custom_bullet;
+    GtkWidget *copy_menu_item;
 };
 
 void get_random_seed(void **randseed, int *randseedsize)
@@ -849,7 +850,7 @@
     else {
 	fe->cfgret = TRUE;
 	gtk_widget_destroy(fe->cfgbox);
-	update_preset_tick(fe);
+	changed_preset(fe);
     }
 }
 
@@ -1115,11 +1116,18 @@
     }
 }
 
-static void update_preset_tick(frontend *fe)
+/*
+ * Called when any other code in this file has changed the
+ * selected game parameters.
+ */
+static void changed_preset(frontend *fe)
 {
     int n = midend_which_preset(fe->me);
     int i;
 
+    /*
+     * Update the tick mark in the Type menu.
+     */
     if (fe->preset_bullets) {
 	for (i = 0; i < fe->npresets; i++)
 	    update_menuitem_bullet(fe->preset_bullets[i], n == i);
@@ -1127,6 +1135,14 @@
     if (fe->preset_custom_bullet) {
 	update_menuitem_bullet(fe->preset_custom_bullet, n < 0);
     }
+
+    /*
+     * Update the greying on the Copy menu option.
+     */
+    if (fe->copy_menu_item) {
+	int enabled = midend_can_format_as_text_now(fe->me);
+	gtk_widget_set_sensitive(fe->copy_menu_item, enabled);
+    }
 }
 
 static void resize_fe(frontend *fe)
@@ -1158,7 +1174,7 @@
 
     midend_set_params(fe->me, params);
     midend_new_game(fe->me);
-    update_preset_tick(fe);
+    changed_preset(fe);
     resize_fe(fe);
 }
 
@@ -1388,7 +1404,7 @@
             return;
         }
 
-	update_preset_tick(fe);
+	changed_preset(fe);
         resize_fe(fe);
     }
 }
@@ -1673,7 +1689,7 @@
 	} else
 	    fe->preset_custom_bullet = NULL;
 
-	update_preset_tick(fe);
+	changed_preset(fe);
     } else {
 	fe->npresets = 0;
 	fe->preset_bullets = NULL;
@@ -1694,7 +1710,7 @@
     add_menu_separator(GTK_CONTAINER(menu));
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u');
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", 'r');
-    if (thegame.can_format_as_text) {
+    if (thegame.can_format_as_text_ever) {
 	add_menu_separator(GTK_CONTAINER(menu));
 	menuitem = gtk_menu_item_new_with_label("Copy");
 	gtk_container_add(GTK_CONTAINER(menu), menuitem);
@@ -1701,6 +1717,9 @@
 	gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
 			   GTK_SIGNAL_FUNC(menu_copy_event), fe);
 	gtk_widget_show(menuitem);
+	fe->copy_menu_item = menuitem;
+    } else {
+	fe->copy_menu_item = NULL;
     }
     if (thegame.can_solve) {
 	add_menu_separator(GTK_CONTAINER(menu));
--- a/guess.c
+++ b/guess.c
@@ -371,6 +371,11 @@
     return dupstr("S");
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1339,7 +1344,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/inertia.c
+++ b/inertia.c
@@ -1445,6 +1445,11 @@
     return soln;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -2184,7 +2189,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/lightup.c
+++ b/lightup.c
@@ -1705,6 +1705,11 @@
     return move;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 /* 'borrowed' from slant.c, mainly. I could have printed it one
  * character per cell (like debug_state) but that comes out tiny.
  * 'L' is used for 'light here' because 'O' looks too much like '0'
@@ -2240,7 +2245,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/loopy.c
+++ b/loopy.c
@@ -151,6 +151,11 @@
     char *clue_error;
 };
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state);
 static char *state_to_text(const game_state *state);
 static char *validate_desc(game_params *params, char *desc);
@@ -3821,7 +3826,7 @@
     dup_game,
     free_game,
     1, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/map.c
+++ b/map.c
@@ -2247,6 +2247,11 @@
     return dupstr(aux);
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -3122,7 +3127,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/midend.c
+++ b/midend.c
@@ -1198,9 +1198,18 @@
     return NULL;
 }
 
+int midend_can_format_as_text_now(midend *me)
+{
+    if (me->ourgame->can_format_as_text_ever)
+	return me->ourgame->can_format_as_text_now(me->params);
+    else
+	return FALSE;
+}
+
 char *midend_text_format(midend *me)
 {
-    if (me->ourgame->can_format_as_text && me->statepos > 0)
+    if (me->ourgame->can_format_as_text_ever && me->statepos > 0 &&
+	me->ourgame->can_format_as_text_now(me->params))
 	return me->ourgame->text_format(me->states[me->statepos-1].state);
     else
 	return NULL;
--- a/mines.c
+++ b/mines.c
@@ -2312,6 +2312,11 @@
     return dupstr("S");
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     char *ret;
@@ -3091,7 +3096,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/net.c
+++ b/net.c
@@ -1782,6 +1782,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -3005,7 +3010,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/netslide.c
+++ b/netslide.c
@@ -895,6 +895,11 @@
     return dupstr(aux);
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1795,7 +1800,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/nullgame.c
+++ b/nullgame.c
@@ -123,6 +123,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -266,7 +271,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/osx.m
+++ b/osx.m
@@ -806,7 +806,8 @@
 - (BOOL)validateMenuItem:(NSMenuItem *)item
 {
     if ([item action] == @selector(copy:))
-	return (ourgame->can_format_as_text ? YES : NO);
+	return (ourgame->can_format_as_text_ever &&
+		midend_can_format_as_text_now(me) ? YES : NO);
     else if ([item action] == @selector(solveGame:))
 	return (ourgame->can_solve ? YES : NO);
     else
--- a/pattern.c
+++ b/pattern.c
@@ -728,6 +728,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1269,7 +1274,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/pegs.c
+++ b/pegs.c
@@ -711,6 +711,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     int w = state->w, h = state->h;
@@ -1201,7 +1206,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/puzzles.h
+++ b/puzzles.h
@@ -242,6 +242,7 @@
 char *midend_set_config(midend *me, int which, config_item *cfg);
 char *midend_game_id(midend *me, char *id);
 char *midend_get_game_id(midend *me);
+int midend_can_format_as_text_now(midend *me);
 char *midend_text_format(midend *me);
 char *midend_solve(midend *me);
 void midend_supersede_game_desc(midend *me, char *desc, char *privdesc);
@@ -419,7 +420,8 @@
     int can_solve;
     char *(*solve)(game_state *orig, game_state *curr,
                    char *aux, char **error);
-    int can_format_as_text;
+    int can_format_as_text_ever;
+    int (*can_format_as_text_now)(game_params *params);
     char *(*text_format)(game_state *state);
     game_ui *(*new_ui)(game_state *state);
     void (*free_ui)(game_ui *ui);
--- a/rect.c
+++ b/rect.c
@@ -2043,6 +2043,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     char *ret, *p, buf[80];
@@ -2878,7 +2883,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/samegame.c
+++ b/samegame.c
@@ -1022,6 +1022,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     char *ret, *p;
@@ -1641,7 +1646,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/sixteen.c
+++ b/sixteen.c
@@ -509,6 +509,11 @@
     return dupstr("S");
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     char *ret, *p, buf[80];
@@ -1029,7 +1034,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/slant.c
+++ b/slant.c
@@ -1615,6 +1615,11 @@
     return move;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     int w = state->p.w, h = state->p.h, W = w+1, H = h+1;
@@ -2201,7 +2206,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/solo.c
+++ b/solo.c
@@ -3162,6 +3162,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return grid_text_format(state->cr, state->blocks, state->xtype,
@@ -3935,7 +3940,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/tents.c
+++ b/tents.c
@@ -1369,6 +1369,11 @@
     }
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     int w = state->p.w, h = state->p.h;
@@ -2068,7 +2073,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/twiddle.c
+++ b/twiddle.c
@@ -545,6 +545,11 @@
     return dupstr("S");
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     char *ret, *p, buf[80];
@@ -1196,7 +1201,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/unequal.c
+++ b/unequal.c
@@ -408,6 +408,11 @@
     return -1;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     int x, y, len, n;
@@ -1736,7 +1741,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/unfinished/pearl.c
+++ b/unfinished/pearl.c
@@ -1236,6 +1236,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1379,7 +1384,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/unfinished/separate.c
+++ b/unfinished/separate.c
@@ -680,6 +680,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -823,7 +828,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/unfinished/slide.c
+++ b/unfinished/slide.c
@@ -1166,6 +1166,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return board_text_format(state->w, state->h, state->board,
@@ -2317,7 +2322,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    TRUE, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/unfinished/sokoban.c
+++ b/unfinished/sokoban.c
@@ -907,6 +907,11 @@
     return NULL;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1429,7 +1434,7 @@
     dup_game,
     free_game,
     FALSE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/untangle.c
+++ b/untangle.c
@@ -1018,6 +1018,11 @@
     return ret;
 }
 
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
 static char *game_text_format(game_state *state)
 {
     return NULL;
@@ -1440,7 +1445,7 @@
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
--- a/windows.c
+++ b/windows.c
@@ -195,7 +195,7 @@
     HBRUSH *brushes;
     HPEN *pens;
     HRGN clip;
-    HMENU typemenu;
+    HMENU gamemenu, typemenu;
     UINT timer;
     DWORD timer_last_tickcount;
     int npresets;
@@ -222,6 +222,7 @@
 };
 
 static void update_type_menu_tick(frontend *fe);
+static void update_copy_menu_greying(frontend *fe);
 
 void fatal(char *fmt, ...)
 {
@@ -1573,6 +1574,7 @@
 	HMENU menu = SHGetSubMenu(SHFindMenuBar(fe->hwnd), ID_GAME);
 	DeleteMenu(menu, 0, MF_BYPOSITION);
 #endif
+	fe->gamemenu = menu;
 	AppendMenu(menu, MF_ENABLED, IDM_NEW, TEXT("&New"));
 	AppendMenu(menu, MF_ENABLED, IDM_RESTART, TEXT("&Restart"));
 #ifndef _WIN32_WCE
@@ -1635,7 +1637,7 @@
 	AppendMenu(menu, MF_ENABLED, IDM_UNDO, TEXT("Undo"));
 	AppendMenu(menu, MF_ENABLED, IDM_REDO, TEXT("Redo"));
 #ifndef _WIN32_WCE
-	if (thegame.can_format_as_text) {
+	if (thegame.can_format_as_text_ever) {
 	    AppendMenu(menu, MF_SEPARATOR, 0, 0);
 	    AppendMenu(menu, MF_ENABLED, IDM_COPY, TEXT("&Copy"));
 	}
@@ -1680,6 +1682,7 @@
     SetForegroundWindow(fe->hwnd);
 
     update_type_menu_tick(fe);
+    update_copy_menu_greying(fe);
 
     midend_redraw(fe->me);
 
@@ -2657,11 +2660,19 @@
     DrawMenuBar(fe->hwnd);
 }
 
+static void update_copy_menu_greying(frontend *fe)
+{
+    UINT enable = (midend_can_format_as_text_now(fe->me) ?
+		   MF_ENABLED : MF_GRAYED);
+    EnableMenuItem(fe->gamemenu, IDM_COPY, MF_BYCOMMAND | enable);
+}
+
 static void new_game_type(frontend *fe)
 {
     midend_new_game(fe->me);
     new_game_size(fe);
     update_type_menu_tick(fe);
+    update_copy_menu_greying(fe);
 }
 
 static int is_alt_pressed(void)