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