root/04/bugfix/v.py

Revision 21, 33.5 kB (checked in by ug, 5 years ago)

Import release 0.4d

  • Property svn:executable set to
Line 
1 #!/usr/bin/python
2 # File: v.py
3
4 ##   Copyright (C) 2001-2 Ulrich Goertz (u@g0ertz.de)
5
6 ##   This is a simple SGF viewer; it comes with the go database program
7 ##   Kombilo.
8
9 ##   This program is free software; you can redistribute it and/or modify
10 ##   it under the terms of the GNU General Public License as published by
11 ##   the Free Software Foundation; either version 2 of the License, or
12 ##   (at your option) any later version.
13
14 ##   This program is distributed in the hope that it will be useful,
15 ##   but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ##   GNU General Public License for more details.
18
19 ##   You should have received a copy of the GNU General Public License
20 ##   along with this program (gpl.txt); if not, write to the Free Software
21 ##   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22 ##   The GNU GPL is also currently available at
23 ##   http://www.gnu.org/copyleft/gpl.html
24
25
26 from Tkinter import *
27 from tkMessageBox import *
28 from ScrolledText import ScrolledText
29 import tkFileDialog
30 import cPickle
31 import os
32 import sys
33 from string import split, find, replace, join, strip
34 from copy import copy, deepcopy
35
36 from sgfparser import *           
37 from board1 import *
38
39 # ---------------------------------------------------------------------------------------
40
41 class BunchTkVar:
42     """ This class is used to collect the Tk variables where the options
43         are stored. """
44    
45     def saveToDisk(self, filename, onFailure = lambda:None):
46         d = {}
47         for x in self.__dict__.keys():
48             d[x] = self.__dict__[x].get()
49         try:
50             f = open(filename, 'w')
51             cPickle.dump(d,f)
52             f.close()
53         except IOError:
54             onFailure()
55            
56
57     def loadFromDisk(self, filename, onFailure = lambda:None):
58         try:
59             f = open(filename)
60             d = cPickle.load(f)
61             f.close()
62         except IOError:
63             onFailure()
64         else:
65             for x in self.__dict__.keys():
66                 if d.has_key(x): self.__dict__[x].set(d[x])
67
68
69
70 # ---------------------------------------------------------------------------------------
71
72 class EnhancedCursor(Cursor):
73     """ Adds a snapshot/restore feature to Cursor. """
74    
75     def __init__(self, sgf, comments):
76         self.comments = comments
77         Cursor.__init__(self, sgf)
78        
79     def snapshot(self):
80         """ Returns the current position in the sgf file (as some tuple);
81             with this tuple, the position can be restored via 'restore'.
82             This is used for the 'back' button."""
83
84         comment = self.comments.get('1.0', END)
85
86         return (copy(self.currentPosition),
87                 copy(self.oldIndices), comment)
88
89     def restore(self, d):
90         """ Restore the position given by d (cf. snapshot). """
91        
92         self.currentPosition = d[0]
93         self.oldIndices = d[1]
94         self.currentNode = self.parseNode(self.currentPosition)
95         self.setFlags()
96
97         self.comments.delete('1.0', END)
98         self.comments.insert('1.0', d[2])
99    
100 # ---------------------------------------------------------------------------------------
101
102 class ScrolledText_HSB(ScrolledText):
103     """ A ScrolledText which hides the scroll bar when it is not needed. """
104
105     def __init__(self, parent, **args):
106         if not args:
107             args = {}
108         apply(ScrolledText.__init__, (self, parent,) , args)
109         self.vbarPackinfo = self.vbar.pack_info()
110         self.vbar.pack_forget()
111
112     def insert(self, pos, text):
113         ScrolledText.insert(self, pos, text)
114         if self.yview() != (0.0, 1.0):
115             apply(self.vbar.pack, (), self.vbarPackinfo)
116
117     def delete(self, pos1, pos2):
118         ScrolledText.delete(self, pos1, pos2)
119         if self.yview() == (0.0, 1.0):
120             self.vbar.pack_forget()
121
122 # ---------------------------------------------------------------------------------------
123
124 class Viewer:
125     """ This is the main class of v.py. """
126
127     def convCoord(self, x):
128         """ This takes coordinates in SGF style (aa - ss),
129             and returns the corresponding
130             integer coordinates (between 1 and 19). """
131
132         try:
133             p, q =  ord(x[0])-ord('a')+1, ord(x[1])-ord('a')+1
134             if 1 <= p <= 19 and 1 <= q <= 19: return (p,q)
135             else: return 0
136         except:
137             return 0
138        
139
140     def setup(self):
141         """ Set up initial position (clear board, put handi stones etc.). """
142
143         self.board.clear()
144         self.cursor.game(0)
145
146         # display game name
147         gameName = self.currentFile[:15]
148         self.gameName.set(gameName)
149
150         self.moveno.set('0')
151         self.capW = 0
152         self.capB = 0
153         self.capVar.set('Cap - B: ' + str(self.capB) + ', W: ' + str(self.capW))
154
155         if self.cursor.currentNode.has_key('PW'): pw = self.cursor.currentNode['PW'][0]
156         else: pw = ''
157         if self.cursor.currentNode.has_key('PB'): pb = self.cursor.currentNode['PB'][0]
158         else: pb = ''
159         self.master.title(pw + ' - ' + pb)
160
161         try:
162        
163             # look for first relevant node
164             while not (self.cursor.currentNode.has_key('AB') or self.cursor.currentNode.has_key('AW') \
165                        or self.cursor.currentNode.has_key('B') or self.cursor.currentNode.has_key('W')):
166                 self.cursor.next()
167
168             # and put stones on the board
169             while not (self.cursor.currentNode.has_key('B') or self.cursor.currentNode.has_key('W')):
170                 if self.cursor.currentNode.has_key('AB'):
171                     for x in self.cursor.currentNode['AB']:
172                         self.board.play(self.convCoord(x), 'black')
173                 if self.cursor.currentNode.has_key('AW'):
174                     for x in self.cursor.currentNode['AW']:
175                         self.board.play(self.convCoord(x), 'white')
176                 self.cursor.next()
177        
178             if not self.cursor.atStart:
179                 if self.cursor.currentNode.has_key('B'):   self.board.currentColor = 'black'
180                 elif self.cursor.currentNode.has_key('W'): self.board.currentColor = 'white'
181                 self.cursor.previous()
182             else:
183                 if self.cursor.currentNode.has_key('B'):
184                     self.board.play(self.convCoord(self.cursor.currentNode['B'][0]), 'black')
185                 elif self.cursor.currentNode.has_key('W'):
186                     self.board.play(self.convCoord(self.cursor.currentNode['B'][0]), 'white')
187
188         except SGFError:
189             showwarning('SGF Error', 'SGF Error')
190             self.gameName.set('')
191             self.currentFile = ''
192             self.cursor = None
193             return 0
194        
195         self.displayLabels(self.cursor.currentNode)
196         self.markAll()
197         return 1
198
199
200     def markAll(self):
201         """ Mark all variations for the next move. """
202
203         if not self.cursor or self.cursor.atEnd: return
204        
205         passV = 0
206
207         try:
208        
209             for i in range(self.cursor.noChildren()):
210                 c = self.cursor.next(i)
211
212                 for color in ['B', 'W']:
213                     if c.has_key(color):
214                         if c[color][0] and self.convCoord(c[color][0]):
215                             if self.options.showNextMoveVar.get():
216                                 self.board.placeMark(self.convCoord(c[color][0]),'')
217                         else:
218                             passV = 1
219                         if color == 'B': self.board.currentColor = 'black'
220                         else: self.board.currentColor = 'white'
221
222                 self.cursor.previous()
223
224             if passV: self.passButton.config(state=NORMAL)
225             else: self.passButton.config(state=DISABLED)
226
227         except SGFError:
228             showwarning('SGF Error', 'SGF Error')
229             self.board.delMarks()
230
231    
232     def showNextMove(self):
233         """ Toggle 'show next move' option. """
234         if self.options.showNextMoveVar.get():
235             self.markAll()
236         else:
237             self.board.delMarks()
238
239     def passFct(self):
240         """ React to pass button: choose the 'pass variation' in the SGF file."""
241        
242         if not self.currentFile: return 0
243
244         self.leaveNode()
245
246         if self.board.currentColor == 'black': nM = 'B'
247         else:                                  nM = 'W'
248
249         for i in range(self.cursor.noChildren()):             
250             try:
251                 c = self.cursor.next(i)
252             except SGFError:
253                 continue
254             if c.has_key(nM) and not self.convCoord(c[nM][0]):  # found
255                 self.board.delMarks()
256                 self.board.delLabels()
257                 self.moveno.set(str(int(self.moveno.get())+1))
258                
259                 self.markAll()
260                 self.displayNode(c)
261                 return 1
262             else:
263                 self.cursor.previous()
264         return 0
265                
266
267     def nextMove(self, p):
268         """ React to mouse-click on the board"""
269
270         if not self.currentFile: return 0
271
272         self.leaveNode()
273
274         x, y = p
275
276         if self.board.currentColor == 'black':
277             nM = 'B'
278         else:
279             nM = 'W'
280
281         done = 0
282         for i in range(self.cursor.noChildren()):             # look for the move in the SGF file
283             if (not done):
284                 try:
285                     c = self.cursor.next(i)
286                 except SGFError:
287                     continue
288                 if c.has_key(nM) and self.convCoord(c[nM][0])==p:  # found
289                     self.board.delMarks()
290                     self.board.delLabels()
291                     done = 1
292                    
293                     self.moveno.set(str(int(self.moveno.get())+1))
294
295                     self.markAll()
296                     self.displayNode(c)
297                     if c.has_key('B'):
298                         self.capB = self.capB + len(self.board.undostack[len(self.board.undostack)-1][2])
299                     if c.has_key('W'):
300                         self.capW = self.capW + len(self.board.undostack[len(self.board.undostack)-1][2])
301                     self.capVar.set('Cap - B: ' + str(self.capB) + ', W: ' + str(self.capW))
302
303                 else:
304                     self.cursor.previous()
305                    
306         return done
307
308
309     def prev(self):
310         """ Go back one move. """
311
312         if not self.currentFile: return
313
314         if not self.cursor.atStart:
315             self.leaveNode()
316             c = self.cursor.currentNode
317             if (c.has_key('B') and c['B'][0]) or (c.has_key('W') and c['W'][0]):
318                 if c.has_key('B'):
319                     self.capB = self.capB - len(self.board.undostack[len(self.board.undostack)-1][2])
320                 if c.has_key('W'):
321                     self.capW = self.capW - len(self.board.undostack[len(self.board.undostack)-1][2])
322                 self.capVar.set('Cap - B: ' + str(self.capB) + ', W: ' + str(self.capW))
323
324                 self.board.undo()
325
326             if c.has_key('AE') and c['AE'][0]:
327                 for p in c['AE']:
328                     self.board.undo(1, 0)
329             if c.has_key('AW') and c['AW'][0]:
330                 for p in c['AW']:
331                     self.board.undo(1, 0)
332             if c.has_key('AB') and c['AB'][0]:
333                 for p in c['AB']:
334                     self.board.undo(1, 0)
335
336             c = self.cursor.previous()
337             self.moveno.set(str(int(self.moveno.get())-1))
338
339             self.board.delLabels()   
340             self.board.delMarks()
341            
342             self.markAll()
343             self.displayLabels(c)
344        
345     def next(self):
346         """Go to the next move."""
347        
348         if not self.currentFile: return
349        
350         if not self.cursor.atEnd:
351             self.leaveNode()
352
353             try:
354                 c = self.cursor.next()
355             except SGFError:
356                 return 0 # failure
357             
358             self.moveno.set(str(int(self.moveno.get())+1))
359            
360             self.board.delMarks()
361             self.board.delLabels()
362            
363             self.displayNode(c)
364             if c.has_key('B') and c['B'][0]:
365                 self.capB = self.capB + len(self.board.undostack[len(self.board.undostack)-1][2])
366             if c.has_key('W') and c['W'][0]:
367                 self.capW = self.capW + len(self.board.undostack[len(self.board.undostack)-1][2])
368             self.capVar.set('Cap - B: ' + str(self.capB) + ', W: ' + str(self.capW))
369
370             if not self.cursor.atEnd: self.markAll()
371             else: self.passButton.config(state=DISABLED)
372                
373             return 1 # success
374
375        
376     def displayNode(self, c):
377         """Display the stones played in the current node,
378            and call displayLabels(). """
379        
380         if c.has_key('AB') and c['AB'][0]:
381             for p in c['AB']:
382                 if not self.board.play(self.convCoord(p), 'black'):
383                     self.board.undostack.append(((20,20), '' ,[]))
384                 else: self.board.currentColor = self.board.invert(self.board.currentColor)
385         if c.has_key('AW') and c['AW'][0]:
386             for p in c['AW']:
387                 if not self.board.play(self.convCoord(p), 'white'):
388                     self.board.undostack.append(((20,20), '' ,[]))
389                 else: self.board.currentColor = self.board.invert(self.board.currentColor)
390         if c.has_key('AE') and c['AE'][0]:
391             for p in c['AE']:
392                 if not self.board.remove(self.convCoord(p)):
393                     self.board.undostack.append(((20,20), '' ,[]))
394
395         if c.has_key('B') and c['B'][0]:
396             p = self.convCoord(c['B'][0])
397             if not p or not self.board.play(p, 'black'):
398                 self.board.undostack.append(((20,20), '' ,[]))
399         elif c.has_key('W') and c['W'][0]:
400             p = self.convCoord(c['W'][0])
401             if not p or not self.board.play(p, 'white'):
402                 self.board.undostack.append(((20,20), '' ,[]))
403
404         self.displayLabels(c)
405
406
407     def displayLabels(self, c):
408         """ Display the labels in the current node."""
409        
410         self.comments.delete('1.0', END)
411         if c.has_key('C'):
412             self.comments.insert('1.0', replace(c['C'][0], '\r', ''))
413
414         for type in ['CR', 'MA', 'SQ', 'TR']:
415             if c.has_key(type) and c[type][0]:
416                 for p in c[type]:
417                     self.board.placeLabel(self.convCoord(p), type)
418                
419         if c.has_key('LB') and c['LB'][0]:
420             for p1 in c['LB']:
421                 p, text = split(p1, ':')
422                 self.board.placeLabel(self.convCoord(p), 'LB', text)
423
424     def next10(self):
425         for i in range(10): self.next()
426
427     def prev10(self):
428         for i in range(10): self.prev()
429        
430     def end(self):
431         """ Go to end of game. """
432         if not self.currentFile: return
433         while not self.cursor.atEnd and self.next(): pass
434
435     def start(self, update=1):
436         """ Go to beginning of game."""
437         if not self.currentFile: return
438         if update: self.leaveNode()
439         self.board.delMarks()
440         self.board.delLabels()
441         self.setup()
442
443
444     def leaveNode(self):
445         """This method should be called before leaving the current node in the
446         SGF file. It will take care of saving the changes (currently that
447         means: changes to the comments) to the SGF file."""
448
449         if not self.currentFile: return
450
451         s = strip(self.comments.get('1.0', END))
452         changed = 0
453        
454         if self.cursor.currentNode.has_key('C'):
455             if strip(self.cursor.currentNode['C'][0]) != strip(s):
456                 self.cursor.currentNode['C'] = [s]
457                 changed = 1
458         else:
459             if strip(s):
460                 self.cursor.currentNode['C'] = [s]
461                 changed = 1
462
463         if changed: self.cursor.updateCurrentNode()
464
465
466
467     def readSGFfile(self, path = None, filename = None):
468         """ Read an SGF file given by filename (if None, ask
469             for a filename). """
470
471         self.leaveNode()
472
473         self.board.clear()
474         self.board.delMarks()
475        
476         self.gameName.set('')
477        
478         self.board.state('normal', self.nextMove)
479         self.master.update()
480  
481         if not path:
482             path = '.'
483
484         if not filename:
485             path, filename = os.path.split(tkFileDialog.askopenfilename(filetypes=[('SGF files', '*.sgf'),
486                                                                                    ('All files', '*')],
487                                                                         initialdir=self.sgfpath))
488         if filename:
489             try:
490                 f = open(os.path.join(path,filename))
491                 s = f.read()
492                 f.close()
493             except IOError:
494                 showwarning('Open file', 'Cannot open this file\n')
495                 return 0
496             else:
497                 if not s: return 0
498                 self.sgfpath = path
499                 try:
500                     self.currentSGF = s
501                     self.cursor = EnhancedCursor(self.currentSGF, self.comments)
502                 except SGFError:
503                     showwarning('Parsing Error', 'Error in SGF file!')
504                     return 0
505                 self.currentFile = filename
506                 return self.setup()
507                
508
509     def saveSGFfile(self):
510         if not self.currentFile or not self.cursor: return
511         self.leaveNode()
512        
513         file = open(os.path.join(self.sgfpath, self.currentFile), 'w')
514         file.write(self.cursor.output())
515         file.close()
516        
517     def saveasSGFfile(self):
518         if not self.currentFile or not self.cursor: return
519         self.leaveNode()
520
521         f = tkFileDialog.asksaveasfilename(filetypes=[('SGF files', '*.sgf'), ('All files', '*')],
522                                            initialdir = self.sgfpath)
523
524         if not f: return
525         try:
526             file = open(f, 'w')
527             file.write(self.cursor.output())
528             file.close()
529         except IOError:
530             showwarning('I/O Error', 'Could not write to file ' + f)
531
532         self.sgfpath, self.currentFile = os.path.split(f)
533         self.gameName.set(self.currentFile[:15])
534
535        
536     def quit(self):
537         """ Exit the program. """
538         self.master.destroy()
539
540
541     def saveOptions(self):
542         """ Save options to disk (to file 'v.opt'). """
543         self.options.windowGeom = StringVar()
544         self.options.windowGeom.set(self.master.geometry())
545         self.options.saveToDisk(os.path.join(self.optionspath,'v.opt'),
546                                 lambda: showwarning('Save options', 'IO Error'))
547
548     def loadOptions(self):
549         """ Load options from disk. """       
550         self.options.windowGeom = StringVar()
551         self.options.loadFromDisk(os.path.join(self.optionspath,'v.opt'))
552         if self.options.windowGeom.get():
553             self.master.geometry(self.options.windowGeom.get())
554
555
556     def helpAbout(self):
557         """ Display the 'About ...' window with some basic information. """
558        
559         t = 'v.py - written by Ulrich Goertz (u@g0ertz.de)\n\n'
560         t = t + 'v.py is a program to display go game records in SGF format.\n'
561         t = t + 'It comes together with the go database program Kombilo.\n'
562         t = t + 'You can find more information on v.py and Kombilo and the newest '
563         t = t + 'version at http://www.g0ertz.de/kombilo/\n\n'
564        
565         t = t + 'v.py is free software; for more information '
566         t = t + 'see the file license.txt.\n\n'
567         t = t + 'It is written in Python (see http://www.python.org/). '
568
569         window = Toplevel()
570         window.title('About v.py ...')
571
572         text = Text(window, height=15, width=60, relief=FLAT, wrap=WORD)
573         text.insert(1.0, t)
574  
575         text.config(state=DISABLED)
576         text.pack()
577
578         b = Button(window, text="OK", command = window.destroy)
579         b.pack(side=RIGHT)
580        
581         window.update_idletasks()
582        
583         window.focus()
584         window.grab_set()
585         window.wait_window()
586
587
588     def helpLicense(self):
589         """ Display the GNU General Public License. """
590         try:
591             t = 'v.py\n (C) Ulrich Goertz (u@g0ertz.de), 2001-2002.\n'
592             t = t + '------------------------------------------------------------------------\n\n'
593             file = open(os.path.join(self.basepath,'doc/license.txt'))
594             t = t + file.read()
595             file.close()
596         except IOError:
597             t = 'v.py was written by Ulrich Goertz (u@g0ertz.de).\n'
598             t = t + 'It is published under the GNU General Public License. '
599             t = t + 'See the file doc/license.txt for more information. '
600             t = t + 'The GPL is also available at http://www.gnu.org/copyleft/gpl.html\n'
601             t = t + 'This program is distributed WITHOUT ANY WARRANTY!\n\n'
602         self.textWindow(t,'v.py license')
603
604     def textWindow(self, t, title='', grab=1):
605         """ Open a window and display the text in the string t.
606             The window has the title title, and grabs the focus if grab==1. """
607        
608         window = Toplevel()
609         window.title(title)
610         text = ScrolledText(window, height=25, width=80, relief=FLAT, wrap=WORD)
611         text.insert(1.0, t)
612  
613         text.config(state=DISABLED)
614         text.pack()
615
616         b = Button(window, text="OK", command = window.destroy)
617         b.pack(side=RIGHT)
618        
619         window.update_idletasks()
620         if grab:
621             window.focus()
622             window.grab_set()
623             window.wait_window()
624
625
626    
627     def gameinfoOK(self):
628         keylist = ['PB', 'BR', 'PW', 'WR', 'EV', 'RE', 'DT', 'KM']
629         for key in keylist:
630             self.gameinfoDict[key] = [self.gameinfoVars[key].get()]
631                
632         self.gameinfoDict['GC'] = [strip(self.gameinfoGCText.get('1.0', END))]
633
634         for key in keylist + ['GC']:
635             if not strip(self.gameinfoDict[key][0]):
636                 del self.gameinfoDict[key]
637
638         s = self.gameinfoOthersText.get('1.0', END)
639         try:
640             d = Cursor('(;' + s + ')').getRootNode()
641             for k in d.keys():
642                 self.gameinfoDict[k] = d[k]
643
644             for k in self.gameinfoDict.keys():
645                 if (not k in keylist + ['GC']) and (not k in d.keys()):
646                     del self.gameinfoDict[k]
647
648         except:
649             showwarning('SGF Error', "Parse error in 'Other SGF tags'")
650
651         self.gameinfoWindow.destroy()
652
653
654     def gameinfo(self, data=None):
655         """ Open window with the game info of the current game."""
656
657         if not data and not self.currentFile:
658             return
659        
660         if not data:
661             self.gameinfoDict = self.cursor.getRootNode()
662             returnChanges = 0
663         else:
664             self.gameinfoDict = data
665             returnChanges = 1
666            
667         window = Toplevel()
668         self.gameinfoWindow = window
669         window.title('Game Info')
670        
671         keylist = ['GC', 'PB', 'BR', 'PW', 'WR', 'EV', 'RE', 'DT', 'KM']
672
673         if self.gameinfoDict.has_key('GC'):
674             self.gameinfoDict['GC'][0] = replace(self.gameinfoDict['GC'][0], '\r', '')               # ?!
675
676         self.gameinfoVars = {}
677         for key in keylist:
678             self.gameinfoVars[key] = StringVar()
679             if not (self.gameinfoDict.has_key(key) and self.gameinfoDict[key]):
680                 self.gameinfoVars[key].set('')
681             else:
682                 self.gameinfoVars[key].set(self.gameinfoDict[key][0])
683                
684         self.gameinfoVars['others'] = StringVar()
685         oth = ''
686         for key in self.gameinfoDict.keys():
687             if key not in keylist: oth += key + '[' + join([SGFescape(s) for s in self.gameinfoDict[key]], '][') + ']\n'
688         self.gameinfoVars['others'].set(oth)
689
690         f = Frame(window)
691         f.pack(side=TOP, anchor=W)
692         Label(f, text='White:', justify=LEFT).grid(row=0, column=0, sticky=W)
693         Entry(f, width = 30, textvariable=self.gameinfoVars['PW']).grid(row=0, column=1)
694         Entry(f, width = 5, textvariable=self.gameinfoVars['WR']).grid(row=0, column=2)
695
696         Label(f, text='Black:', justify=LEFT).grid(row=1, column=0, sticky=W)
697         Entry(f, width = 30, textvariable=self.gameinfoVars['PB']).grid(row=1, column=1)
698         Entry(f, width = 5, textvariable=self.gameinfoVars['BR']).grid(row=1, column=2)
699
700         i = 2
701         for key, text in [('EV', 'Event'), ('RE', 'Result'), ('DT', 'Date'),
702                           ('KM', 'Komi')]:
703             Label(f, text=text+':').grid(row=i, column=0, sticky=W)
704             Entry(f, width = 35, textvariable=self.gameinfoVars[key]).grid(row=i, column=1, columnspan=2, sticky=W+E)
705             i += 1
706            
707         Label(window, text='Game Comment: ').pack(anchor=W)
708         self.gameinfoGCText = ScrolledText(window, height=5, width=40, relief=SUNKEN, wrap=WORD)
709         self.gameinfoGCText.pack(expand=YES, fill=BOTH)
710         self.gameinfoGCText.insert(END, self.gameinfoVars['GC'].get())
711        
712         Label(window, text='Other SGF tags: ').pack(anchor=W)
713         self.gameinfoOthersText = ScrolledText(window, height=5, width=40, relief=FLAT, wrap=WORD)
714         self.gameinfoOthersText.pack(expand=YES, fill=X)
715         self.gameinfoOthersText.insert(END, self.gameinfoVars['others'].get())
716        
717         Button(window, text='Cancel', command = window.destroy).pack(side=RIGHT)
718         Button(window, text="OK", command = self.gameinfoOK).pack(side=RIGHT)
719        
720         window.update_idletasks()
721         window.focus()
722         window.grab_set()
723         window.wait_window()
724
725         if returnChanges:
726             return self.gameinfoDict
727         else:
728             self.cursor.updateRootNode(self.gameinfoDict)
729
730     def initMenus(self):
731
732         self.options = BunchTkVar()
733
734         menu = Menu(self.master)
735         self.master.config(menu=menu)
736
737         self.filemenu = Menu(menu)
738         menu.add_cascade(label='File', underline=0, menu=self.filemenu)
739         self.filemenu.add_command(label='Open SGF', underline=0, command=self.readSGFfile)
740         self.filemenu.add_command(label='Save SGF', underline=0, command=self.saveSGFfile)
741         self.filemenu.add_command(label='Save SGF as', underline=9, command=self.saveasSGFfile)
742        
743         self.filemenu.add_separator()
744         self.filemenu.add_command(label='Exit', underline =1, command=self.quit)
745         self.optionsmenu = Menu(menu)
746         menu.add_cascade(label='Options', underline=0, menu=self.optionsmenu)
747
748         self.options.fuzzy = self.board.fuzzy    # make 'save options' easy
749         self.optionsmenu.add_checkbutton(label='Fuzzy stone placement', underline = 0,
750                                          variable=self.options.fuzzy,
751                                          command=self.board.fuzzyStones)
752
753         self.options.shadedStoneVar = self.board.shadedStoneVar
754         self.optionsmenu.add_checkbutton(label='Shaded stone mouse pointer',
755                                          variable=self.options.shadedStoneVar)
756         self.options.shadedStoneVar.set(1)
757
758         self.options.showNextMoveVar = IntVar()
759         self.optionsmenu.add_checkbutton(label='Show next move', underline=5,
760                                          variable = self.options.showNextMoveVar,
761                                          command = self.showNextMove)
762
763         self.optionsmenu.add_separator()
764
765         self.optionsmenu.add_command(label = 'Save options', underline = 0,
766                                      command = self.saveOptions)
767
768         self.helpmenu = Menu(menu, name='help')
769         menu.add_cascade(label='Help', underline=0, menu=self.helpmenu)
770
771         self.helpmenu.add_command(label='About ...', underline=0, command=self.helpAbout)
772        
773         self.helpmenu.add_command(label='License', underline=0, command=self.helpLicense)
774
775         self.mainMenu = menu
776
777
778     def initButtons(self, navFrame, labelFrame, commentFrame):
779         # The buttons
780
781         self.nextButton = Button(navFrame, text='->', command=self.next)
782         self.boardFrame.bind('<Right>', lambda e, s = self.nextButton: s.invoke())
783         self.prevButton = Button(navFrame, text='<-', command=self.prev)                               
784         self.boardFrame.bind('<Left>', lambda e, s = self.prevButton: s.invoke())
785         self.next10Button = Button(navFrame, text='-> 10', command=self.next10)
786         self.boardFrame.bind('<Down>', lambda e, s = self.next10Button: s.invoke())
787         self.prev10Button = Button(navFrame, text='<- 10', command=self.prev10)
788         self.boardFrame.bind('<Up>', lambda e, s = self.prev10Button: s.invoke())
789         self.startButton = Button(navFrame, text='|<-', command=self.start)
790         self.boardFrame.bind('<Home>', lambda e, s = self.startButton: s.invoke())
791         self.endButton = Button(navFrame, text='->|', command=self.end)
792         self.boardFrame.bind('<End>', lambda e, s = self.endButton: s.invoke())
793        
794         self.passButton = Button(navFrame, text='Pass', command = self.passFct)
795         self.passButton.config(state=DISABLED)
796        
797         self.gameinfoButton = Button(navFrame, text = 'Info', command = self.gameinfo,
798       &nb