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
15 import crosswordpuzzle
18 # typing mode constants
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.
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.
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/>."""
36 def verify_quit (self
):
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
:
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
))
53 if dlg
.run () == gtk
.RESPONSE_OK
:
54 puzzlefile
= dlg
.get_filename ()
55 self
.open_file (puzzlefile
)
59 # callback for menu item save as activated event
60 def on_save_as_activate (self
, menuitem
):
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
,
66 if dlg
.run () == gtk
.RESPONSE_OK
:
67 puzzlefile
= dlg
.get_filename ()
68 self
.save_file (puzzlefile
)
72 # callback for main window destroy
73 def on_mainwindow_destroy (self
, args
):
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
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
90 self
.window
.destroy ()
92 # callback for menu item clear grid activated event
93 def on_cleargrid_activate (self
, menuitem
):
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
:
100 self
.puzzle
.clear_guesses ()
102 puzgrid
= self
.ui
.get_object ("puzzlegrid")
103 puzgrid
.queue_draw ()
106 # callback for menu item verify board activated event
107 def on_verify_activate (self
, menuitem
):
110 ans
= self
.puzzle
.is_solution_correct ()
111 # if the solution is correct
113 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
114 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_CLOSE
,
115 "Success! Your entries are correct.")
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")
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")
129 # callback for menu item hide solution activated event
130 def on_hidesolution_activate (self
, menuitem
):
133 self
.puzzle
.reveal_solution (False)
134 puzgrid
= self
.ui
.get_object ("puzzlegrid")
136 puzgrid
.queue_draw ()
138 # callback for menu item reveal solution activated event
139 def on_revealsolution_activate (self
, menuitem
):
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 ()
149 puzgrid
= self
.ui
.get_object ("puzzlegrid")
150 puzgrid
.queue_draw ()
153 # callback for menu item reveal word activated event
154 def on_revealword_activate (self
, menuitem
):
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 ()
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 ()
179 # callback for menu help about activated event
180 def on_about_activate (self
, menu_item
):
181 # display the about dialog
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
)
190 # set the selected row and column
191 self
.selected_row
= row
192 self
.selected_col
= col
193 # set typing mode to across
195 self
.typing_mode
= self
.ACROSS
197 self
.typing_mode
= self
.DOWN
199 # update the puzzle grid
200 puzgrid
= self
.ui
.get_object ("puzzlegrid")
202 # set focus to the puzzle grid
203 self
.window
.set_focus (puzgrid
)
205 puzgrid
.queue_draw ()
207 # callback for tree view "across" being activated
208 # activated - when double clicked or enter pressed
209 def on_tree_clues_across_row_activated (self
, view
, path
, column
):
210 # get the across list object
211 across_list
= self
.ui
.get_object ("clues_across")
212 # get the number of the across word
213 anum
= int (across_list
.get_value (across_list
.get_iter (path
), 0))
215 self
.set_selection_of_num (anum
)
219 # callback for tree view "down" being activated
220 # activated - when double clicked or enter pressed
221 def on_tree_clues_down_row_activated (self
, view
, path
, column
):
222 # get the down list object
223 down_list
= self
.ui
.get_object ("clues_down")
224 # get the number of the down word
225 dnum
= int (down_list
.get_value (down_list
.get_iter (path
), 0))
227 self
.set_selection_of_num (dnum
, False)
229 # moving the current selection in grid by one up or down
230 def move_selection_updown (self
, step
):
231 # increase or reduce the row by step until an occupied grid is found
234 last_occupied_row
= self
.selected_row
236 self
.selected_row
+= step
237 if self
.selected_row
< 0 or self
.selected_row
>= self
.puzzle
.rows
:
238 self
.selected_row
= last_occupied_row
240 if (self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_across
is True
241 or self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_down
is True):
244 # moving the current selection in grid by one across either way
245 def move_selection_across (self
, step
):
246 # increase or reduce the row by step until an occupied grid is found
249 last_occupied_col
= self
.selected_col
251 self
.selected_col
+= step
252 if self
.selected_col
< 0 or self
.selected_col
>= self
.puzzle
.cols
:
253 self
.selected_col
= last_occupied_col
255 if (self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_across
is True
256 or self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].occupied_down
is True):
259 # set the guessed character in the grid at selected location and move the
260 # selection across or down as the case may be
261 def set_guess (self
, guess_char
):
263 # set a guess only if not revealed
264 if self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].revealed
is False:
265 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= guess_char
267 if self
.typing_mode
== self
.ACROSS
:
268 # move by one character across but only if there is no block
270 old_col
= self
.selected_col
271 self
.move_selection_across (1)
272 if abs (self
.selected_col
- old_col
) > 1:
273 self
.selected_col
= old_col
276 # move by one character down but only if there is no block
278 old_row
= self
.selected_row
279 self
.move_selection_updown (1)
280 if abs (self
.selected_row
- old_row
) > 1:
281 self
.selected_row
= old_row
283 # delete the guessed char in the previous row/col depending on the input mode
284 # If input mode is ACROSS then delete guessed char at previous column else
286 def delete_prev_guess (self
):
288 if self
.typing_mode
== self
.ACROSS
:
289 # prevent deleting characters when there is a gap
290 old_sel_col
= self
.selected_col
291 self
.move_selection_across (-1)
292 # only if there is no block inbetween delete
293 if abs (self
.selected_col
- old_sel_col
) <= 1:
294 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= None
297 self
.selected_col
= old_sel_col
298 elif self
.typing_mode
== self
.DOWN
:
299 # prevent deleting characters when there is a gap
300 old_sel_row
= self
.selected_row
301 self
.move_selection_updown (-1)
302 # only if there is no block inbetween delete
303 if abs (self
.selected_row
- old_sel_row
) <= 1:
304 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= None
307 self
.selected_row
= old_sel_row
309 # callback for puzzle grid mouse button release event
310 def on_puzzlegrid_button_press_event (self
, drawarea
, event
):
311 # set the focus on the puzzle grid
313 self
.window
.set_focus (drawarea
)
315 col
= int (event
.x
/ 30)
316 row
= int (event
.y
/ 30)
318 if col
< self
.puzzle
.cols
and row
< self
.puzzle
.rows
:
319 if (self
.puzzle
.data
[row
][col
].occupied_across
is True or
320 self
.puzzle
.data
[row
][col
].occupied_down
is True):
321 self
.selected_col
= col
322 self
.selected_row
= row
323 drawarea
.queue_draw ()
327 # callback for puzzle grid key release event
328 def on_puzzlegrid_key_press_event (self
, drawarea
, event
):
330 key
= gtk
.gdk
.keyval_name (event
.keyval
).lower ()
332 if event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "up":
333 # reduce the row by 1 until you find an occupied grid and not a
335 self
.move_selection_updown (-1)
336 self
.typing_mode
= self
.DOWN
337 drawarea
.queue_draw ()
338 elif event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "down":
339 # increase the row by 1 until you find an occupied grid and not a
341 self
.move_selection_updown (1)
342 self
.typing_mode
= self
.DOWN
343 drawarea
.queue_draw ()
344 elif event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "right":
345 # increase the column by 1 until you find an occupied grid and not
347 self
.move_selection_across (1)
348 self
.typing_mode
= self
.ACROSS
349 drawarea
.queue_draw ()
350 elif event
.state
== gtk
.gdk
.SHIFT_MASK
and key
== "left":
351 # decrease the column by 1 until you find an occupied grid and not
353 self
.move_selection_across (-1)
354 self
.typing_mode
= self
.ACROSS
355 drawarea
.queue_draw ()
356 # if it is A-Z or a-z then
357 elif len (key
) == 1 and key
.isalpha ():
358 guess_char
= key
.upper ()
359 self
.set_guess (guess_char
)
360 drawarea
.queue_draw ()
361 # if it is the delete key then delete character at selected row/col
362 elif key
== "delete":
363 self
.puzzle
.data
[self
.selected_row
][self
.selected_col
].guess
= None
364 drawarea
.queue_draw ()
365 # if the key is space key then delete character and move across or
366 # down one step depending on the mode
368 self
.set_guess (None)
369 drawarea
.queue_draw ()
370 # if it is backspace key then delete character at previous row/col
371 # depending on the input mode. If across editing mode, then delete
372 # at previous column else at previous row
373 elif key
== "backspace":
374 self
.delete_prev_guess ()
375 drawarea
.queue_draw ()
379 # puzzle grid focus in event
380 def on_puzzlegrid_focus_out_event (self
, drawarea
, event
):
382 col
= drawarea
.window
.get_colormap ().alloc_color (gtk
.gdk
.Color ("gray"))
383 drawarea
.window
.set_background (col
)
387 # puzzle grid focus out event
388 def on_puzzlegrid_focus_in_event (self
, drawarea
, event
):
390 col
= drawarea
.window
.get_colormap ().alloc_color (gtk
.gdk
.Color ("white"))
391 drawarea
.window
.set_background (col
)
394 # callback for drawing the puzzle grid
395 def on_puzzlegrid_expose_event (self
, drawarea
, event
):
396 # if puzzle is loaded
399 drawarea
.set_size_request (self
.puzzle
.cols
*30+2, self
.puzzle
.rows
*30+2)
401 ctx
= drawarea
.window
.cairo_create ()
402 ctx
.set_line_width (1.5)
404 # run through the grid
405 for row
in range (self
.puzzle
.rows
):
406 for col
in range (self
.puzzle
.cols
):
407 # (re)set foreground color
408 ctx
.set_source_rgb (0, 0, 0)
409 # if the area is not occupied
410 if (self
.puzzle
.data
[row
][col
].occupied_across
is False and
411 self
.puzzle
.data
[row
][col
].occupied_down
is False):
412 ctx
.rectangle (col
*30, row
*30, 30, 30)
415 # if selected row/column
416 if row
== self
.selected_row
and col
== self
.selected_col
:
417 ctx
.set_source_rgb (1, 1, 0)
418 ctx
.rectangle (col
*30,row
*30, 30, 30)
421 ctx
.set_source_rgb (1, 1, 1)
422 ctx
.rectangle (col
*30, row
*30, 30, 30)
424 ctx
.set_source_rgb (0, 0, 0)
425 ctx
.rectangle (col
*30, row
*30, 30, 30)
429 if self
.puzzle
.data
[row
][col
].numbered
<> 0:
430 ctx
.set_source_rgb (0, 0, 0)
431 ctx
.select_font_face ("Serif")
432 ctx
.set_font_size (10)
433 ctx
.move_to (col
*30+2, row
*30+10)
434 ctx
.show_text (str(self
.puzzle
.data
[row
][col
].numbered
))
436 # if there is a guessed character at the location
437 # and it is not revealed as a solution
438 if (self
.puzzle
.data
[row
][col
].guess
and
439 self
.puzzle
.data
[row
][col
].revealed
is False and
440 (self
.puzzle
.data
[row
][col
].occupied_across
is True
441 or self
.puzzle
.data
[row
][col
].occupied_down
is True)):
442 ctx
.set_source_rgb (0, 0, 0)
443 ctx
.select_font_face ("Serif", cairo
.FONT_SLANT_NORMAL
,
444 cairo
.FONT_WEIGHT_BOLD
)
445 ctx
.set_font_size (16)
446 ctx
.move_to (col
*30+10, row
*30+20)
447 ctx
.show_text (self
.puzzle
.data
[row
][col
].guess
)
449 # if there is a revealed solution character at the location
450 if (self
.puzzle
.data
[row
][col
].revealed
is True and
451 (self
.puzzle
.data
[row
][col
].occupied_across
is True
452 or self
.puzzle
.data
[row
][col
].occupied_down
is True)):
453 ctx
.set_source_rgb (0, 0, 0.8)
454 ctx
.select_font_face ("Serif", cairo
.FONT_SLANT_NORMAL
,
455 cairo
.FONT_WEIGHT_BOLD
)
456 ctx
.set_font_size (16)
457 ctx
.move_to (col
*30+10, row
*30+20)
458 ctx
.show_text (self
.puzzle
.data
[row
][col
].char
)
462 # load clues to the list
463 def load_clues (self
):
464 # get the clues list store objects
465 across
= self
.ui
.get_object ("clues_across")
466 down
= self
.ui
.get_object ("clues_down")
470 # if puzzle is loaded
472 clues_across
= self
.puzzle
.get_clues_across ()
473 clues_down
= self
.puzzle
.get_clues_down ()
474 # insert the numbers and the clues for across
475 for word
, clue
in clues_across
:
476 across
.append ([str(self
.puzzle
.data
[word
[1]][word
[2]].numbered
),
478 # insert the numbers and the clues for down
479 for word
, clue
in clues_down
:
480 down
.append ([ str(self
.puzzle
.data
[word
[1]][word
[2]].numbered
),
483 def save_file (self
, file):
484 # try to save the file
486 cPickle
.dump (self
.puzzle
, open (file, "wb"), cPickle
.HIGHEST_PROTOCOL
)
487 except (IOError, OSError, cPickle
.PicklingError
):
488 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
489 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
490 "Error in saving puzzle")
495 def open_file (self
, file):
496 # try to open the file
499 self
.puzzle
= cPickle
.load (open (file, "rb"))
500 # assert that it is unfrozen otherwise raise frozen grid exception
501 self
.puzzle
.assert_frozen_grid ()
503 # set selected initial row and column to 0
504 self
.selected_row
= 0
505 self
.selected_col
= 0
506 # set the typing mode to default - across
507 self
.typing_mode
= self
.ACROSS
509 self
.window
.set_title ("GetAClue player - " + file)
512 # handle unpickling, and file errors
513 except (cPickle
.UnpicklingError
, IOError, OSError):
514 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
515 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
516 "Invalid file. Cannot be loaded")
519 # if the puzzle has no words, then it cannot be played obviously
520 except crosswordpuzzle
.NoWordsException
:
522 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
523 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
524 "Word grid has no words. Cannot play")
527 # if the puzzle is not frozen then it cannot be played
528 except crosswordpuzzle
.FrozenGridException
:
530 dlg
= gtk
.MessageDialog (self
.window
, gtk
.DIALOG_MODAL
,
531 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_CLOSE
,
532 "Word grid is not finalized/frozen. Cannot play")
538 dlg
= gtk
.AboutDialog ()
539 dlg
.set_name ("GetAClue Player")
540 dlg
.set_copyright ("Copyright 2010 V.Harishankar")
541 dlg
.set_website ("http://harishankar.org/software")
542 dlg
.set_authors (("Harishankar",))
543 dlg
.set_logo (self
.window
.get_icon())
544 dlg
.set_license (self
.LICENSE_TEXT
)
545 dlg
.set_comments ("Create and play Crossword puzzles")
549 def __init__ (self
, file_to_play
= None):
550 # load the user interface
551 self
.ui
= gtk
.Builder ()
553 # Path for the interface file - change this if you are distributing
554 # the application and put the icon, interface file in a different
556 gladepath
= os
.path
.join (sys
.path
[0], "playerwindow.glade")
557 self
.ui
.add_from_file (gladepath
)
560 self
.window
= self
.ui
.get_object ("mainwindow")
563 # set the cell renderer for tree views
564 cell
= gtk
.CellRendererText ()
565 tree_acol1
= self
.ui
.get_object ("tree_clues_across").get_column (0)
566 tree_acol2
= self
.ui
.get_object ("tree_clues_across").get_column (1)
567 tree_acol1
.pack_start (cell
)
568 tree_acol1
.add_attribute (cell
, "text", 0)
569 tree_acol2
.pack_start (cell
)
570 tree_acol2
.add_attribute (cell
, "text", 1)
572 tree_down
= self
.ui
.get_object ("tree_clues_down")
573 tree_dcol1
= self
.ui
.get_object ("tree_clues_down").get_column (0)
574 tree_dcol2
= self
.ui
.get_object ("tree_clues_down").get_column (1)
575 tree_dcol1
.pack_start (cell
)
576 tree_dcol1
.add_attribute (cell
, "text", 0)
577 tree_dcol2
.pack_start (cell
)
578 tree_dcol2
.add_attribute (cell
, "text", 1)
580 # connect the signals
581 self
.ui
.connect_signals (self
)
583 # set the puzzle to None
586 # open the file if it is set
588 self
.open_file (file_to_play
)