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