root/04/release-0.4d/v.py

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

Import release 0.4d

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
26from Tkinter import *
27from tkMessageBox import *
28from ScrolledText import ScrolledText
29import tkFileDialog
30import cPickle
31import os
32import sys
33from string import split, find, replace, join, strip
34from copy import copy, deepcopy
35
36from sgfparser import *           
37from board1 import *
38
39# ---------------------------------------------------------------------------------------
40
41class 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
72class 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
102class 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
124class 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