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