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

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

Import release 0.4d

Line 
1#!/usr/bin/python
2# File: kombilo.py
3
4##   Copyright (C) 2001-2 Ulrich Goertz (u@g0ertz.de)
5
6##   Kombilo 0.4d is a go database program.
7
8##   This program is free software; you can redistribute it and/or modify
9##   it under the terms of the GNU General Public License as published by
10##   the Free Software Foundation; either version 2 of the License, or
11##   (at your option) any later version.
12
13##   This program is distributed in the hope that it will be useful,
14##   but WITHOUT ANY WARRANTY; without even the implied warranty of
15##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16##   GNU General Public License for more details.
17
18##   You should have received a copy of the GNU General Public License
19##   along with this program (see doc/license.txt); if not, write to the Free Software
20##   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21##   The GNU GPL is also currently available at
22##   http://www.gnu.org/copyleft/gpl.html
23
24from Tkinter import *
25from tkMessageBox import *
26from ScrolledText import ScrolledText
27import tkFileDialog
28from tkCommonDialog import Dialog
29from tkSimpleDialog import askstring
30import time
31import os
32import sys
33import cPickle
34from copy import copy, deepcopy
35from string import split, find, join, strip, replace, digits
36import glob
37import re
38from array import *
39import webbrowser
40
41from sgfparser import *
42from board1 import *
43import v
44
45try:
46    import matchC
47    CimportSucceeded = 1
48except:
49    CimportSucceeded = 0
50
51# ---------------------------------------------------------------------------------------
52
53class BoardWC(Board):
54    """ Board with support for wildcards and selection
55        of search-relevant region. Furthermore, snapshot returns a dictionary which
56        describes the current board position. It can then be restored with restore."""
57   
58    def __init__(self, master, boardSize, canvasSize, fuzzy, labelFontsize, fixedColor, smartFixedColor):
59        Board.__init__(self, master, boardSize, canvasSize, fuzzy, labelFontsize)
60
61        self.wildcards = {}
62
63        self.selection = ((1,1),(19,19))
64
65        self.fixedColor = fixedColor
66        self.smartFixedColor = smartFixedColor
67       
68        self.bind('<Button-3>', self.selStart)
69        self.bind('<B3-Motion>', self.selDrag)
70        self.bind('<Shift-1>', self.wildcard)
71
72    def resize(self, event):
73        Board.resize(self, event)
74        for x,y in self.wildcards.keys():
75            x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
76            self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
77                                                     tags=('wildcard','non-bg'))
78
79        self.delete('selection')
80        if self.selection != ((1,1),(19,19)) and self.selection[1] != (0,0):
81            p0 = self.getPixelCoord(self.selection[0],1)
82            p1 = self.getPixelCoord((self.selection[1][0]+1, self.selection[1][1]+1),1)
83
84            self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50',
85                                  tags='selection')
86            self.tkraise('non-bg')
87           
88        self.update_idletasks()
89
90    def wildcard(self, event):
91        """ Place a wildcard at position of click. """
92        x, y = self.getBoardCoord((event.x, event.y), 1)
93        if not x*y or self.status.has_key((x,y)) \
94           or self.wildcards.has_key((x,y)): return
95
96        x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
97        self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
98                                                 tags=('wildcard','non-bg'))
99
100        self.changed.set(1)
101       
102
103    def delWildcards(self):
104        if self.wildcards: self.changed.set(1)
105        self.delete('wildcard')
106        self.wildcards = {}
107                             
108    # ---- selection of search-relevant section -----------------------------------
109
110    def selStart(self, event):
111        """ React to right-click. """
112        self.delete('selection')
113        x, y = self.getBoardCoord((event.x, event.y), 1)
114        x = max(x, 1)
115        y = max(y, 1)
116        self.selection = ((x,y), (0,0))
117        if self.smartFixedColor.get():
118            self.fixedColor.set(1)
119        self.changed.set(1)
120
121    def selDrag(self, event):
122        """ React to right-mouse-key-drag. """
123        pos = self.getBoardCoord((event.x, event.y), 1)
124        if pos[0] >= self.selection[0][0] and pos[1] >= self.selection[0][1]:
125            self.setSelection(self.selection[0], pos)
126           
127
128    def setSelection(self, pos0, pos1):
129        self.selection = (pos0, pos1)
130        self.delete('selection')
131        p0 = self.getPixelCoord(pos0,1)
132        p1 = self.getPixelCoord((pos1[0]+1, pos1[1]+1), 1)
133
134        self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50',
135                              tags='selection')
136        self.tkraise('non-bg')
137       
138        if self.smartFixedColor.get():
139            if self.selection == ((1,1), (19,19)):
140                self.fixedColor.set(1)
141            else:
142                self.fixedColor.set(0)
143               
144       
145    def newPosition(self):
146        """ Clear board, selection. """
147        self.delete('selection')
148        self.clear()
149        self.delLabels()
150        self.delMarks()
151        self.delWildcards()
152        self.selection = ((1,1),(19,19))
153
154        if self.smartFixedColor.get():
155            self.fixedColor.set(1)
156
157    # ---- snapshot & restore (for 'back' button)
158
159    def snapshot(self):
160        """ Return a dictionary which contains the data of all the objects
161            currently displayed on the board. """
162       
163        data = {}
164        data['status'] = copy(self.status)
165        data['marks'] = copy(self.marks)
166        data['labels'] = copy(self.labels)
167        data['wildcards'] = copy(self.wildcards)
168        data['selection'] = self.selection
169        data['currentcolor'] = self.currentColor
170        data['undostack'] = deepcopy(self.undostack)
171       
172        return data
173   
174    def restore(self, d):
175        """ Restore a board position from a 'snapshot' dictionary. """
176       
177        self.newPosition()
178        for x in d['status'].keys(): self.play(x, d['status'][x])
179        for x in d['marks'].keys(): self.placeMark(x, d['marks'][x])
180        for x in d['labels'].keys(): self.placeLabel(x, d['labels'][x][0], d['labels'][x][1])
181        for x,y in d['wildcards'].keys():
182            x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
183            self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
184                                                     tags=('wildcard','non-bg'))
185
186        if d['selection'] != ((1,1),(19,19)) and d['selection'][1] != (0,0):
187            p0 = self.getPixelCoord(d['selection'][0],1)
188            p1 = self.getPixelCoord((d['selection'][1][0]+1, d['selection'][1][1]+1),1)
189
190            self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50',
191                                  tags='selection')
192            self.tkraise('non-bg')
193            self.selection = d['selection']
194
195        self.currentColor = d['currentcolor']
196        self.undostack = deepcopy(d['undostack'])
197
198# ---------------------------------------------------------------------------------------
199
200class chooseDirectory(Dialog):
201    """ A wrapper tor the Tk chooseDirectory widget. """
202   
203    command = "tk_chooseDirectory"
204
205    def _fixresult(self, widget, result):
206        if result:
207            self.options["initialdir"] = result
208        self.directory = result
209        return result
210
211def askdirectory(**options):
212    return apply(chooseDirectory, (), options).show()
213
214# ---------------------------------------------------------------------------------------
215
216class TextEditor:
217    """ A very simple text editor, based on the Tkinter ScrolledText widget.
218    You can perform very limited editing, and save the result to a file. """
219
220    def __init__(self, t = '', defpath='', font = None):
221
222        if font is None:
223            font = (StringVar(), IntVar(), StringVar())
224            font[0].set('Courier')
225            font[1].set(10)
226            font[2].set('')
227           
228        self.window = Toplevel()
229
230        self.window.protocol('WM_DELETE_WINDOW', self.quit)
231
232        self.text = ScrolledText(self.window, width=70, height=30,
233                                 font=(font[0].get(), font[1].get(), font[2].get()))
234        self.text.pack(fill=BOTH, expand=YES)
235        self.text.insert(END, t)
236
237        self.buttonFrame = Frame(self.window)
238        self.buttonFrame.pack(side=RIGHT)
239
240        Button(self.buttonFrame, text='Quit', command=self.quit).pack(side=RIGHT)
241        Button(self.buttonFrame, text='Save as', command=self.saveas).pack(side=RIGHT)
242
243        # self.window.tkraise()
244        self.window.focus_force()
245
246        if defpath:
247            self.defpath = defpath
248        else:
249            self.defpath = os.curdir
250           
251    def saveas(self):
252        f = tkFileDialog.asksaveasfilename(initialdir = self.defpath)
253        try:
254            file = open(f, 'w')
255            file.write(self.text.get('1.0', END))
256            file.close()
257        except IOError:
258            showwarning('IO Error', 'Cannot write to ' + f)
259
260    def quit(self):
261        self.window.destroy()
262
263
264
265class ESR_TextEditor(TextEditor):
266    """The text editor which is used by the exportSearchResults function.
267    It adds a button to include the complete game list to the TextEditor."""
268
269    def __init__(self, master, style, t='', defpath='', font=None):
270        TextEditor.__init__(self, t, defpath, font)
271        self.master = master
272        self.style = style
273       
274        Button(self.buttonFrame, text='Include game list', command=self.includeGameList).pack(side=RIGHT)
275
276    def includeGameList(self):
277        if self.style == 'wiki':
278            self.text.insert(END, '\n\n!Game list\n\n' + join(self.master.gamelist.list.get(0, END), ' %%%\n'))
279        else:
280            self.text.insert(END, '\n\nGame list\n\n' + join(self.master.gamelist.list.get(0, END), '\n'))
281       
282
283# ---------------------------------------------------------------------------------------
284
285class ScrolledList(Frame):
286    """ A listbox with vertical and horizontal scrollbars. """
287   
288    def __init__(self, parent, **kw):
289        Frame.__init__(self, parent)
290
291        self.sbar = Scrollbar(self)
292        self.sbar1 = Scrollbar(self)
293
294        if not kw: kw = {}
295
296        for var, value in [('height', 12), ('width', 40), ('relief', SUNKEN),
297                           ('selectmode', SINGLE), ('takefocus', 1)]:
298            if not kw.has_key(var): kw[var] = value
299       
300        self.list = Listbox(self, kw)
301        self.sbar.config(command = self.list.yview)
302        self.sbar1.config(command = self.list.xview, orient='horizontal')
303        self.list.config(xscrollcommand = self.sbar1.set, yscrollcommand = self.sbar.set)
304        self.list.grid(row=0, column=0, sticky=NSEW)
305        self.sbar.grid(row=0, column=1, sticky=NSEW)
306        self.sbar1.grid(row=1, column=0, sticky=NSEW)
307
308        self.rowconfigure(0, weight=1)
309        self.columnconfigure(0, weight=1)
310
311        self.focus_force()
312       
313
314# -------------------------------------------------------------------------------------
315
316class GameList(ScrolledList):
317    """ This is a scrolled list which shows the game list. All the underlying data
318        is contained in self.DBlist, which is a list of dictionaries containing the
319        information for the single databases. self.DBlist[i] will contain the keys
320        'name': name (=path) of the database
321        'data': list of all games in the database.
322                This is a list of tuples of the form
323                (filename, namelistIndex, PB, PW, result, signature)
324        'current': a list of the indices of the games in data which are in the current
325                   game list
326        'results': for each game in current, the list of matches found in the previous
327                   search (If there are lots of matches, this list will consume a lot
328                   of memory. Maybe at some time I will get around to fix this ...)
329        'stringlist': A list of strings from entries in 'data' which are not yet
330                      displayed in the ScrolledList (to speed things up by inserting
331                      many entries at once into the ScrolledList)
332        """
333   
334    def __init__(self, parent, master, noGamesLabel, winPercLabel, gameinfo):
335        ScrolledList.__init__(self, parent)
336        self.list.config(width=52, height=10)
337        self.list.bind('<Button-1>', self.printGameInfo)
338
339        self.bind('<Up>', self.up)
340        self.bind('<Down>', self.down)
341        self.bind('<Prior>', self.pgup)
342        self.bind('<Next>', self.pgdown)
343        self.bind('<Return>', self.handleDoubleClick)
344        self.list.bind('<Double-1>', self.handleDoubleClick)
345        self.list.bind('<Button-3>', self.rightMouseButton)
346
347        self.master = master
348       
349        self.DBlist = []      # list of dicts
350
351        self.noGamesLabel = noGamesLabel
352        self.winPercLabel = winPercLabel
353        self.gameinfo = gameinfo
354       
355        self.Bwins = 0
356        self.Wwins = 0
357        self.Owins = 0 # others: Jigo, Void, Left unfinished, ? (Unknown)
358
359        self.appendClist = []
360
361
362    def rightMouseButton(self, event):
363       
364        index = self.list.nearest(event.y)
365        if index == -1: return
366        DBindex = 0
367        while index >= len(self.DBlist[DBindex]['current']):
368            index -= len(self.DBlist[DBindex]['current'])
369            DBindex += 1
370        if DBindex >= len(self.DBlist) or index > len(self.DBlist[DBindex]['current']): return
371           
372        filename = strip(os.path.join(self.DBlist[DBindex]['name'],
373                                      self.DBlist[DBindex]['data'][self.DBlist[DBindex]['current'][index]][0] + '.sgf'))
374
375        try:
376            file = open(filename)
377            sgf = file.read()
378            file.close()
379            c = Cursor(sgf)
380            rootNode = c.getRootNode()
381        except:
382            return
383
384        backup = copy(rootNode)
385
386        newRootNode = self.master.gameinfo(rootNode)
387        if backup != newRootNode:
388            c.updateRootNode(newRootNode)
389            try:
390                file = open(filename, 'w')
391                file.write(c.output())
392                file.close()
393            except IOError:
394                showwarning('I/O Error', 'Could not write to file ' + filename)
395
396
397    def handleDoubleClick(self, event):
398        """ This is called upon double-clicks."""
399       
400        index = self.list.curselection()
401        if index:
402            label = self.list.get(index)
403            self.master.openViewer(index, label)
404
405
406    def up(self, event):
407        if not self.list.curselection(): return
408        index = int(self.list.curselection()[0])
409        if index != 0:
410            self.list.select_clear(index)
411            self.list.select_set(index-1)
412            self.list.see(index-1)
413            self.printGameInfo(None, index-1)
414
415    def down(self, event):
416        if not self.list.curselection(): return
417        index = int(self.list.curselection()[0])
418        if index != self.list.size()-1:
419            self.list.select_clear(index)
420            self.list.select_set(index+1)
421            self.list.see(index+1)
422            self.printGameInfo(None, index+1)
423
424    def pgup(self, event):
425        if not self.list.curselection(): return
426        index = int(self.list.curselection()[0])
427        if index >= 10:
428            self.list.select_clear(index)
429            self.list.select_set(index-10)
430            self.list.see(index-10)
431            self.printGameInfo(None, index-10)
432        elif self.list.size():
433            self.list.select_clear(index)
434            self.list.select_set(0)
435            self.list.see(0)
436            self.printGameInfo(None, 0)
437
438    def pgdown(self, event):
439        if not self.list.curselection(): return
440        index = int(self.list.curselection()[0])
441        if index <= self.list.size()-10:
442            self.list.select_clear(index)
443            self.list.select_set(index+10)
444            self.list.see(index+10)
445            self.printGameInfo(None, index+10)
446        elif self.list.size():
447            self.list.select_clear(index)
448            self.list.select_set(self.list.size()-1)
449            self.list.see(END)
450            self.printGameInfo(None, self.list.size()-1)
451
452    def delete(self):
453        """ Clear the list. """
454
455        self.update()
456
457        for db in self.DBlist:
458            if not db['disabled']:
459                db['current'] = array('L')
460                db['results'] = []
461           
462        self.Bwins = 0
463        self.Wwins = 0
464        self.Owins = 0 
465        self.update()
466
467        self.list.delete(0,END)
468        self.list.update_idletasks()
469
470    def append(self, i, d):
471        """ Append an entry to the gamelist. """
472        self.DBlist[i]['data'].append(d)
473
474        if not self.DBlist[i]['disabled']: 
475
476            if d[4] == 'B': self.Bwins = self.Bwins + 1
477            elif d[4] == 'W': self.Wwins = self.Wwins + 1
478            else: self.Owins = self.Owins + 1
479       
480            s = d[0] + ': ' + d[3] + ' - ' + d[2] + ' (' + d[4] + ') ' # filename: PW-PB (RES)
481            self.DBlist[i]['stringlist'].append(s)
482
483
484    def appendC(self, db, i, res = ''):
485        """ Append an entry to self.current """
486       
487        db['current'].append(i)
488        db['results'].append(res)
489        self.appendClist.append((db['data'][i], res))
490
491
492    def reset(self):
493        """ Reset the list, s.t. it includes all the games from self.data. """
494        for db in self.DBlist:
495            if not db['disabled']:
496                db['current'] = array('L', range(len(db['data'])))
497                db['results'] = [''] * len(db['data'])
498                self.appendClist += map(None, db['data'], db['results'])
499            else:
500                db['current'] = array('L')
501                db['results'] = []
502        self.list.delete(0, END)
503        self.Bwins, self.Wwins, self.Owins = 0, 0, 0
504        self.update()
505        self.clearGameInfo()
506       
507    def update(self):
508        """ Put the changes in data, current in effect in self.list. """
509       
510        if self.appendClist:         # display appendC'ed entries
511           
512            strl = []
513           
514            for d, res in self.appendClist:
515                if d[4] == 'B': self.Bwins = self.Bwins + 1
516                elif d[4] == 'W': self.Wwins = self.Wwins + 1
517                else: self.Owins = self.Owins + 1
518       
519                s = d[0] + ': ' + d[3] + ' - ' + d[2] + ' (' + d[4] + ') ' + res
520                strl.append(s)
521            apply(self.list.insert, [END] + strl)
522
523            self.appendClist = []
524
525        for db in self.DBlist:
526            if db['stringlist']:                              # display append'ed entries
527                apply(self.list.insert, [END] + db['stringlist'])
528
529                db['current'].extend(array('L', range(len(db['data'])-len(db['stringlist']), len(db['data']))))
530            db['results'] = db['results'] + [''] * len(db['stringlist'])
531            db['stringlist'] = []
532
533        noOfG = self.noOfGames()
534        self.noGamesLabel.config(text = `noOfG` + ' games')
535
536        if noOfG:
537            Bperc = self.Bwins * 100.0 / noOfG
538            Wperc = self.Wwins * 100.0 / noOfG
539            self.winPercLabel.config(text='B: %1.1f%%, W: %1.1f%%' % (Bperc, Wperc))
540        else: self.winPercLabel.config(text='')
541
542    def noOfGames(self):
543        return reduce(lambda x,y:x+y, [ len(db['current']) for db in self.DBlist if not db['disabled'] ], 0)
544
545    def printGameInfo(self, event, index = -1):
546        """ Print game info of selected game. """
547
548        if index == -1:
549            index = self.list.nearest(event.y)
550
551        if index == -1:
552            return
553
554        DBindex = 0
555
556        while index >= len(self.DBlist[DBindex]['current']):
557            index -= len(self.DBlist[DBindex]['current'])
558            DBindex += 1
559
560        if DBindex >= len(self.DBlist) or index > len(self.DBlist[DBindex]['current']): return
561           
562        filename = strip(os.path.join(self.DBlist[DBindex]['name'],
563                                      self.DBlist[DBindex]['data'][self.DBlist[DBindex]['current'][index]][0] + '.sgf'))
564
565        try:
566            f = open(filename)
567            sgf = f.read()
568            f.close()
569           
570            node = Cursor(sgf).getRootNode()
571           
572        except:
573            return
574       
575        t = ''
576       
577        if node.has_key('PW'): t = t + node['PW'][0]
578        else: t = t + ' ?'
579        if node.has_key('WR'): t = t + ', ' + node['WR'][0]
580
581        t = t + ' - '
582
583        if node.has_key('PB'): t = t + node['PB'][0]
584        else: t = t + ' ?'
585        if node.has_key('BR'): t = t + ', ' + node['BR'][0]
586
587        if node.has_key('RE'): t = t + ', ' + node['RE'][0]
588        if node.has_key('KM'): t = t + ' (Komi ' + node['KM'][0] + ')'
589        if node.has_key('HA'