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