8234893d0a6923de42a5e4cf55bb6ba377286cbf
[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 # quit verification
21 def verify_quit (self):
22 if self.puzzle:
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:
27 dlg.destroy ()
28 return False
29 dlg.destroy ()
30 return True
31
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))
37
38 if dlg.run () == gtk.RESPONSE_OK:
39 puzzlefile = dlg.get_filename ()
40 self.open_file (puzzlefile)
41
42 dlg.destroy ()
43
44 # callback for menu item save as activated event
45 def on_save_as_activate (self, menuitem):
46 if self.puzzle:
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,
50 gtk.RESPONSE_OK))
51 if dlg.run () == gtk.RESPONSE_OK:
52 puzzlefile = dlg.get_filename ()
53 self.save_file (puzzlefile)
54
55 dlg.destroy ()
56
57 # callback for main window destroy
58 def on_mainwindow_destroy (self, args):
59 gtk.main_quit ()
60
61
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
67 return not v
68
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
74 if v is True:
75 self.window.destroy ()
76
77 # callback for menu item clear grid activated event
78 def on_cleargrid_activate (self, menuitem):
79 if self.puzzle:
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:
84 # clear the guesses
85 self.puzzle.clear_guesses ()
86 # redraw the grid
87 puzgrid = self.ui.get_object ("puzzlegrid")
88 puzgrid.queue_draw ()
89 dlg.destroy()
90
91 # callback for menu item verify board activated event
92 def on_verify_activate (self, menuitem):
93 if self.puzzle:
94 try:
95 ans = self.puzzle.is_solution_correct ()
96 # if the solution is correct
97 if ans:
98 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
99 gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE,
100 "Success! Your entries are correct.")
101 dlg.run ()
102 else:
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")
106 dlg.run ()
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")
111 dlg.run ()
112 dlg.destroy ()
113
114 # callback for menu item hide solution activated event
115 def on_hidesolution_activate (self, menuitem):
116 if self.puzzle:
117 # hide the solution
118 self.puzzle.reveal_solution (False)
119 puzgrid = self.ui.get_object ("puzzlegrid")
120 # redraw the grid
121 puzgrid.queue_draw ()
122
123 # callback for menu item reveal solution activated event
124 def on_revealsolution_activate (self, menuitem):
125 if self.puzzle:
126 # confirm first
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 ()
133 # redraw the grid
134 puzgrid = self.ui.get_object ("puzzlegrid")
135 puzgrid.queue_draw ()
136 dlg.destroy ()
137
138 # callback for menu item reveal word activated event
139 def on_revealword_activate (self, menuitem):
140 if self.puzzle:
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 ()
152 dlg.destroy ()
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 ()
162 dlg.destroy ()
163
164
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)
170
171 # set the selected row and column
172 self.selected_row = row
173 self.selected_col = col
174 # set typing mode to across
175 if across is True:
176 self.typing_mode = self.ACROSS
177 else:
178 self.typing_mode = self.DOWN
179
180 # update the puzzle grid
181 puzgrid = self.ui.get_object ("puzzlegrid")
182
183 # set focus to the puzzle grid
184 self.window.set_focus (puzgrid)
185
186 puzgrid.queue_draw ()
187
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))
195
196 self.set_selection_of_num (anum)
197
198 return False
199
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))
207
208 self.set_selection_of_num (dnum, False)
209
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
213 # black block
214 if self.puzzle:
215 last_occupied_row = self.selected_row
216 while True:
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
220 break
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):
223 break
224
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
228 # black block
229 if self.puzzle:
230 last_occupied_col = self.selected_col
231 while True:
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
235 break
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):
238 break
239
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):
243 if self.puzzle:
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 ()
247 # across mode typing
248 if self.typing_mode == self.ACROSS:
249 # move by one character across but only if there is no block
250 # in between
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
255 # down mode typing
256 else:
257 # move by one character down but only if there is no block
258 # in between
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
263
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
266 # at previous row
267 def delete_prev_guess (self):
268 if self.puzzle:
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
276 # reset selection
277 else:
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
286 # reset selection
287 else:
288 self.selected_row = old_sel_row
289
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
293 if self.puzzle:
294 self.window.set_focus (drawarea)
295
296 col = int (event.x / 30)
297 row = int (event.y / 30)
298
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 ()
305
306 return False
307
308 # callback for puzzle grid key release event
309 def on_puzzlegrid_key_press_event (self, drawarea, event):
310 if self.puzzle:
311 key = gtk.gdk.keyval_name (event.keyval).lower ()
312
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
315 # black block
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
321 # black block
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
327 # a black block
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
333 # a black block
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 ():
339 self.set_guess (key)
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 ()
351
352 return False
353
354 # puzzle grid focus in event
355 def on_puzzlegrid_focus_out_event (self, drawarea, event):
356 if self.puzzle:
357 col = drawarea.window.get_colormap ().alloc_color (gtk.gdk.Color ("gray"))
358 drawarea.window.set_background (col)
359
360 return False
361
362 # puzzle grid focus out event
363 def on_puzzlegrid_focus_in_event (self, drawarea, event):
364 if self.puzzle:
365 col = drawarea.window.get_colormap ().alloc_color (gtk.gdk.Color ("white"))
366 drawarea.window.set_background (col)
367 return False
368
369 # callback for drawing the puzzle grid
370 def on_puzzlegrid_expose_event (self, drawarea, event):
371 # if puzzle is loaded
372 if self.puzzle:
373 # size the area
374 drawarea.set_size_request (self.puzzle.cols*30+2, self.puzzle.rows*30+2)
375
376 ctx = drawarea.window.cairo_create ()
377 ctx.set_line_width (1.5)
378
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)
388 ctx.fill ()
389 else:
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)
394 ctx.fill ()
395 else:
396 ctx.set_source_rgb (1, 1, 1)
397 ctx.rectangle (col*30, row*30, 30, 30)
398 ctx.fill ()
399 ctx.set_source_rgb (0, 0, 0)
400 ctx.rectangle (col*30, row*30, 30, 30)
401 ctx.stroke ()
402
403 # if numbered
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))
410
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)
423
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)
434
435 return False
436
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")
442 across.clear ()
443 down.clear ()
444
445 # if puzzle is loaded
446 if self.puzzle:
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),
452 clue])
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),
456 clue])
457
458 def save_file (self, file):
459 # try to save the file
460 try:
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")
466 dlg.run ()
467 dlg.destroy ()
468
469 # open a file
470 def open_file (self, file):
471 # try to open the file
472 try:
473 # load the puzzle
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 ()
477
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
483
484 self.window.set_title ("GetAClue player - " + file)
485 # load the clues
486 self.load_clues ()
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")
492 dlg.run ()
493 dlg.destroy ()
494 # if the puzzle has no words, then it cannot be played obviously
495 except crosswordpuzzle.NoWordsException:
496 self.puzzle = None
497 dlg = gtk.MessageDialog (self.window, gtk.DIALOG_MODAL,
498 gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
499 "Word grid has no words. Cannot play")
500 dlg.run ()
501 dlg.destroy ()
502 # if the puzzle is not frozen then it cannot be played
503 except crosswordpuzzle.FrozenGridException:
504 self.puzzle = None
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")
508 dlg.run ()
509 dlg.destroy ()
510
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")
515
516 # window object
517 self.window = self.ui.get_object ("mainwindow")
518 self.window.show ()
519
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)
528
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)
536
537 # connect the signals
538 self.ui.connect_signals (self)
539
540 # set the puzzle to None
541 self.puzzle = None
542
543 # open the file if it is set
544 if file_to_play:
545 self.open_file (file_to_play)
546
547 gtk.main ()
548
549