root/04/bugfix/kombilo.py

Revision 37, 195.1 kB (checked in by ug, 5 years ago)

Corrected version number

  • Property svn:executable set to
Line 
1 #!/usr/bin/python
2 # File: kombilo.py
3
4 ##   Copyright (C) 2001-2 Ulrich Goertz (u@g0ertz.de)
5
6 ##   Kombilo 0.4e 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
24 from Tkinter import *
25 from tkMessageBox import *
26 from ScrolledText import ScrolledText
27 import tkFileDialog
28 from tkCommonDialog import Dialog
29 from tkSimpleDialog import askstring
30 import time
31 import os
32 import sys
33 import cPickle
34 from copy import copy, deepcopy
35 from string import split, find, join, strip, replace, digits
36 import glob
37 import re
38 from array import *
39 import webbrowser
40
41 from sgfparser import *
42 from board1 import *
43 import v
44
45 try:
46     import matchC
47     CimportSucceeded = 1
48 except:
49     CimportSucceeded = 0
50
51 # ---------------------------------------------------------------------------------------
52
53 class 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         if sys.platform.startswith('darwin'):
69             self.bind('<M2-Button-1>', self.selStart)
70             self.bind('<M2-B1-Motion>', self.selDrag)
71         else:
72             self.bind('<Button-3>', self.selStart)
73             self.bind('<B3-Motion>', self.selDrag)
74         self.bind('<Shift-1>', self.wildcard)
75
76     def resize(self, event):
77         Board.resize(self, event)
78         for x,y in self.wildcards.keys():
79             x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
80             self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
81                                                      tags=('wildcard','non-bg'))
82
83         self.delete('selection')
84         if self.selection != ((1,1),(19,19)) and self.selection[1] != (0,0):
85             p0 = self.getPixelCoord(self.selection[0],1)
86             p1 = self.getPixelCoord((self.selection[1][0]+1, self.selection[1][1]+1),1)
87
88             self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50',
89                                   tags='selection')
90             self.tkraise('non-bg')
91            
92         self.update_idletasks()
93
94     def wildcard(self, event):
95         """ Place a wildcard at position of click. """
96         x, y = self.getBoardCoord((event.x, event.y), 1)
97         if not x*y or self.status.has_key((x,y)) \
98            or self.wildcards.has_key((x,y)): return
99
100         x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
101         self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
102                                                  tags=('wildcard','non-bg'))
103
104         self.changed.set(1)
105        
106
107     def delWildcards(self):
108         if self.wildcards: self.changed.set(1)
109         self.delete('wildcard')
110         self.wildcards = {}
111                              
112     # ---- selection of search-relevant section -----------------------------------
113
114     def selStart(self, event):
115         """ React to right-click. """
116         self.delete('selection')
117         x, y = self.getBoardCoord((event.x, event.y), 1)
118         x = max(x, 1)
119         y = max(y, 1)
120         self.selection = ((x,y), (0,0))
121         if self.smartFixedColor.get():
122             self.fixedColor.set(1)
123         self.changed.set(1)
124
125     def selDrag(self, event):
126         """ React to right-mouse-key-drag. """
127         pos = self.getBoardCoord((event.x, event.y), 1)
128         if pos[0] >= self.selection[0][0] and pos[1] >= self.selection[0][1]:
129             self.setSelection(self.selection[0], pos)
130            
131
132     def setSelection(self, pos0, pos1):
133         self.selection = (pos0, pos1)
134         self.delete('selection')
135         p0 = self.getPixelCoord(pos0,1)
136         p1 = self.getPixelCoord((pos1[0]+1, pos1[1]+1), 1)
137
138         self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50',
139                               tags='selection')
140         self.tkraise('non-bg')
141        
142         if self.smartFixedColor.get():
143             if self.selection == ((1,1), (19,19)):
144                 self.fixedColor.set(1)
145             else:
146                 self.fixedColor.set(0)
147                
148        
149     def newPosition(self):
150         """ Clear board, selection. """
151         self.delete('selection')
152         self.clear()
153         self.delLabels()
154         self.delMarks()
155         self.delWildcards()
156         self.selection = ((1,1),(19,19))
157
158         if self.smartFixedColor.get():
159             self.fixedColor.set(1)
160
161     # ---- snapshot & restore (for 'back' button)
162
163     def snapshot(self):
164         """ Return a dictionary which contains the data of all the objects
165             currently displayed on the board. """
166        
167         data = {}
168         data['status'] = copy(self.status)
169         data['marks'] = copy(self.marks)
170         data['labels'] = copy(self.labels)
171         data['wildcards'] = copy(self.wildcards)
172         data['selection'] = self.selection
173         data['currentcolor'] = self.currentColor
174         data['undostack'] = deepcopy(self.undostack)
175        
176         return data
177    
178     def restore(self, d):
179         """ Restore a board position from a 'snapshot' dictionary. """
180        
181         self.newPosition()
182         for x in d['status'].keys(): self.play(x, d['status'][x])
183         for x in d['marks'].keys(): self.placeMark(x, d['marks'][x])
184         for x in d['labels'].keys(): self.placeLabel(x, d['labels'][x][0], d['labels'][x][1])
185         for x,y in d['wildcards'].keys():
186             x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
187             self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
188                                                      tags=('wildcard','non-bg'))
189
190         if d['selection'] != ((1,1),(19,19)) and d['selection'][1] != (0,0):
191             p0 = self.getPixelCoord(d['selection'][0],1)
192             p1 = self.getPixelCoord((d['selection'][1][0]+1, d['selection'][1][1]+1),1)
193
194             self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50',
195                                   tags='selection')
196             self.tkraise('non-bg')
197             self.selection = d['selection']
198
199         self.currentColor = d['currentcolor']
200         self.undostack = deepcopy(d['undostack'])
201
202 # ---------------------------------------------------------------------------------------
203
204 class chooseDirectory(Dialog):
205     """ A wrapper tor the Tk chooseDirectory widget. """
206    
207     command = "tk_chooseDirectory"
208
209     def _fixresult(self, widget, result):
210         if result:
211             self.options["initialdir"] = result
212         self.directory = result
213         return result
214
215 def askdirectory(**options):
216     return apply(chooseDirectory, (), options).show()
217
218 # ---------------------------------------------------------------------------------------
219
220 class TextEditor:
221     """ A very simple text editor, based on the Tkinter ScrolledText widget.
222     You can perform very limited editing, and save the result to a file. """
223
224     def __init__(self, t = '', defpath='', font = None):
225
226         if font is None:
227             font = (StringVar(), IntVar(), StringVar())
228             font[0].set('Courier')
229             font[1].set(10)
230             font[2].set('')
231            
232         self.window = Toplevel()
233
234         self.window.protocol('WM_DELETE_WINDOW', self.quit)
235
236         self.text = ScrolledText(self.window, width=70, height=30,
237                                  font=(font[0].get(), font[1].get(), font[2].get()))
238         self.text.pack(fill=BOTH, expand=YES)
239         self.text.insert(END, t)
240
241         self.buttonFrame = Frame(self.window)
242         self.buttonFrame.pack(side=RIGHT)
243
244         Button(self.buttonFrame, text='Quit', command=self.quit).pack(side=RIGHT)
245         Button(self.buttonFrame, text='Save as', command=self.saveas).pack(side=RIGHT)
246
247         # self.window.tkraise()
248         self.window.focus_force()
249
250         if defpath:
251             self.defpath = defpath
252         else:
253             self.defpath = os.curdir
254            
255     def saveas(self):
256         f = tkFileDialog.asksaveasfilename(initialdir = self.defpath)
257         try:
258             file = open(f, 'w')
259             file.write(self.text.get('1.0', END))
260             file.close()
261         except IOError:
262             showwarning('IO Error', 'Cannot write to ' + f)
263
264     def quit(self):
265         self.window.destroy()
266
267
268
269 class ESR_TextEditor(TextEditor):
270     """The text editor which is used by the exportSearchResults function.
271     It adds a button to include the complete game list to the TextEditor."""
272
273     def __init__(self, master, style, t='', defpath='', font=None):
274         TextEditor.__init__(self, t, defpath, font)
275         self.master = master
276         self.style = style
277        
278         Button(self.buttonFrame, text='Include game list', command=self.includeGameList).pack(side=RIGHT)
279
280     def includeGameList(self):
281         if self.style == 'wiki':
282             self.text.insert(END, '\n\n!Game list\n\n' + join(self.master.gamelist.list.get(0, END), ' %%%\n'))
283         else:
284             self.text.insert(END, '\n\nGame list\n\n' + join(self.master.gamelist.list.get(0, END), '\n'))
285        
286
287 # ---------------------------------------------------------------------------------------
288
289 class ScrolledList(Frame):
290     """ A listbox with vertical and horizontal scrollbars. """
291    
292     def __init__(self, parent, **kw):
293         Frame.__init__(self, parent)
294
295         self.sbar = Scrollbar(self)
296         self.sbar1 = Scrollbar(self)
297
298         if not kw: kw = {}
299
300         for var, value in [('height', 12), ('width', 40), ('relief', SUNKEN),
301                            ('selectmode', SINGLE), ('takefocus', 1)]:
302             if not kw.has_key(var): kw[var] = value
303        
304         self.list = Listbox(self, kw)
305         self.sbar.config(command = self.list.yview)
306         self.sbar1.config(command = self.list.xview, orient='horizontal')
307         self.list.config(xscrollcommand = self.sbar1.set, yscrollcommand = self.sbar.set)
308         self.list.grid(row=0, column=0, sticky=NSEW)
309         self.sbar.grid(row=0, column=1, sticky=NSEW)
310         self.sbar1.grid(row=1, column=0, sticky=NSEW)
311
312         self.rowconfigure(0, weight=1)
313         self.columnconfigure(0, weight=1)
314
315         self.focus_force()
316        
317
318 # -------------------------------------------------------------------------------------
319
320 class GameList(ScrolledList):
321     """ This is a scrolled list which shows the game list. All the underlying data
322         is contained in self.DBlist, which is a list of dictionaries containing the
323         information for the single databases. self.DBlist[i] will contain the keys
324         'name': name (=path) of the database
325         'data': list of all games in the database.
326                 This is a list of tuples of the form
327                 (filename, namelistIndex, PB, PW, result, signature)
328         'current': a list of the indices of the games in data which are in the current
329                    game list
330         'results': for each game in current, the list of matches found in the previous
331                    search (If there are lots of matches, this list will consume a lot
332                    of memory. Maybe at some time I will get around to fix this ...)
333         'stringlist': A list of strings from entries in 'data' which are not yet
334                       displayed in the ScrolledList (to speed things up by inserting
335                       many entries at once into the ScrolledList)
336         """
337    
338     def __init__(self, parent, master, noGamesLabel, winPercLabel, gameinfo):
339         ScrolledList.__init__(self, parent)
340         self.list.config(width=52, height=10)
341         self.list.bind('<Button-1>', self.printGameInfo)
342
343         self.bind('<Up>', self.up)
344         self.bind('<Down>', self.down)
345         self.bind('<Prior>', self.pgup)
346         self.bind('<Next>', self.pgdown)
347         self.bind('<Return>', self.handleDoubleClick)
348         self.list.bind('<Double-1>', self.handleDoubleClick)
349         self.list.bind('<Button-3>', self.rightMouseButton)
350
351         self.master = master
352        
353         self.DBlist = []      # list of dicts
354
355         self.noGamesLabel = noGamesLabel
356         self.winPercLabel = winPercLabel
357         self.gameinfo = gameinfo
358        
359         self.Bwins = 0
360         self.Wwins = 0
361         self.Owins = 0 # others: Jigo, Void, Left unfinished, ? (Unknown)
362
363         self.appendClist = []
364
365
366     def rightMouseButton(self, event):
367        
368         index = self.list.nearest(event.y)
369         if index == -1: return
370         DBindex = 0
371         while index >= len(self.DBlist[DBindex]['current']):
372             index -= len(self.DBlist[DBindex]['current'])
373             DBindex += 1
374         if DBindex >= len(self.DBlist) or index > len(self.DBlist[DBindex]['current']): return
375            
376         filename = strip(os.path.join(self.DBlist[DBindex]['name'],
377                                       self.DBlist[DBindex]['data'][self.DBlist[DBindex]['current'][index]][0] + '.sgf'))
378
379         try:
380             file = open(filename)
381             sgf = file.read()
382             file.close()
383             c = Cursor(sgf)
384             rootNode = c.getRootNode()
385         except:
386             return
387
388         backup = copy(rootNode)
389
390         newRootNode = self.master.gameinfo(rootNode)
391         if backup != newRootNode:
392             c.updateRootNode(newRootNode)
393             try:
394                 file = open(filename, 'w')
395                 file.write(c.output())
396                 file.close()
397             except IOError:
398                 showwarning('I/O Error', 'Could not write to file ' + filename)
399
400
401     def handleDoubleClick(self, event):
402         """ This is called upon double-clicks."""
403        
404         index = self.list.curselection()
405         if index:
406             label = self.list.get(index)
407             self.master.openViewer(index, label)
408
409
410     def up(self, event):
411         if not self.list.curselection(): return
412         index = int(self.list.curselection()[0])
413         if index != 0:
414             self.list.select_clear(index)
415             self.list.select_set(index-1)
416             self.list.see(index-1)
417             self.printGameInfo(None, index-1)
418
419     def down(self, event):
420         if not self.list.curselection(): return
421         index = int(self.list.curselection()[0])
422         if index != self.list.size()-1:
423             self.list.select_clear(index)
424             self.list.select_set(index+1)
425             self.list.see(index+1)
426             self.printGameInfo(None, index+1)
427
428     def pgup(self, event):
429         if not self.list.curselection(): return
430         index = int(self.list.curselection()[0])
431         if index >= 10:
432             self.list.select_clear(index)
433             self.list.select_set(index-10)
434             self.list.see(index-10)
435             self.printGameInfo(None, index-10)
436         elif self.list.size():
437             self.list.select_clear(index)
438             self.list.select_set(0)
439             self.list.see(0)
440             self.printGameInfo(None, 0)
441
442     def pgdown(self, event):
443         if not self.list.curselection(): return
444         index = int(self.list.curselection()[0])
445         if index <= self.list.size()-10:
446             self.list.select_clear(index)
447             self.list.select_set(index+10)
448             self.list.see(index+10)
449             self.printGameInfo(None, index+10)
450         elif self.list.size():
451             self.list.select_clear(index)
452             self.list.select_set(self.list.size()-1)
453             self.list.see(END)
454             self.printGameInfo(None, self.list.size()-1)
455
456     def delete(self):
457         """ Clear the list. """
458
459         self.update()
460
461         for db in self.DBlist:
462             if not db['disabled']:
463                 db['current'] = array('L')
464                 db['results'] = []
465            
466         self.Bwins = 0
467         self.Wwins = 0
468         self.Owins = 0
469         self.update()
470
471         self.list.delete(0,END)
472         self.list.update_idletasks()
473
474     def append(self, i, d):
475         """ Append an entry to the gamelist. """
476         self.DBlist[i]['data'].append(d)
477
478         if not self.DBlist[i]['disabled']:
479
480             if d[4] == 'B': self.Bwins = self.Bwins + 1
481             elif d[4] == 'W': self.Wwins = self.Wwins + 1
482             else: self.Owins = self.Owins + 1
483        
484             s = d[0] + ': ' + d[3] + ' - ' + d[2] + ' (' + d[4] + ') ' # filename: PW-PB (RES)
485             self.DBlist[i]['stringlist'].append(s)
486
487
488     def appendC(self, db, i, res = ''):
489         """ Append an entry to self.current """
490        
491         db['current'].append(i)
492         db['results'].append(res)
493         self.appendClist.append((db['data'][i], res))
494
495
496     def reset(self):
497         """ Reset the list, s.t. it includes all the games from self.data. """
498         for db in self.DBlist:
499             if not db['disabled']:
500                 db['current'] = array('L', range(len(db['data'])))
501                 db['results'] = [''] * len(db['data'])
502                 self.appendClist += map(None, db['data'], db['results'])
503             else:
504                 db['current'] = array('L')
505                 db['results'] = []
506         self.list.delete(0, END)
507         self.Bwins, self.Wwins, self.Owins = 0, 0, 0
508         self.update()
509         self.clearGameInfo()
510        
511     def update(self):
512         """ Put the changes in data, current in effect in self.list. """
513        
514         if self.appendClist:         # display appendC'ed entries
515             
516             strl = []
517            
518             for d, res in self.appendClist:
519                 if d[4] == 'B': self.Bwins = self.Bwins + 1
520                 elif d[4] == 'W': self.Wwins = self.Wwins + 1
521                 else: self.Owins = self.Owins + 1
522        
523                 s = d[0] + ': ' + d[3] + ' - ' + d[2] + ' (' + d[4] + ') ' + res
524                 strl.append(s)
525             apply(self.list.insert, [END] + strl)
526
527             self.appendClist = []
528
529         for db in self.DBlist:
530             if db['stringlist']:                              # display append'ed entries
531                 apply(self.list.insert, [END] + db['stringlist'])
532
533                 db['current'].extend(array('L', range(len(db['data'])-len(db['stringlist']), len(db['data']))))
534             db['results'] = db['results'] + [''] * len(db['stringlist'])
535             db['stringlist'] = []
536
537         noOfG = self.noOfGames()
538         self.noGamesLabel.config(text = `noOfG` + ' games')
539
540         if noOfG:
541             Bperc = self.Bwins * 100.0 / noOfG
542             Wperc = self.Wwins * 100.0 / noOfG
543             self.winPercLabel.config(text='B: %1.1f%%, W: %1.1f%%' % (Bperc, Wperc))
544         else: self.winPercLabel.config(text='')
545
546     def noOfGames(self):
547         return reduce(lambda x,y:x+y, [ len(db['current']) for db in self.DBlist if not db['disabled'] ], 0)
548
549     def printGameInfo(self, event, index = -1):
550         """ Print game info of selected game. """
551
552         if index == -1:
553             index = self.list.nearest(event.y)
554
555         if index == -1:
556             return
557
558         DBindex = 0
559
560         while index >= len(self.DBlist[DBindex]['current']):
561             index -= len(self.DBlist[DBindex]['current'])
562             DBindex += 1
563
564         if DBindex >= len(self.DBlist) or index > len(self.DBlist[DBindex]['current']): return
565            
566         filename = strip(os.path.join(self.DBlist[DBindex]['name'],
567                                       self.DBlist[DBindex]['data'][self.DBlist[DBindex]['current'][index]][0] + '.sgf'))
568
569         try:
570             f = open(filename)
571             sgf = f.read()
572             f.close()
573            
574             node = Cursor(sgf).getRootNode()
575            
576         except:
577             return
578        
579         t = ''
580        
581         if node.has_key('PW'): t = t + node['PW'][0]
582         else: t = t + ' ?'
583         if node.has_key('WR'): t = t + ', ' + node['WR'][0]
584
585         t = t + ' - '
586
587         if node.has_key('PB'): t = t + node['PB'][0]
588         else: t = t + ' ?'
589         if node.has_key('BR'): t = t + ', ' + node['BR'][0]
590
591         if node.has_key('RE'): t = t + ', ' + node['RE'][0]
592         if node.has_key('KM'): t = t + ' (Komi ' + node['KM'][0] + ')'
593         if node.has_key('HA'): t = t + ' (Hcp ' + node['HA'][0] + ')'
594
595         t = t + '\n'
596
597         if node.has_key('EV'): t = t + node['EV'][0] + ', '
598         if node.has_key('RO'): t = t + node['RO'][0] + ', '
599         if node.has_key('DT'): t = t + node['DT'][0] + '\n'
600
601         if node.has_key('GC'):
602             gc = node['GC'][0]
603             gc = replace(gc, '\n\r', ' ')
604             gc = replace(gc, '\r\n', ' ')
605             gc = replace(gc, '\r', ' ')
606             gc = replace(gc, '\n', ' ')
607            
608             t = t + gc
609
610         self.gameinfo.config(state=NORMAL)
611         self.gameinfo.delete('1.0', END)
612         self.gameinfo.insert('1.0', t)
613         self.gameinfo.config(state=DISABLED)
614
615     def clearGameInfo(self):
616         self.gameinfo.config(state=NORMAL)
617         self.gameinfo.delete('1.0', END)
618         self.gameinfo.config(state=DISABLED)
619
620     def readDB(self, dbpath):
621         """ Read the database files containing the information for a pattern search. """
622
623         try:
624             posTable = array('L')
625             mainlistArr = array('B')
626             finalpos = array('B')
627             hashT = array('l')
628             hash = array('B')
629
630             filelist = [(mainlistArr, 'lists'), (posTable, 'posTable'),
631                         (finalpos, 'finalpos'),
632                         (hashT, 'hashT'), (hash, 'hash')]
633
634             for var, filename in filelist:
635                 file = open(os.path.join(dbpath, filename+'.db'), 'rb')
636                 while 1:
637                     try: var.fromfile(file, 1000000)
638                     except EOFError: break
639                 file.close()
640
641             return tuple([x[0] for x in filelist])
642                        
643         except IOError:
644             showwarning('Error', 'Database files not found: '+dbpath)
645             return None, None, None, None, None
646
647     def checkDuplicate(self, sig, pos, strict):                                   
648         """ Check if a game with Dyer signature sig and final position pos
649             occurs among the games in self.data. """
650        
651         duplist = []
652         for db in self.DBlist:
653             if db['disabled']: continue
654             for g in db['data']:
655                 if sig == g[5]:
656                     if not strict:
657                         duplist.append(os.path.join(db['name'], g[0]))
658                     else:
659                         mainlistArr, posTable, finalpos, hashT, hash = self.readDB(db['name'])
660                         if not finalpos: continue
661                         if pos == finalpos[150*g[1]:150*g[1]+100]:
662                             duplist.append(os.path.join(db['name'], g[0]))
663         return duplist
664
665
666 # ---------------------------------------------------------------------------------------
667
668 class PrevSearchesStack:
669
670     """ This class is a stack which contains the data of the previous searches,
671     s.t. one can return to the previous search with the back button. This is a
672     little bit tricky since we have to distinguish if we want to return
673     to the very last search (if after it something changed on the board)
674     or to the search before that. Thus the very last search is stored in
675     self.previous, and the rest in a list self.data. """
676
677     def __init__(self, maxLength, boardChanged):
678         self.data = []
679         self.previous = ()
680         self.maxLength = maxLength
681         self.boardChanged = boardChanged
682        
683     def append(self, s):
684         if self.previous:
685             if len(self.data) >= self.maxLength.get() > 0 :
686                 del self.data[0]
687             self.data.append(self.previous)
688         self.boardChanged.set(0)
689        
690         self.previous = s
691
692     def pop(self):
693         if not self.previous: return
694
695         if not self.boardChanged.get() or not self.previous[0]:
696             if not self.data:
697                 self.previous = ()
698             else:
699                 self.previous = self.data.pop()
700
701         return self.previous
702
703     def clear(self):
704         self.data = []
705         self.previous = ()
706
707 # ---------------------------------------------------------------------------------------
708
709 class Message(ScrolledText):
710     """ A ScrolledText widget which is usually DISABLED (i.e. the user cannot
711         enter any text), and which automatically scrolls down upon insertion. """
712
713     def __init__(self, window):
714         ScrolledText.__init__(self, window, height=8, width=75, relief=SUNKEN, wrap=WORD)
715         self.config(state=DISABLED)
716
717     def insert(self, pos, text):
718         self.config(state=NORMAL)
719         ScrolledText.insert(self, pos, text)
720         self.see(END)
721         self.config(state=DISABLED)
722
723     def delete(self, pos1, pos2):
724         self.config(state=NORMAL)
725         ScrolledText.delete(self, pos1, pos2)
726         self.config(state=DISABLED)
727
728 # ---------------------------------------------------------------------------------------
729
730 class ProgressBar(Canvas):
731     """ A simple progress bar. """
732    
733     def __init__(self, parent, width=390, font = None, colors = None):
734         if not font:
735             self.font = (StringVar(), IntVar(), StringVar())
736             self.font[0].set('Helvetica')
737             self.font[1].set(8)
738             self.font[2].set('')
739         else: self.font = font
740         if not colors:
741             self.colors = (StringVar(), StringVar())
742             self.colors[0].set('black')
743             self.colors[1].set('white')
744         else: self.colors = colors
745         self.width = width
746         Canvas.__init__(self, parent, height=15, width=width, highlightthickness=0,
747                         borderwidth=2, relief=RIDGE)
748
749         self.current, self.previous = -1, -1
750        
751     def redraw(self, percent):
752         self.delete(ALL)
753         x = int(self.width*percent+3)
754         self.create_rectangle(0,0,x,18, fill=self.colors[0].get(), outline='')
755         self.update()
756        
757
758     def clear(self):
759         self.delete(ALL)
760        
761     def write(self, text):
762         self.create_text(self.width-10,9, text=text, fill=self.colors[1].get(),
763                          font=(self.font[0].get(), self.font[1].get(), self.font[2].get()), anchor=E)
764        
765 # ---------------------------------------------------------------------------------------
766
767
768
769 class customMenus:
770
771     def __init__(self, master):
772         self.mainMenu = master.mainMenu
773         self.master = master
774         self.htmlpath = os.curdir
775         self.windowOpen = 0
776         self.path = os.curdir
777
778     def compare(self, entry1, entry2):
779         if entry1['name'] < entry2['name']: return -1
780         elif entry1['name'] == entry2['name']: return 0
781         elif entry1['name'] > entry2['name']: return 1
782
783     def buildMenus(self, reload=1, alternativePath = ''):
784         """
785         format of menuList entries:
786         { 'name' : name (which will be displayed in the menu,
787           'entries': [list of entries]
788           'subm' : [list of submenus]  }
789
790         format of entry:
791         { 'name' : name which will be displayed in the menu,
792           'file' : name of a html file which will be displayed upon clicking
793                    this entry (or empty),
794           'gisearch' : parameters of a game info search to be done when
795                        this is chosen,
796           'psearch' : pattern and options for a pattern search }
797
798         format of submenu: {} as before
799         """
800
801         if reload:
802
803             try:
804                 file = open(os.path.join(self.path, 'menus.def'))     
805                 self.customMenuList = cPickle.load(file)
806                 file.close()
807             except IOError:
808                 if alternativePath:
809                     try:
810                         file = open(os.path.join(alternativePath, 'menus.def'))     
811                         self.customMenuList = cPickle.load(file)
812                         file.close()
813                     except IOError:
814                         self.customMenuList = []
815                 else:
816                     self.customMenuList = []
817                    
818         self.noOfMenus = len(self.customMenuList)
819        
820         # build menus
821
822         self.customMenuCommands = []
823
824         self.mmIndex = 3
825
826         self.customMenuList.sort(self.compare)
827
828         for item in self.customMenuList:
829             m = Menu(self.mainMenu)
830             self.mainMenu.insert_cascade(self.mmIndex, menu=m, label = item['name'])
831             self.mmIndex += 1
832    
833             self.addMenu(item['subm'], m)
834
835             item['entries'].sort(self.compare)
836            
837             for e in item['entries']:
838        
839                 m.add_command(label=e['name'],