Added check for non-alphabetic words
[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 sys
8 import os.path
9 import cPickle
10 import pygtk
11 pygtk.require20 ()
12 import gtk
13 import cairo
14
15 import crosswordpuzzle
16
17 class MainWindow:
18 # typing mode constants
19 ACROSS = 1
20 DOWN = 2
21 # license text to be displayed in the about dialog
22 LICENSE_TEXT = """GetAClue is free software: you can redistribute it and/or modify
23 it under the terms of the GNU General Public License as published by
24 the Free Software Foundation, either version 3 of the License, or
25 (at your option) any later version.
26
27 GetAClue is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 GNU General Public License for more details.
31
32 You should have received a copy of the GNU General Public License
33 along with GetAClue. If not, see <http://www.gnu.org/licenses/>."""
34
35 # quit verification
36 def verify_quit (self):
37 if self.puzzle:
38 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
39 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
40 "Puzzle is open. Are you sure you wish to quit?")
41 if dlg.run () <> gtk.RESPONSE_YES:
42 dlg.destroy ()
43 return False
44 dlg.destroy ()
45 return True
46
47 # callback for menu item open activated event
48 def on_open_activate (self, menuitem):
49 dlg = gtk.FileChooserDialog ("Open a GetAClue puzzle", self.window,
50 gtk.FILE_CHOOSER_ACTION_OPEN,
51 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
52
53 if dlg.run () == gtk.RESPONSE_OK:
54 puzzlefile = dlg.get_filename ()
55 self.open_file (puzzlefile)
56
57 dlg.destroy ()
58
59 # callback for menu item save as activated event
60 def on_save_as_activate (self, menuitem):
61 if self.puzzle:
62 dlg = gtk.FileChooserDialog ("Save GetAClue puzzle as", self.window,
63 gtk.FILE_CHOOSER_ACTION_SAVE,
64 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE,
65 gtk.RESPONSE_OK))
66 if dlg.run () == gtk.RESPONSE_OK:
67 puzzlefile = dlg.get_filename ()
68 self.save_file (puzzlefile)
69
70 dlg.destroy ()
71
72 # callback for main window destroy
73 def on_mainwindow_destroy (self, args):
74 gtk.main_quit ()
75
76
77 # callback for window closing dialog
78 def on_mainwindow_delete_event (self, window, event):
79 # verify whether really to quit or not if a puzzle is open
80 v = self.verify_quit ()
81 # return False for deleting and True for not deleting
82 return not v
83
84 # callback for menu item quit activated event
85 def on_quit_activate (self, menuitem):
86 # verify whether really to quit or not if a puzzle is open
87 v = self.verify_quit ()
88 # if verified, then quit
89 if v is True:
90 self.window.destroy ()
91
92 # callback for menu item clear grid activated event
93 def on_cleargrid_activate (self, menuitem):
94 if self.puzzle:
95 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
96 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
97 "Are you sure you wish to clear your entries?")
98 if dlg.run () == gtk.RESPONSE_YES:
99 # clear the guesses
100 self.puzzle.clear_guesses ()
101 # redraw the grid
102 puzgrid = self.ui.get_object ("puzzlegrid")
103 puzgrid.queue_draw ()
104 dlg.destroy()
105
106 # callback for menu item verify board activated event
107 def on_verify_activate (self, menuitem):
108 if self.puzzle:
109 try:
110 ans = self.puzzle.is_solution_correct ()
111 # if the solution is correct
112 if ans:
113 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
114 gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE,
115 "Success! Your entries are correct.")
116 dlg.run ()
117 else:
118 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
119 gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE,
120 "Your solution has some errors. Fix them and try again")
121 dlg.run ()
122 except crosswordpuzzle.IncompleteSolutionException:
123 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
124 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
125 "You've not completed the board yet. Cannot verify")
126 dlg.run ()
127 dlg.destroy ()
128
129 # callback for menu item hide solution activated event
130 def on_hidesolution_activate (self, menuitem):
131 if self.puzzle:
132 # hide the solution
133 self.puzzle.reveal_solution (False)
134 puzgrid = self.ui.get_object ("puzzlegrid")
135 # redraw the grid
136 puzgrid.queue_draw ()
137
138 # callback for menu item reveal solution activated event
139 def on_revealsolution_activate (self, menuitem):
140 if self.puzzle:
141 # confirm first
142 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
143 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
144 "This will reveal all words in the puzzle! Are you sure?")
145 if dlg.run () == gtk.RESPONSE_YES:
146 # reveal the solution
147 self.puzzle.reveal_solution ()
148 # redraw the grid
149 puzgrid = self.ui.get_object ("puzzlegrid")
150 puzgrid.queue_draw ()
151 dlg.destroy ()
152
153 # callback for menu item reveal word activated event
154 def on_revealword_activate (self, menuitem):
155 if self.puzzle:
156 # reveal across/down word if any the position
157 if self.puzzle.data[self.selected_row][self.selected_col].occupied_across is True:
158 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
159 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
160 "Are you sure you wish to reveal across word at current cell?")
161 # confirm that the user wants to reveal
162 if dlg.run () == gtk.RESPONSE_YES:
163 self.puzzle.reveal_word_across (self.selected_row, self.selected_col)
164 # redraw the grid to reveal the word
165 puzgrid = self.ui.get_object ("puzzlegrid")
166 puzgrid.queue_draw ()
167 dlg.destroy ()
168 if self.puzzle.data[self.selected_row][self.selected_col].occupied_down is True:
169 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
170 gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
171 "Are you sure wish to reveal down word at current cell?")
172 if dlg.run () == gtk.RESPONSE_YES:
173 self.puzzle.reveal_word_down (self.selected_row, self.selected_col)
174 # redraw the grid to reveal the word
175 puzgrid = self.ui.get_object ("puzzlegrid")
176 puzgrid.queue_draw ()
177 dlg.destroy ()
178
179 # callback for menu help about activated event
180 def on_about_activate (self, menu_item):
181 # display the about dialog
182 self.about ()
183
184 # function to set the selected row/col based on the number clicked
185 # on the clues list and also set the typing mode
186 def set_selection_of_num (self, num, across = True):
187 # get the row, col of the word
188 row, col = self.puzzle.get_position_of_num (num)
189
190 # set the selected row and column
191 self.selected_row = row
192 self.selected_col = col
193 # set typing mode to across
194 if across is True:
195 self.typing_mode = self.ACROSS
196 else:
197 self.typing_mode = self.DOWN
198
199 # update the puzzle grid
200 puzgrid = self.ui.get_object ("puzzlegrid")
201
202 puzgrid.queue_draw ()
203
204 # callback for tree view "across" being activated
205 # activated - when double clicked or enter pressed
206 def on_tree_clues_across_row_activated (self, view, path, column):
207 # get the across list object
208 across_list = self.ui.get_object ("clues_across")
209 # get the number of the across word
210 anum = int (across_list.get_value (across_list.get_iter (path), 0))
211
212 self.set_selection_of_num (anum)
213
214 # focus on the drawing area
215 puzgrid = self.ui.get_object ("puzzlegrid")
216 self.window.set_focus (puzgrid)
217
218 return False
219
220 # callback for tree view "down" being activated
221 # activated - when double clicked or enter pressed
222 def on_tree_clues_down_row_activated (self, view, path, column):
223 # get the down list object
224 down_list = self.ui.get_object ("clues_down")
225 # get the number of the down word
226 dnum = int (down_list.get_value (down_list.get_iter (path), 0))
227
228 self.set_selection_of_num (dnum, False)
229
230 # focus on the drawing area
231 puzgrid = self.ui.get_object ("puzzlegrid")
232 self.window.set_focus (puzgrid)
233
234 # moving the current selection in grid by one up or down
235 def move_selection_updown (self, step):
236 # increase or reduce the row by step until an occupied grid is found
237 # black block
238 if self.puzzle:
239 last_occupied_row = self.selected_row
240 while True:
241 self.selected_row += step
242 if self.selected_row < 0 or self.selected_row >= self.puzzle.rows:
243 self.selected_row = last_occupied_row
244 break
245 if (self.puzzle.data[self.selected_row][self.selected_col].occupied_across is True
246 or self.puzzle.data[self.selected_row][self.selected_col].occupied_down is True):
247 break
248
249 # moving the current selection in grid by one across either way
250 def move_selection_across (self, step):
251 # increase or reduce the row by step until an occupied grid is found
252 # black block
253 if self.puzzle:
254 last_occupied_col = self.selected_col
255 while True:
256 self.selected_col += step
257 if self.selected_col < 0 or self.selected_col >= self.puzzle.cols:
258 self.selected_col = last_occupied_col
259 break
260 if (self.puzzle.data[self.selected_row][self.selected_col].occupied_across is True
261 or self.puzzle.data[self.selected_row][self.selected_col].occupied_down is True):
262 break
263
264 # set the guessed character in the grid at selected location and move the
265 # selection across or down as the case may be
266 def set_guess (self, guess_char):
267 if self.puzzle:
268 # set a guess only if not revealed
269 if self.puzzle.data[self.selected_row][self.selected_col].revealed is False:
270 self.puzzle.data[self.selected_row][self.selected_col].guess = guess_char
271 # across mode typing
272 if self.typing_mode == self.ACROSS:
273 # move by one character across but only if there is no block
274 # in between
275 old_col = self.selected_col
276 self.move_selection_across (1)
277 if abs (self.selected_col - old_col) > 1:
278 self.selected_col = old_col
279 # down mode typing
280 else:
281 # move by one character down but only if there is no block
282 # in between
283 old_row = self.selected_row
284 self.move_selection_updown (1)
285 if abs (self.selected_row - old_row) > 1:
286 self.selected_row = old_row
287
288 # delete the guessed char in the previous row/col depending on the input mode
289 # If input mode is ACROSS then delete guessed char at previous column else
290 # at previous row
291 def delete_prev_guess (self):
292 if self.puzzle:
293 if self.typing_mode == self.ACROSS:
294 # prevent deleting characters when there is a gap
295 old_sel_col = self.selected_col
296 self.move_selection_across (-1)
297 # only if there is no block inbetween delete
298 if abs (self.selected_col - old_sel_col) <= 1:
299 self.puzzle.data[self.selected_row][self.selected_col].guess = None
300 # reset selection
301 else:
302 self.selected_col = old_sel_col
303 elif self.typing_mode == self.DOWN:
304 # prevent deleting characters when there is a gap
305 old_sel_row = self.selected_row
306 self.move_selection_updown (-1)
307 # only if there is no block inbetween delete
308 if abs (self.selected_row - old_sel_row) <= 1:
309 self.puzzle.data[self.selected_row][self.selected_col].guess = None
310 # reset selection
311 else:
312 self.selected_row = old_sel_row
313
314 # callback for puzzle grid mouse button release event
315 def on_puzzlegrid_button_press_event (self, drawarea, event):
316 # set the focus on the puzzle grid
317 if self.puzzle:
318 self.window.set_focus (drawarea)
319
320 col = int (event.x / 30)
321 row = int (event.y / 30)
322
323 if col < self.puzzle.cols and row < self.puzzle.rows:
324 if (self.puzzle.data[row][col].occupied_across is True or
325 self.puzzle.data[row][col].occupied_down is True):
326 self.selected_col = col
327 self.selected_row = row
328 drawarea.queue_draw ()
329
330 return False
331
332 # callback for main window key release event
333 def on_puzzlegrid_key_press_event (self, drawarea, event):
334 if self.puzzle:
335 key = gtk.gdk.keyval_name (event.keyval).lower ()
336
337 if key == "up":
338 # reduce the row by 1 until you find an occupied grid and not a
339 # black block
340 self.move_selection_updown (-1)
341 self.typing_mode = self.DOWN
342 drawarea.queue_draw ()
343 return True
344 elif key == "down":
345 # increase the row by 1 until you find an occupied grid and not a
346 # black block
347 self.move_selection_updown (1)
348 self.typing_mode = self.DOWN
349 drawarea.queue_draw ()
350 return True
351 elif key == "right":
352 # increase the column by 1 until you find an occupied grid and not
353 # a black block
354 self.move_selection_across (1)
355 self.typing_mode = self.ACROSS
356 drawarea.queue_draw ()
357 return True
358 elif key == "left":
359 # decrease the column by 1 until you find an occupied grid and not
360 # a black block
361 self.move_selection_across (-1)
362 self.typing_mode = self.ACROSS
363 drawarea.queue_draw ()
364 return True
365 # if it is A-Z or a-z then
366 elif len (key) == 1 and key.isalpha ():
367 guess_char = key.upper ()
368 self.set_guess (guess_char)
369 drawarea.queue_draw ()
370 return True
371 # if it is the delete key then delete character at selected row/col
372 elif key == "delete":
373 self.puzzle.data[self.selected_row][self.selected_col].guess = None
374 drawarea.queue_draw ()
375 return True
376 # if the key is space key then delete character and move across or
377 # down one step depending on the mode
378 elif key == "space":
379 self.set_guess (None)
380 drawarea.queue_draw ()
381 return True
382 # if it is backspace key then delete character at previous row/col
383 # depending on the input mode. If across editing mode, then delete
384 # at previous column else at previous row
385 elif key == "backspace":
386 self.delete_prev_guess ()
387 drawarea.queue_draw ()
388 return True
389
390 return False
391 # puzzle grid focus in event
392 def on_puzzlegrid_focus_out_event (self, drawarea, event):
393 if self.puzzle:
394 col = drawarea.window.get_colormap ().alloc_color (gtk.gdk.Color ("gray"))
395 drawarea.window.set_background (col)
396
397 return False
398
399 # puzzle grid focus out event
400 def on_puzzlegrid_focus_in_event (self, drawarea, event):
401 if self.puzzle:
402 col = drawarea.window.get_colormap ().alloc_color (gtk.gdk.Color ("white"))
403 drawarea.window.set_background (col)
404 return False
405
406 # callback for drawing the puzzle grid
407 def on_puzzlegrid_expose_event (self, drawarea, event):
408 # if puzzle is loaded
409 if self.puzzle:
410 # size the area
411 drawarea.set_size_request (self.puzzle.cols*30+2, self.puzzle.rows*30+2)
412
413 ctx = drawarea.window.cairo_create ()
414 ctx.set_line_width (1.5)
415
416 # run through the grid
417 for row in range (self.puzzle.rows):
418 for col in range (self.puzzle.cols):
419 # (re)set foreground color
420 ctx.set_source_rgb (0, 0, 0)
421 # if the area is not occupied
422 if (self.puzzle.data[row][col].occupied_across is False and
423 self.puzzle.data[row][col].occupied_down is False):
424 ctx.rectangle (col*30, row*30, 30, 30)
425 ctx.fill ()
426 else:
427 # if selected row/column
428 if row == self.selected_row and col == self.selected_col:
429 ctx.set_source_rgb (1, 1, 0)
430 ctx.rectangle (col*30,row*30, 30, 30)
431 ctx.fill ()
432 else:
433 ctx.set_source_rgb (1, 1, 1)
434 ctx.rectangle (col*30, row*30, 30, 30)
435 ctx.fill ()
436 ctx.set_source_rgb (0, 0, 0)
437 ctx.rectangle (col*30, row*30, 30, 30)
438 ctx.stroke ()
439
440 # if numbered
441 if self.puzzle.data[row][col].numbered <> 0:
442 ctx.set_source_rgb (0, 0, 0)
443 ctx.select_font_face ("Serif")
444 ctx.set_font_size (10)
445 ctx.move_to (col*30+2, row*30+10)
446 ctx.show_text (str(self.puzzle.data[row][col].numbered))
447
448 # if there is a guessed character at the location
449 # and it is not revealed as a solution
450 if (self.puzzle.data[row][col].guess and
451 self.puzzle.data[row][col].revealed is False and
452 (self.puzzle.data[row][col].occupied_across is True
453 or self.puzzle.data[row][col].occupied_down is True)):
454 ctx.set_source_rgb (0, 0, 0)
455 ctx.select_font_face ("Serif", cairo.FONT_SLANT_NORMAL,
456 cairo.FONT_WEIGHT_BOLD)
457 ctx.set_font_size (16)
458 ctx.move_to (col*30+10, row*30+20)
459 ctx.show_text (self.puzzle.data[row][col].guess)
460
461 # if there is a revealed solution character at the location
462 if (self.puzzle.data[row][col].revealed is True and
463 (self.puzzle.data[row][col].occupied_across is True
464 or self.puzzle.data[row][col].occupied_down is True)):
465 ctx.set_source_rgb (0, 0, 0.8)
466 ctx.select_font_face ("Serif", cairo.FONT_SLANT_NORMAL,
467 cairo.FONT_WEIGHT_BOLD)
468 ctx.set_font_size (16)
469 ctx.move_to (col*30+10, row*30+20)
470 ctx.show_text (self.puzzle.data[row][col].char)
471
472 return False
473
474 # load clues to the list
475 def load_clues (self):
476 # get the clues list store objects
477 across = self.ui.get_object ("clues_across")
478 down = self.ui.get_object ("clues_down")
479 across.clear ()
480 down.clear ()
481
482 # if puzzle is loaded
483 if self.puzzle:
484 clues_across = self.puzzle.get_clues_across ()
485 clues_down = self.puzzle.get_clues_down ()
486 # insert the numbers and the clues for across
487 for word, clue in clues_across:
488 across.append ([str(self.puzzle.data[word[1]][word[2]].numbered),
489 clue])
490 # insert the numbers and the clues for down
491 for word, clue in clues_down:
492 down.append ([ str(self.puzzle.data[word[1]][word[2]].numbered),
493 clue])
494
495 def save_file (self, file):
496 # try to save the file
497 try:
498 cPickle.dump (self.puzzle, open (file, "wb"), cPickle.HIGHEST_PROTOCOL)
499 except (IOError, OSError, cPickle.PicklingError):
500 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
501 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
502 "Error in saving puzzle")
503 dlg.run ()
504 dlg.destroy ()
505
506 # open a file
507 def open_file (self, file):
508 # try to open the file
509 try:
510 # load the puzzle
511 self.puzzle = cPickle.load (open (file, "rb"))
512 # assert that it is unfrozen otherwise raise frozen grid exception
513 self.puzzle.assert_frozen_grid ()
514
515 # set selected initial row and column to 0
516 self.selected_row = 0
517 self.selected_col = 0
518 # set the typing mode to default - across
519 self.typing_mode = self.ACROSS
520
521 self.window.set_title ("GetAClue player - " + file)
522 # load the clues
523 self.load_clues ()
524 # handle unpickling, and file errors
525 except (cPickle.UnpicklingError, IOError, OSError):
526 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
527 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
528 "Invalid file. Cannot be loaded")
529 dlg.run ()
530 dlg.destroy ()
531 # if the puzzle has no words, then it cannot be played obviously
532 except crosswordpuzzle.NoWordsException:
533 self.puzzle = None
534 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
535 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
536 "Word grid has no words. Cannot play")
537 dlg.run ()
538 dlg.destroy ()
539 # if the puzzle is not frozen then it cannot be played
540 except crosswordpuzzle.FrozenGridException:
541 self.puzzle = None
542 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
543 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
544 "Word grid is not finalized/frozen. Cannot play")
545 dlg.run ()
546 dlg.destroy ()
547
548 # about dialog
549 def about (self):
550 dlg = gtk.AboutDialog ()
551 dlg.set_name ("GetAClue Player")
552 dlg.set_copyright ("Copyright 2010 V.Harishankar")
553 dlg.set_website ("http://harishankar.org/software")
554 dlg.set_authors (("Harishankar",))
555 dlg.set_logo (self.window.get_icon())
556 dlg.set_license (self.LICENSE_TEXT)
557 dlg.set_comments ("Create and play Crossword puzzles")
558 dlg.run ()
559 dlg.destroy ()
560
561 def __init__ (self, file_to_play = None):
562 # load the user interface
563 self.ui = gtk.Builder ()
564
565 # Path for the interface file - change this if you are distributing
566 # the application and put the icon, interface file in a different
567 # location!!
568 gladepath = os.path.join (sys.path[0], "playerwindow.glade")
569 self.ui.add_from_file (gladepath)
570
571 # window object
572 self.window = self.ui.get_object ("mainwindow")
573 self.window.show ()
574
575 # set the cell renderer for tree views
576 cell = gtk.CellRendererText ()
577 tree_acol1 = self.ui.get_object ("tree_clues_across").get_column (0)
578 tree_acol2 = self.ui.get_object ("tree_clues_across").get_column (1)
579 tree_acol1.pack_start (cell)
580 tree_acol1.add_attribute (cell, "text", 0)
581 tree_acol2.pack_start (cell)
582 tree_acol2.add_attribute (cell, "text", 1)
583
584 tree_down = self.ui.get_object ("tree_clues_down")
585 tree_dcol1 = self.ui.get_object ("tree_clues_down").get_column (0)
586 tree_dcol2 = self.ui.get_object ("tree_clues_down").get_column (1)
587 tree_dcol1.pack_start (cell)
588 tree_dcol1.add_attribute (cell, "text", 0)
589 tree_dcol2.pack_start (cell)
590 tree_dcol2.add_attribute (cell, "text", 1)
591
592 # connect the signals
593 self.ui.connect_signals (self)
594
595 # set the puzzle to None
596 self.puzzle = None
597
598 # open the file if it is set
599 if file_to_play:
600 self.open_file (file_to_play)
601
602 gtk.main ()
603
604