Added the "reveal word" functionality
[getaclue.git] / player_mainwindow.py
1 # Get A Clue (C) 2010 V. Harishankar
2 # Crossword puzzle maker program
3 # Licensed under the GNU GPL v3
4
5 # Main window class for GetAClue player
6
7 import cPickle
8 import pygtk
9 pygtk.require20 ()
10 import gtk
11 import cairo
12
13 import crosswordpuzzle
14
15 class MainWindow:
16 # typing mode constants
17 ACROSS = 1
18 DOWN = 2
19
20 def gtk_main_quit (self, *args):
21 gtk.main_quit ()
22
23 # callback for menu item quit activated event
24 def on_quit_activate (self, menuitem):
25 if self.puzzle:
26 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
27 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
28 "Puzzle is open. Are you sure you wish to quit?")
29 if dlg.run () <> gtk.RESPONSE_YES:
30 dlg.destroy ()
31 return False
32 dlg.destroy ()
33
34 self.gtk_main_quit ()
35
36 # callback for menu item reveal word activated event
37 def on_revealword_activate (self, menuitem):
38 if self.puzzle:
39 # reveal across/down word if any the position
40 if self.puzzle.data[self.selected_row][self.selected_col].occupied_across is True:
41 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
42 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
43 "Are you sure you wish to reveal across word at current cell?")
44 # confirm that the user wants to reveal
45 if dlg.run () == gtk.RESPONSE_YES:
46 self.puzzle.reveal_word_across (self.selected_row, self.selected_col)
47 # redraw the grid to reveal the word
48 puzgrid = self.ui.get_object ("puzzlegrid")
49 puzgrid.queue_draw ()
50 dlg.destroy ()
51 if self.puzzle.data[self.selected_row][self.selected_col].occupied_down is True:
52 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
53 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
54 "Are you sure wish to reveal down word at current cell?")
55 if dlg.run () == gtk.RESPONSE_YES:
56 self.puzzle.reveal_word_down (self.selected_row, self.selected_col)
57 # redraw the grid to reveal the word
58 puzgrid = self.ui.get_object ("puzzlegrid")
59 puzgrid.queue_draw ()
60 dlg.destroy ()
61
62
63 # function to set the selected row/col based on the number clicked
64 # on the clues list and also set the typing mode
65 def set_selection_of_num (self, num, across = True):
66 # get the row, col of the word
67 row, col = self.puzzle.get_position_of_num (num)
68
69 # set the selected row and column
70 self.selected_row = row
71 self.selected_col = col
72 # set typing mode to across
73 if across is True:
74 self.typing_mode = self.ACROSS
75 else:
76 self.typing_mode = self.DOWN
77
78 # update the puzzle grid
79 puzgrid = self.ui.get_object ("puzzlegrid")
80
81 # set focus to the puzzle grid
82 self.window.set_focus (puzgrid)
83
84 puzgrid.queue_draw ()
85
86 # callback for tree view "across" being activated
87 # activated - when double clicked or enter pressed
88 def on_tree_clues_across_row_activated (self, view, path, column):
89 # get the across list object
90 across_list = self.ui.get_object ("clues_across")
91 # get the number of the across word
92 anum = int (across_list.get_value (across_list.get_iter (path), 0))
93
94 self.set_selection_of_num (anum)
95
96 return False
97
98 # callback for tree view "down" being activated
99 # activated - when double clicked or enter pressed
100 def on_tree_clues_down_row_activated (self, view, path, column):
101 # get the down list object
102 down_list = self.ui.get_object ("clues_down")
103 # get the number of the down word
104 dnum = int (down_list.get_value (down_list.get_iter (path), 0))
105
106 self.set_selection_of_num (dnum, False)
107
108 # moving the current selection in grid by one up or down
109 def move_selection_updown (self, step):
110 # increase or reduce the row by step until an occupied grid is found
111 # black block
112 if self.puzzle:
113 last_occupied_row = self.selected_row
114 while True:
115 self.selected_row += step
116 if self.selected_row < 0 or self.selected_row >= self.puzzle.rows:
117 self.selected_row = last_occupied_row
118 break
119 if (self.puzzle.data[self.selected_row][self.selected_col].occupied_across is True
120 or self.puzzle.data[self.selected_row][self.selected_col].occupied_down is True):
121 break
122
123 # moving the current selection in grid by one across either way
124 def move_selection_across (self, step):
125 # increase or reduce the row by step until an occupied grid is found
126 # black block
127 if self.puzzle:
128 last_occupied_col = self.selected_col
129 while True:
130 self.selected_col += step
131 if self.selected_col < 0 or self.selected_col >= self.puzzle.cols:
132 self.selected_col = last_occupied_col
133 break
134 if (self.puzzle.data[self.selected_row][self.selected_col].occupied_across is True
135 or self.puzzle.data[self.selected_row][self.selected_col].occupied_down is True):
136 break
137
138 # set the guessed character in the grid at selected location and move the
139 # selection across or down as the case may be
140 def set_guess (self, guess_char):
141 if self.puzzle:
142 # set a guess only if not revealed
143 if self.puzzle.data[self.selected_row][self.selected_col].revealed is False:
144 self.puzzle.data[self.selected_row][self.selected_col].guess = guess_char.upper ()
145 # across mode typing
146 if self.typing_mode == self.ACROSS:
147 # move by one character across but only if there is no block
148 # in between
149 old_col = self.selected_col
150 self.move_selection_across (1)
151 if abs (self.selected_col - old_col) > 1:
152 self.selected_col = old_col
153 # down mode typing
154 else:
155 # move by one character down but only if there is no block
156 # in between
157 old_row = self.selected_row
158 self.move_selection_updown (1)
159 if abs (self.selected_row - old_row) > 1:
160 self.selected_row = old_row
161
162 # delete the guessed char in the previous row/col depending on the input mode
163 # If input mode is ACROSS then delete guessed char at previous column else
164 # at previous row
165 def delete_prev_guess (self):
166 if self.puzzle:
167 if self.typing_mode == self.ACROSS:
168 # prevent deleting characters when there is a gap
169 old_sel_col = self.selected_col
170 self.move_selection_across (-1)
171 # only if there is no block inbetween delete
172 if abs (self.selected_col - old_sel_col) <= 1:
173 self.puzzle.data[self.selected_row][self.selected_col].guess = None
174 # reset selection
175 else:
176 self.selected_col = old_sel_col
177 elif self.typing_mode == self.DOWN:
178 # prevent deleting characters when there is a gap
179 old_sel_row = self.selected_row
180 self.move_selection_updown (-1)
181 # only if there is no block inbetween delete
182 if abs (self.selected_row - old_sel_row) <= 1:
183 self.puzzle.data[self.selected_row][self.selected_col].guess = None
184 # reset selection
185 else:
186 self.selected_row = old_sel_row
187
188 # callback for puzzle grid mouse button release event
189 def on_puzzlegrid_button_press_event (self, drawarea, event):
190 # set the focus on the puzzle grid
191 if self.puzzle:
192 self.window.set_focus (drawarea)
193
194 col = int (event.x / 30)
195 row = int (event.y / 30)
196
197 if col < self.puzzle.cols and row < self.puzzle.rows:
198 if (self.puzzle.data[row][col].occupied_across is True or
199 self.puzzle.data[row][col].occupied_down is True):
200 self.selected_col = col
201 self.selected_row = row
202 drawarea.queue_draw ()
203
204 return False
205
206 # callback for puzzle grid key release event
207 def on_puzzlegrid_key_press_event (self, drawarea, event):
208 if self.puzzle:
209 key = gtk.gdk.keyval_name (event.keyval).lower ()
210
211 if event.state == gtk.gdk.SHIFT_MASK and key == "up":
212 # reduce the row by 1 until you find an occupied grid and not a
213 # black block
214 self.move_selection_updown (-1)
215 self.typing_mode = self.DOWN
216 drawarea.queue_draw ()
217 elif event.state == gtk.gdk.SHIFT_MASK and key == "down":
218 # increase the row by 1 until you find an occupied grid and not a
219 # black block
220 self.move_selection_updown (1)
221 self.typing_mode = self.DOWN
222 drawarea.queue_draw ()
223 elif event.state == gtk.gdk.SHIFT_MASK and key == "right":
224 # increase the column by 1 until you find an occupied grid and not
225 # a black block
226 self.move_selection_across (1)
227 self.typing_mode = self.ACROSS
228 drawarea.queue_draw ()
229 elif event.state == gtk.gdk.SHIFT_MASK and key == "left":
230 # decrease the column by 1 until you find an occupied grid and not
231 # a black block
232 self.move_selection_across (-1)
233 self.typing_mode = self.ACROSS
234 drawarea.queue_draw ()
235 # if it is A-Z or a-z then
236 elif len (key) == 1 and key.isalpha ():
237 self.set_guess (key)
238 drawarea.queue_draw ()
239 # if it is the delete key then delete character at selected row/col
240 elif key == "delete" or key == "space":
241 self.puzzle.data[self.selected_row][self.selected_col].guess = None
242 drawarea.queue_draw ()
243 # if it is backspace key then delete character at previous row/col
244 # depending on the input mode. If across editing mode, then delete
245 # at previous column else at previous row
246 elif key == "backspace":
247 self.delete_prev_guess ()
248 drawarea.queue_draw ()
249
250 return False
251
252 # puzzle grid focus in event
253 def on_puzzlegrid_focus_out_event (self, drawarea, event):
254 if self.puzzle:
255 col = drawarea.window.get_colormap ().alloc_color (gtk.gdk.Color ("gray"))
256 drawarea.window.set_background (col)
257
258 return False
259
260 # puzzle grid focus out event
261 def on_puzzlegrid_focus_in_event (self, drawarea, event):
262 if self.puzzle:
263 col = drawarea.window.get_colormap ().alloc_color (gtk.gdk.Color ("white"))
264 drawarea.window.set_background (col)
265 return False
266
267 # callback for drawing the puzzle grid
268 def on_puzzlegrid_expose_event (self, drawarea, event):
269 # if puzzle is loaded
270 if self.puzzle:
271 # size the area
272 drawarea.set_size_request (self.puzzle.cols*30+2, self.puzzle.rows*30+2)
273
274 ctx = drawarea.window.cairo_create ()
275 ctx.set_line_width (1.5)
276
277 # run through the grid
278 for row in range (self.puzzle.rows):
279 for col in range (self.puzzle.cols):
280 # (re)set foreground color
281 ctx.set_source_rgb (0, 0, 0)
282 # if the area is not occupied
283 if (self.puzzle.data[row][col].occupied_across is False and
284 self.puzzle.data[row][col].occupied_down is False):
285 ctx.rectangle (col*30, row*30, 30, 30)
286 ctx.fill ()
287 else:
288 # if selected row/column
289 if row == self.selected_row and col == self.selected_col:
290 ctx.set_source_rgb (1, 1, 0)
291 ctx.rectangle (col*30,row*30, 30, 30)
292 ctx.fill ()
293 else:
294 ctx.set_source_rgb (1, 1, 1)
295 ctx.rectangle (col*30, row*30, 30, 30)
296 ctx.fill ()
297 ctx.set_source_rgb (0, 0, 0)
298 ctx.rectangle (col*30, row*30, 30, 30)
299 ctx.stroke ()
300
301 # if numbered
302 if self.puzzle.data[row][col].numbered <> 0:
303 ctx.set_source_rgb (0, 0, 0)
304 ctx.select_font_face ("Serif")
305 ctx.set_font_size (10)
306 ctx.move_to (col*30+2, row*30+10)
307 ctx.show_text (str(self.puzzle.data[row][col].numbered))
308
309 # if there is a guessed character at the location
310 # and it is not revealed as a solution
311 if (self.puzzle.data[row][col].guess and
312 self.puzzle.data[row][col].revealed is False and
313 (self.puzzle.data[row][col].occupied_across is True
314 or self.puzzle.data[row][col].occupied_down is True)):
315 ctx.set_source_rgb (0, 0, 0)
316 ctx.select_font_face ("Serif", cairo.FONT_SLANT_NORMAL,
317 cairo.FONT_WEIGHT_BOLD)
318 ctx.set_font_size (16)
319 ctx.move_to (col*30+10, row*30+20)
320 ctx.show_text (self.puzzle.data[row][col].guess)
321
322 # if there is a revealed solution character at the location
323 if (self.puzzle.data[row][col].revealed is True and
324 (self.puzzle.data[row][col].occupied_across is True
325 or self.puzzle.data[row][col].occupied_down is True)):
326 ctx.set_source_rgb (0, 0, 0.8)
327 ctx.select_font_face ("Serif", cairo.FONT_SLANT_NORMAL,
328 cairo.FONT_WEIGHT_BOLD)
329 ctx.set_font_size (16)
330 ctx.move_to (col*30+10, row*30+20)
331 ctx.show_text (self.puzzle.data[row][col].char)
332
333 return False
334
335 # load clues to the list
336 def load_clues (self):
337 # get the clues list store objects
338 across = self.ui.get_object ("clues_across")
339 down = self.ui.get_object ("clues_down")
340 across.clear ()
341 down.clear ()
342
343 # if puzzle is loaded
344 if self.puzzle:
345 clues_across = self.puzzle.get_clues_across ()
346 clues_down = self.puzzle.get_clues_down ()
347 # insert the numbers and the clues for across
348 for word, clue in clues_across:
349 across.append ([str(self.puzzle.data[word[1]][word[2]].numbered),
350 clue])
351 # insert the numbers and the clues for down
352 for word, clue in clues_down:
353 down.append ([ str(self.puzzle.data[word[1]][word[2]].numbered),
354 clue])
355
356 def open_file (self, file):
357 # try to open the file
358 try:
359 # load the puzzle
360 self.puzzle = cPickle.load (open (file, "rb"))
361 # assert that it is unfrozen otherwise raise frozen grid exception
362 self.puzzle.assert_frozen_grid ()
363
364 # set selected initial row and column to 0
365 self.selected_row = 0
366 self.selected_col = 0
367 # set the typing mode to default - across
368 self.typing_mode = self.ACROSS
369
370 self.window.set_title ("GetAClue player - " + file)
371 # load the clues
372 self.load_clues ()
373 # handle unpickling, and file errors
374 except (cPickle.UnpicklingError, IOError, OSError):
375 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
376 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
377 "Invalid file. Cannot be loaded")
378 dlg.run ()
379 dlg.destroy ()
380 # if the puzzle has no words, then it cannot be played obviously
381 except crosswordpuzzle.NoWordsException:
382 self.puzzle = None
383 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
384 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
385 "Word grid has no words. Cannot play")
386 dlg.run ()
387 dlg.destroy ()
388 # if the puzzle is not frozen then it cannot be played
389 except crosswordpuzzle.FrozenGridException:
390 self.puzzle = None
391 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
392 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
393 "Word grid is not finalized/frozen. Cannot play")
394 dlg.run ()
395 dlg.destroy ()
396
397 def __init__ (self, file_to_play = None):
398 # load the user interface
399 self.ui = gtk.Builder ()
400 self.ui.add_from_file ("playerwindow.glade")
401
402 # window object
403 self.window = self.ui.get_object ("mainwindow")
404 self.window.show ()
405
406 # set the cell renderer for tree views
407 cell = gtk.CellRendererText ()
408 tree_acol1 = self.ui.get_object ("tree_clues_across").get_column (0)
409 tree_acol2 = self.ui.get_object ("tree_clues_across").get_column (1)
410 tree_acol1.pack_start (cell)
411 tree_acol1.add_attribute (cell, "text", 0)
412 tree_acol2.pack_start (cell)
413 tree_acol2.add_attribute (cell, "text", 1)
414
415 tree_down = self.ui.get_object ("tree_clues_down")
416 tree_dcol1 = self.ui.get_object ("tree_clues_down").get_column (0)
417 tree_dcol2 = self.ui.get_object ("tree_clues_down").get_column (1)
418 tree_dcol1.pack_start (cell)
419 tree_dcol1.add_attribute (cell, "text", 0)
420 tree_dcol2.pack_start (cell)
421 tree_dcol2.add_attribute (cell, "text", 1)
422
423 # connect the signals
424 self.ui.connect_signals (self)
425
426 # set the puzzle to None
427 self.puzzle = None
428
429 # open the file if it is set
430 if file_to_play:
431 self.open_file (file_to_play)
432
433 gtk.main ()
434
435