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