1 # Get A Clue (C) 2010 V. Harishankar
2 # Crossword puzzle maker program
3 # Licensed under the GNU GPL v3
5 # Main window class for GetAClue player
13 import crosswordpuzzle
16 # typing mode constants
21 def verify_quit (self
):
23 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
24 gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
,
25 "Puzzle is open. Are you sure you wish to quit?")
26 if dlg
.run () <> gtk
.RESPONSE_YES
:
32 # callback for menu item open activated event
33 def on_open_activate (self
, menuitem
):
34 dlg
= gtk
.FileChooserDialog ("Open a GetAClue puzzle", self
.window
,
35 gtk
.FILE_CHOOSER_ACTION_OPEN
,
36 (gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
, gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
))
38 if dlg
.run () == gtk
.RESPONSE_OK
:
39 puzzlefile
= dlg
.get_filename ()
40 self
.open_file (puzzlefile
)
44 # callback for menu item save as activated event
45 def on_save_as_activate (self
, menuitem
):
47 dlg
= gtk
.FileChooserDialog ("Save GetAClue puzzle as", self
.window
,
48 gtk
.FILE_CHOOSER_ACTION_SAVE
,
49 (gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
, gtk
.STOCK_SAVE
,
51 if dlg
.run () == gtk
.RESPONSE_OK
:
52 puzzlefile
= dlg
.get_filename ()
53 self
.save_file (puzzlefile
)
57 # callback for main window destroy
58 def on_mainwindow_destroy (self
, args
):
62 # callback for window closing dialog
63 def on_mainwindow_delete_event (self
, window
, event
):
64 # verify whether really to quit or not if a puzzle is open
65 v
= self
.verify_quit ()
66 # return False for deleting and True for not deleting
69 # callback for menu item quit activated event
70 def on_quit_activate (self
, menuitem
):
71 # verify whether really to quit or not if a puzzle is open
72 v
= self
.verify_quit ()
73 # if verified, then quit
75 self
.window
.destroy ()
77 # callback for menu item clear grid activated event
78 def on_cleargrid_activate (self
, menuitem
):
80 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
81 gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
,
82 "Are you sure you wish to clear your entries?")
83 if dlg
.run () == gtk
.RESPONSE_YES
:
85 self
.puzzle
.clear_guesses ()
87 puzgrid
= self
.ui
.get_object ("puzzlegrid")
91 # callback for menu item verify board activated event
92 def on_verify_activate (self
, menuitem
):
95 ans
= self
.puzzle
.is_solution_correct ()
96 # if the solution is correct
98 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
99 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_CLOSE
,
100 "Success! Your entries are correct.")
103 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
104 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_CLOSE
,
105 "Your solution has some errors. Fix them and try again")
107 except crosswordpuzzle
.IncompleteSolutionException
:
108 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
109 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
110 "You've not completed the board yet. Cannot verify")
114 # callback for menu item hide solution activated event
115 def on_hidesolution_activate (self
, menuitem
):
118 self
.puzzle
.reveal_solution (False)
119 puzgrid
= self
.ui
.get_object ("puzzlegrid")
121 puzgrid
.queue_draw ()
123 # callback for menu item reveal solution activated event
124 def on_revealsolution_activate (self
, menuitem
):
127 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
128 gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
,
129 "This will reveal all words in the puzzle! Are you sure?")
130 if dlg
.run () == gtk
.RESPONSE_YES
:
131 # reveal the solution
132 self
.puzzle
.reveal_solution ()
134 puzgrid
= self
.ui
.get_object ("puzzlegrid")
135 puzgrid
.queue_draw ()
138 # callback for menu item reveal word activated event
139 def on_revealword_activate (self
, menuitem
):
141 # reveal across/down word if any the position
142 if self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_across
is True:
143 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
144 gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
,
145 "Are you sure you wish to reveal across word at current cell?")
146 # confirm that the user wants to reveal
147 if dlg
.run () == gtk
.RESPONSE_YES
:
148 self
.puzzle
.reveal_word_across (self
.selected_row
, self
.selected_col
)
149 # redraw the grid to reveal the word
150 puzgrid
= self
.ui
.get_object ("puzzlegrid")
151 puzgrid
.queue_draw ()
153 if self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_down
is True:
154 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
155 gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_YES_NO
,
156 "Are you sure wish to reveal down word at current cell?")
157 if dlg
.run () == gtk
.RESPONSE_YES
:
158 self
.puzzle
.reveal_word_down (self
.selected_row
, self
.selected_col
)
159 # redraw the grid to reveal the word
160 puzgrid
= self
.ui
.get_object ("puzzlegrid")
161 puzgrid
.queue_draw ()
165 # function to set the selected row/col based on the number clicked
166 # on the clues list and also set the typing mode
167 def set_selection_of_num (self
, num
, across
= True):
168 # get the row, col of the word
169 row
, col
= self
.puzzle
.get_position_of_num (num
)
171 # set the selected row and column
172 self
.selected_row
= row
173 self
.selected_col
= col
174 # set typing mode to across
176 self
.typing_mode
= self
.ACROSS
178 self
.typing_mode
= self
.DOWN
180 # update the puzzle grid
181 puzgrid
= self
.ui
.get_object ("puzzlegrid")
183 # set focus to the puzzle grid
184 self
.window
.set_focus (puzgrid
)
186 puzgrid
.queue_draw ()
188 # callback for tree view "across" being activated
189 # activated - when double clicked or enter pressed
190 def on_tree_clues_across_row_activated (self
, view
, path
, column
):
191 # get the across list object
192 across_list
= self
.ui
.get_object ("clues_across")
193 # get the number of the across word
194 anum
= int (across_list
.get_value (across_list
.get_iter (path
), 0))
196 self
.set_selection_of_num (anum
)
200 # callback for tree view "down" being activated
201 # activated - when double clicked or enter pressed
202 def on_tree_clues_down_row_activated (self
, view
, path
, column
):
203 # get the down list object
204 down_list
= self
.ui
.get_object ("clues_down")
205 # get the number of the down word
206 dnum
= int (down_list
.get_value (down_list
.get_iter (path
), 0))
208 self
.set_selection_of_num (dnum
, False)
210 # moving the current selection in grid by one up or down
211 def move_selection_updown (self
, step
):
212 # increase or reduce the row by step until an occupied grid is found
215 last_occupied_row
= self
.selected_row
217 self
.selected_row
+= step
218 if self
.selected_row
< 0 or self
.selected_row
>= self
.puzzle
.rows
:
219 self
.selected_row
= last_occupied_row
221 if (self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_across
is True
222 or self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_down
is True):
225 # moving the current selection in grid by one across either way
226 def move_selection_across (self
, step
):
227 # increase or reduce the row by step until an occupied grid is found
230 last_occupied_col
= self
.selected_col
232 self
.selected_col
+= step
233 if self
.selected_col
< 0 or self
.selected_col
>= self
.puzzle
.cols
:
234 self
.selected_col
= last_occupied_col
236 if (self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_across
is True
237 or self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_down
is True):
240 # set the guessed character in the grid at selected location and move the
241 # selection across or down as the case may be
242 def set_guess (self
, guess_char
):
244 # set a guess only if not revealed
245 if self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].revealed
is False:
246 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= guess_char
.upper ()
248 if self
.typing_mode
== self
.ACROSS
:
249 # move by one character across but only if there is no block
251 old_col
= self
.selected_col
252 self
.move_selection_across (1)
253 if abs (self
.selected_col
- old_col
) > 1:
254 self
.selected_col
= old_col
257 # move by one character down but only if there is no block
259 old_row
= self
.selected_row
260 self
.move_selection_updown (1)
261 if abs (self
.selected_row
- old_row
) > 1:
262 self
.selected_row
= old_row
264 # delete the guessed char in the previous row/col depending on the input mode
265 # If input mode is ACROSS then delete guessed char at previous column else
267 def delete_prev_guess (self
):
269 if self
.typing_mode
== self
.ACROSS
:
270 # prevent deleting characters when there is a gap
271 old_sel_col
= self
.selected_col
272 self
.move_selection_across (-1)
273 # only if there is no block inbetween delete
274 if abs (self
.selected_col
- old_sel_col
) <= 1:
275 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= None
278 self
.selected_col
= old_sel_col
279 elif self
.typing_mode
== self
.DOWN
:
280 # prevent deleting characters when there is a gap
281 old_sel_row
= self
.selected_row
282 self
.move_selection_updown (-1)
283 # only if there is no block inbetween delete
284 if abs (self
.selected_row
- old_sel_row
) <= 1:
285 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= None
288 self
.selected_row
= old_sel_row
290 # callback for puzzle grid mouse button release event
291 def on_puzzlegrid_button_press_event (self
, drawarea
, event
):
292 # set the focus on the puzzle grid
294 self
.window
.set_focus (drawarea
)
296 col
= int (event
.x
/ 30)
297 row
= int (event
.y
/ 30)
299 if col
< self
.puzzle
.cols
and row
< self
.puzzle
.rows
:
300 if (self
.puzzle
.data
[row
][col
].occupied_across
is True or
301 self
.puzzle
.data
[row
][col
].occupied_down
is True):
302 self
.selected_col
= col
303 self
.selected_row
= row
304 drawarea
.queue_draw ()
308 # callback for puzzle grid key release event
309 def on_puzzlegrid_key_press_event (self
, drawarea
, event
):
311 key
= gtk
.gdk
.keyval_name (event
.keyval
).lower ()
313 if event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "up":
314 # reduce the row by 1 until you find an occupied grid and not a
316 self
.move_selection_updown (-1)
317 self
.typing_mode
= self
.DOWN
318 drawarea
.queue_draw ()
319 elif event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "down":
320 # increase the row by 1 until you find an occupied grid and not a
322 self
.move_selection_updown (1)
323 self
.typing_mode
= self
.DOWN
324 drawarea
.queue_draw ()
325 elif event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "right":
326 # increase the column by 1 until you find an occupied grid and not
328 self
.move_selection_across (1)
329 self
.typing_mode
= self
.ACROSS
330 drawarea
.queue_draw ()
331 elif event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "left":
332 # decrease the column by 1 until you find an occupied grid and not
334 self
.move_selection_across (-1)
335 self
.typing_mode
= self
.ACROSS
336 drawarea
.queue_draw ()
337 # if it is A-Z or a-z then
338 elif len (key
) == 1 and key
.isalpha ():
340 drawarea
.queue_draw ()
341 # if it is the delete key then delete character at selected row/col
342 elif key
== "delete" or key
== "space":
343 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= None
344 drawarea
.queue_draw ()
345 # if it is backspace key then delete character at previous row/col
346 # depending on the input mode. If across editing mode, then delete
347 # at previous column else at previous row
348 elif key
== "backspace":
349 self
.delete_prev_guess ()
350 drawarea
.queue_draw ()
354 # puzzle grid focus in event
355 def on_puzzlegrid_focus_out_event (self
, drawarea
, event
):
357 col
= drawarea
.window
.get_colormap ().alloc_color (gtk
.gdk
.Color ("gray"))
358 drawarea
.window
.set_background (col
)
362 # puzzle grid focus out event
363 def on_puzzlegrid_focus_in_event (self
, drawarea
, event
):
365 col
= drawarea
.window
.get_colormap ().alloc_color (gtk
.gdk
.Color ("white"))
366 drawarea
.window
.set_background (col
)
369 # callback for drawing the puzzle grid
370 def on_puzzlegrid_expose_event (self
, drawarea
, event
):
371 # if puzzle is loaded
374 drawarea
.set_size_request (self
.puzzle
.cols
*30+2, self
.puzzle
.rows
*30+2)
376 ctx
= drawarea
.window
.cairo_create ()
377 ctx
.set_line_width (1.5)
379 # run through the grid
380 for row
in range (self
.puzzle
.rows
):
381 for col
in range (self
.puzzle
.cols
):
382 # (re)set foreground color
383 ctx
.set_source_rgb (0, 0, 0)
384 # if the area is not occupied
385 if (self
.puzzle
.data
[row
][col
].occupied_across
is False and
386 self
.puzzle
.data
[row
][col
].occupied_down
is False):
387 ctx
.rectangle (col
*30, row
*30, 30, 30)
390 # if selected row/column
391 if row
== self
.selected_row
and col
== self
.selected_col
:
392 ctx
.set_source_rgb (1, 1, 0)
393 ctx
.rectangle (col
*30,row
*30, 30, 30)
396 ctx
.set_source_rgb (1, 1, 1)
397 ctx
.rectangle (col
*30, row
*30, 30, 30)
399 ctx
.set_source_rgb (0, 0, 0)
400 ctx
.rectangle (col
*30, row
*30, 30, 30)
404 if self
.puzzle
.data
[row
][col
].numbered
<> 0:
405 ctx
.set_source_rgb (0, 0, 0)
406 ctx
.select_font_face ("Serif")
407 ctx
.set_font_size (10)
408 ctx
.move_to (col
*30+2, row
*30+10)
409 ctx
.show_text (str(self
.puzzle
.data
[row
][col
].numbered
))
411 # if there is a guessed character at the location
412 # and it is not revealed as a solution
413 if (self
.puzzle
.data
[row
][col
].guess
and
414 self
.puzzle
.data
[row
][col
].revealed
is False and
415 (self
.puzzle
.data
[row
][col
].occupied_across
is True
416 or self
.puzzle
.data
[row
][col
].occupied_down
is True)):
417 ctx
.set_source_rgb (0, 0, 0)
418 ctx
.select_font_face ("Serif", cairo
.FONT_SLANT_NORMAL
,
419 cairo
.FONT_WEIGHT_BOLD
)
420 ctx
.set_font_size (16)
421 ctx
.move_to (col
*30+10, row
*30+20)
422 ctx
.show_text (self
.puzzle
.data
[row
][col
].guess
)
424 # if there is a revealed solution character at the location
425 if (self
.puzzle
.data
[row
][col
].revealed
is True and
426 (self
.puzzle
.data
[row
][col
].occupied_across
is True
427 or self
.puzzle
.data
[row
][col
].occupied_down
is True)):
428 ctx
.set_source_rgb (0, 0, 0.8)
429 ctx
.select_font_face ("Serif", cairo
.FONT_SLANT_NORMAL
,
430 cairo
.FONT_WEIGHT_BOLD
)
431 ctx
.set_font_size (16)
432 ctx
.move_to (col
*30+10, row
*30+20)
433 ctx
.show_text (self
.puzzle
.data
[row
][col
].char
)
437 # load clues to the list
438 def load_clues (self
):
439 # get the clues list store objects
440 across
= self
.ui
.get_object ("clues_across")
441 down
= self
.ui
.get_object ("clues_down")
445 # if puzzle is loaded
447 clues_across
= self
.puzzle
.get_clues_across ()
448 clues_down
= self
.puzzle
.get_clues_down ()
449 # insert the numbers and the clues for across
450 for word
, clue
in clues_across
:
451 across
.append ([str(self
.puzzle
.data
[word
[1]][word
[2]].numbered
),
453 # insert the numbers and the clues for down
454 for word
, clue
in clues_down
:
455 down
.append ([ str(self
.puzzle
.data
[word
[1]][word
[2]].numbered
),
458 def save_file (self
, file):
459 # try to save the file
461 cPickle
.dump (self
.puzzle
, open (file, "wb"), cPickle
.HIGHEST_PROTOCOL
)
462 except (IOError, OSError, cPickle
.PicklingError
):
463 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
464 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
465 "Error in saving puzzle")
470 def open_file (self
, file):
471 # try to open the file
474 self
.puzzle
= cPickle
.load (open (file, "rb"))
475 # assert that it is unfrozen otherwise raise frozen grid exception
476 self
.puzzle
.assert_frozen_grid ()
478 # set selected initial row and column to 0
479 self
.selected_row
= 0
480 self
.selected_col
= 0
481 # set the typing mode to default - across
482 self
.typing_mode
= self
.ACROSS
484 self
.window
.set_title ("GetAClue player - " + file)
487 # handle unpickling, and file errors
488 except (cPickle
.UnpicklingError
, IOError, OSError):
489 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
490 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
491 "Invalid file. Cannot be loaded")
494 # if the puzzle has no words, then it cannot be played obviously
495 except crosswordpuzzle
.NoWordsException
:
497 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
498 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
499 "Word grid has no words. Cannot play")
502 # if the puzzle is not frozen then it cannot be played
503 except crosswordpuzzle
.FrozenGridException
:
505 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
506 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
507 "Word grid is not finalized/frozen. Cannot play")
511 def __init__ (self
, file_to_play
= None):
512 # load the user interface
513 self
.ui
= gtk
.Builder ()
514 self
.ui
.add_from_file ("playerwindow.glade")
517 self
.window
= self
.ui
.get_object ("mainwindow")
520 # set the cell renderer for tree views
521 cell
= gtk
.CellRendererText ()
522 tree_acol1
= self
.ui
.get_object ("tree_clues_across").get_column (0)
523 tree_acol2
= self
.ui
.get_object ("tree_clues_across").get_column (1)
524 tree_acol1
.pack_start (cell
)
525 tree_acol1
.add_attribute (cell
, "text", 0)
526 tree_acol2
.pack_start (cell
)
527 tree_acol2
.add_attribute (cell
, "text", 1)
529 tree_down
= self
.ui
.get_object ("tree_clues_down")
530 tree_dcol1
= self
.ui
.get_object ("tree_clues_down").get_column (0)
531 tree_dcol2
= self
.ui
.get_object ("tree_clues_down").get_column (1)
532 tree_dcol1
.pack_start (cell
)
533 tree_dcol1
.add_attribute (cell
, "text", 0)
534 tree_dcol2
.pack_start (cell
)
535 tree_dcol2
.add_attribute (cell
, "text", 1)
537 # connect the signals
538 self
.ui
.connect_signals (self
)
540 # set the puzzle to None
543 # open the file if it is set
545 self
.open_file (file_to_play
)