root/05/release-0.5h/kombilo.py

Revision 44, 255.1 kB (checked in by ug, 5 years ago)

Exception handling for webbrowser.open

Line 
1#! /usr/bin/env python
2# File: kombilo.py
3
4##   Copyright (C) 2001-4 Ulrich Goertz (u@g0ertz.de)
5
6##   Kombilo 0.5h 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
28# from tkFileDialog import askdirectory
29from tkCommonDialog import Dialog
30from tkSimpleDialog import askstring
31
32try:
33    import Pmw
34except:
35    root=Tk()
36    t = Text(root, wrap=WORD, width=70, height=12)
37    t.pack()
38    t.insert(END, 'Kombilo 0.5h\n\n The Python Megawidgets (Pmw) library was not found. It is needed by Kombilo, ' + \
39             'so you have to install it before you can use Kombilo. See the Kombilo tutorial at ' + \
40             'http://www.u-go.net/kombilo/tutorial#installation or the Pmw web site at ' + \
41             'http://pmw.sourceforge.net/ for more information.')
42    root.mainloop()
43    sys.exit()
44
45try: import Image
46except: pass
47
48import encodings.utf_8
49import time
50import os
51import sys
52import cPickle
53from copy import copy, deepcopy
54from string import split, find, join, strip, replace, digits, maketrans, translate, lower
55import glob
56import re
57from array import *
58import webbrowser
59
60from board1 import *
61import v
62
63try:
64    from sgfpars import Node, Cursor, SGFError, SGFescape
65except:
66    from sgfparser import Node, Cursor, SGFError, SGFescape
67   
68try:
69    import matchC
70    CimportSucceeded = 1
71except:
72    CimportSucceeded = 0
73
74#---------------------------------------------------------------------------------------
75
76class BoardWC(Board):
77    """ Board with support for wildcards and selection
78        of search-relevant region. Furthermore, snapshot returns a dictionary which
79        describes the current board position. It can then be restored with restore."""
80
81   
82    def __init__(self, master, boardSize, canvasSize, fuzzy, labelFontsize, fixedColor, smartFixedColor,
83                 boardImg, blackImg, whiteImg):
84        Board.__init__(self, master, boardSize, canvasSize, fuzzy, labelFontsize, 1, None,
85                       boardImg, blackImg, whiteImg)
86
87        self.wildcards = {}
88
89        self.selection = ((1,1),(19,19))
90
91        self.fixedColor = fixedColor
92        self.smartFixedColor = smartFixedColor
93
94        self.boundM2_1 = self.bind('<M2-Button-1>', self.selStart)
95        self.boundM2_1m = self.bind('<M2-B1-Motion>', self.selDrag)
96        self.bound3 = self.bind('<Button-3>', self.selStart)
97        self.bound3m = self.bind('<B3-Motion>', self.selDrag)
98        self.bounds1 = self.bind('<Shift-1>', self.wildcard)
99
100        self.invertSelection = IntVar()
101
102
103    def resize(self, event = None):
104        """ Resize the board. Take care of wildcards and selection here. """
105       
106        Board.resize(self, event)
107        for x,y in self.wildcards.keys():
108            x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
109            if self.canvasSize[1]<=7: margin = 5
110            else: margin = 4
111            self.wildcards[(x,y)] = self.create_oval(x1+margin, x2+margin, y1-margin, y2-margin, fill = 'green',
112                                                     tags=('wildcard','non-bg'))
113
114        self.delete('selection')
115        if self.selection != ((1,1),(19,19)) and self.selection[1] != (0,0):
116            p0 = self.getPixelCoord(self.selection[0],1)
117            p1 = self.getPixelCoord((self.selection[1][0]+1, self.selection[1][1]+1), 1)
118            min = self.getPixelCoord((1,1), 1)[0]+1
119            max = self.getPixelCoord((self.boardSize+1,self.boardSize+1),1)[1]-1
120            if self.canvasSize[1] <= 7:
121                self.create_rectangle(p0[0], p0[1], p1[0], p1[1],
122                                      tags=('selection', 'non-bg'))
123            elif self.invertSelection.get():
124                self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50', outline='',
125                                      tags='selection')
126            else:
127                if p0[1] > min:
128                    self.create_rectangle(min, min, max, p0[1], fill='brown', stipple='gray50', outline='',
129                                          tags='selection')
130                if p0[0] > min and p0[1] < max:
131                    self.create_rectangle(min, p0[1], p0[0], max, fill='brown', stipple='gray50', outline='',
132                                          tags='selection')
133                if p1[1] < max:
134                    self.create_rectangle(p0[0], p1[1], p1[0], max, fill='brown', stipple='gray50', outline='',
135                                          tags='selection')
136                if p1[0] < max and p0[1] < max:
137                    self.create_rectangle(p1[0], p0[1], max, max, fill='brown', stipple='gray50', outline='',
138                                          tags='selection')
139            self.tkraise('non-bg')
140           
141        self.update_idletasks()
142
143
144    def wildcard(self, event):
145        """ Place/delete a wildcard at position of click. """
146       
147        x, y = self.getBoardCoord((event.x, event.y), 1)
148        if not x*y or self.status.has_key((x,y)): return
149
150        if self.wildcards.has_key((x,y)):
151            self.delete(self.wildcards[(x,y)])
152            del self.wildcards[(x,y)]
153        else:
154            x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
155            if self.canvasSize[1]<=7: margin = 5
156            else: margin = 4
157            self.wildcards[(x,y)] = self.create_oval(x1+margin, x2+margin, y1-margin, y2-margin, fill = 'green',
158                                                     tags=('wildcard','non-bg'))
159            self.tkraise('label')
160        self.changed.set(1)       
161
162
163    def delWildcards(self):
164        """ Delete all wildcards. """
165       
166        if self.wildcards: self.changed.set(1)
167        self.delete('wildcard')
168        self.wildcards = {}
169
170
171    def placeLabel(self, pos, type, text=None, color=None):
172        """ Place a label; take care of wildcards at same position. """
173       
174        if self.wildcards.has_key(pos): override = ('black', '')
175        else: override = None
176
177        Board.placeLabel(self, pos, type, text, color, override)
178
179                             
180    # ---- selection of search-relevant section -----------------------------------
181
182    def selStart(self, event):
183        """ React to right-click. """
184        self.delete('selection')
185        x, y = self.getBoardCoord((event.x, event.y), 1)
186        x = max(x, 1)
187        y = max(y, 1)
188        self.selection = ((x,y), (0,0))
189        if self.smartFixedColor.get():
190            self.fixedColor.set(1)
191        self.changed.set(1)
192
193
194    def selDrag(self, event):
195        """ React to right-mouse-key-drag. """
196        pos = self.getBoardCoord((event.x, event.y), 1)
197        if pos[0] >= self.selection[0][0] and pos[1] >= self.selection[0][1]:
198            self.setSelection(self.selection[0], pos)
199           
200
201    def setSelection(self, pos0, pos1):
202        self.selection = (pos0, pos1)
203        self.delete('selection')
204        p0 = self.getPixelCoord(pos0,1)
205        p1 = self.getPixelCoord((pos1[0]+1, pos1[1]+1), 1)
206        min = self.getPixelCoord((1,1), 1)[0]+1
207        max = self.getPixelCoord((self.boardSize+1,self.boardSize+1),1)[1]-1
208        if self.canvasSize[1] <= 7:
209            self.create_rectangle(p0[0], p0[1], p1[0], p1[1],
210                                  tags=('selection', 'non-bg'))
211        elif self.invertSelection.get():
212            self.create_rectangle(p0[0], p0[1], p1[0], p1[1], fill='brown', stipple='gray50', outline='',
213                                  tags='selection')
214        else:
215            if p0[1] > min:
216                self.create_rectangle(min, min, max, p0[1], fill='brown', stipple='gray50', outline='',
217                                      tags='selection')
218            if p0[0] > min and p0[1] < max:
219                self.create_rectangle(min, p0[1], p0[0], max, fill='brown', stipple='gray50', outline='',
220                                      tags='selection')
221            if p1[1] < max:
222                self.create_rectangle(p0[0], p1[1], p1[0], max, fill='brown', stipple='gray50', outline='',
223                                      tags='selection')
224            if p1[0] < max and p0[1] < max:
225                self.create_rectangle(p1[0], p0[1], max, max, fill='brown', stipple='gray50', outline='',
226                                      tags='selection')
227           
228        self.tkraise('non-bg')
229       
230        if self.smartFixedColor.get():
231            if self.selection == ((1,1), (19,19)):
232                self.fixedColor.set(1)
233            else:
234                self.fixedColor.set(0)
235               
236       
237    def newPosition(self):
238        """ Clear board, selection. """
239        self.delete('selection')
240        self.clear()
241        self.delLabels()
242        self.delMarks()
243        self.delWildcards()
244        self.selection = ((1,1),(19,19))
245
246        if self.smartFixedColor.get():
247            self.fixedColor.set(1)
248
249
250    # ---- snapshot & restore (for 'back' button)
251
252    def snapshot(self):
253        """ Return a dictionary which contains the data of all the objects
254            currently displayed on the board, which are not stored in the SGF file.
255            This means, at the moment: wildcards, and selection. """
256       
257        data = {}
258        data['wildcards'] = copy(self.wildcards)
259        data['selection'] = self.selection
260        return data
261
262   
263    def restore(self, d):
264        """ Restore the data from a 'snapshot' dictionary. """
265       
266        for x,y in d['wildcards'].keys():
267            x1, x2, y1, y2 = self.getPixelCoord((x,y),1)
268            self.wildcards[(x,y)] = self.create_oval(x1+4, x2+4, y1-4, y2-4, fill = 'green',
269                                                     tags=('wildcard','non-bg'))
270
271        if d['selection'] != ((1,1),(19,19)) and d['selection'][1] != (0,0):
272            self.setSelection(d['selection'][0], d['selection'][1])
273
274
275# ---------------------------------------------------------------------------------------
276
277class chooseDirectory(Dialog):
278    """ A wrapper tor the Tk chooseDirectory widget. """
279   
280    command = "tk_chooseDirectory"
281
282    def _fixresult(self, widget, result):
283        if result:
284            self.options["initialdir"] = result
285        self.directory = result
286        return result
287
288def askdirectory(**options):
289    return apply(chooseDirectory, (), options).show()
290
291# ---------------------------------------------------------------------------------------
292
293class TextEditor:
294    """ A very simple text editor, based on the Tkinter ScrolledText widget.
295    You can perform very limited editing, and save the result to a file. """
296
297    def __init__(self, t = '', defpath='', font = None):
298
299        if font is None:
300            font = (StringVar(), IntVar(), StringVar())
301            font[0].set('Courier')
302            font[1].set(10)
303            font[2].set('')
304           
305        self.window = Toplevel()
306
307        self.window.protocol('WM_DELETE_WINDOW', self.quit)
308
309        self.text = ScrolledText(self.window, width=70, height=30,
310                                 font=(font[0].get(), font[1].get(), font[2].get()))
311        self.text.pack(side=BOTTOM, fill=BOTH, expand=YES)
312        self.text.insert(END, t)
313
314        self.buttonFrame = Frame(self.window)
315        self.buttonFrame.pack(side=TOP, expand=NO, fill=X)
316
317        Button(self.buttonFrame, text='Quit', command=self.quit).pack(side=RIGHT)
318        Button(self.buttonFrame, text='Save as', command=self.saveas).pack(side=RIGHT)
319
320        # self.window.tkraise()
321        self.window.focus_force()
322
323        if defpath:
324            self.defpath = defpath
325        else:
326            self.defpath = os.curdir
327           
328    def saveas(self):
329        f = tkFileDialog.asksaveasfilename(initialdir = self.defpath)
330        if not f: return
331        try:
332            file = open(f, 'w')
333            file.write(self.text.get('1.0', END).encode('utf-8', 'ignore'))
334            file.close()
335        except IOError:
336            showwarning('IO Error', 'Cannot write to ' + f)
337
338    def quit(self):
339        self.window.destroy()
340
341
342class ESR_TextEditor(TextEditor):
343    """The text editor which is used by the exportSearchResults function.
344    It adds a button to include the complete game list to the TextEditor."""
345
346
347    def __init__(self, master, style, t='', defpath='', font=None):
348        TextEditor.__init__(self, t, defpath, font)
349        self.mster = master
350        self.style = style
351       
352        Button(self.buttonFrame, text='Include game list', command=self.includeGameList).pack(side=LEFT)
353
354
355    def includeGameList(self):
356        if self.style: # wiki
357            self.text.insert(END, '\n\n!Game list\n\n' + join(self.mster.gamelist.list.get(0, END), ' %%%\n'))
358        else:
359            self.text.insert(END, '\n\nGame list\n\n' + join(self.mster.gamelist.list.get(0, END), '\n'))
360       
361
362# -------------------------------------------------------------------------------------
363
364class DataWindow(v.DataWindow):
365
366    def __init__(self, master):
367
368        v.DataWindow.__init__(self, master)
369
370        self.prevSF = Pmw.ScrolledFrame(self.prevSearchF, usehullsize=1, hull_width=300, hull_height=135,
371                                        hscrollmode='static', vscrollmode='none', vertflex='elastic')
372        self.prevSF.pack(expand=YES, fill=X)
373        self.prevSV = IntVar()
374        self.prevSV.set(1)
375
376        b1 = Checkbutton(self.toolbarF, text = 'History', variable = self.prevSV,
377                         command = self.togglePrevSearches, indicatoron=0)
378        b1.grid(row=0, column=6)
379
380        self.win.setnaturalsize()
381
382       
383    def initPanes(self):
384        """ Create the panes in the data window. """
385
386        self.toolbarF = self.win.add(name='toolb', min=28, max=28)
387        self.filelistF = self.win.add(name='filel', min=0.01, max=1.0, size=65)
388        self.gamelistF = self.win.add(name='gamel', min=0.01, max=1.0, size=65)
389        self.gameinfoF = self.win.add(name='gamei', min=0.01, max=1.0, size=75)
390        self.editToolsF = self.win.add(name='editt', min=1, max=30, size=30) 
391        self.gametreeF = self.win.add(name='gamet', min=0.01, max=1.0, size=90) 
392        self.commentsF = self.win.add(name='comm', min=0.01, max=1.0, size=80) 
393        self.prevSearchF = self.win.add('prse', min=1, max=1.0, size=135)
394
395
396    def get_geometry(self):
397        """ Return a list of current sizes of the panes. """
398       
399        l = [ self.filelistV.get(), self.win._size['filel'],
400              self.gamelistV.get(), self.win._size['gamel'],
401              self.gameinfoV.get(), self.win._size['gamei'],
402              self.editToolsV.get(), self.win._size['editt'],
403              self.gametreeV.get(), self.win._size['gamet'],
404              self.commentsV.get(), self.win._size['comm'],
405              self.prevSV.get(), self.win._size['prse']]
406        l1 = [ `x` for x in l ]
407        l1.append(self.window.geometry())
408
409        return join(l1, '|%')
410       
411    def set_geometry(self, s):
412        """ Reset the sizes of the panes to the given ones. """
413       
414        l = split(s, '|%')
415       
416        l1 = [ self.filelistV, 
417               self.gamelistV, 
418               self.gameinfoV, 
419               self.editToolsV,
420               self.gametreeV, 
421               self.commentsV,
422               self.prevSV ]
423               
424        for i in range(len(l)/2):
425            l1[i].set(int(l[2*i]))
426            if int(l[2*i]):
427                self.win.configurepane(i+1, min=10, max=1.0, size = int(l[2*i+1]))
428            else:
429                self.win.configurepane(i+1, min=0.0, max=0.0, size = 0.0)
430        self.win.updatelayout()
431        self.window.geometry(l[-1])
432
433
434    def gamelistRelease(self, event):
435        index1, index2 = v.DataWindow.gamelistRelease(self, event)
436        if index1:
437            self.mster.prevSearches.exchangeGames(self.mster.cursor, index1, index2)
438       
439
440    def togglePrevSearches(self):
441        if self.prevSV.get():
442            s = self.window.geometry()
443            x1, x2, x3 = split(s, '+')
444            x, y = split(x1, 'x')
445            y = `int(y)+135`
446            self.window.geometry('%sx%s+%s+%s' % (x, y, x2, x3))
447            self.win.configurepane(7, min=10, max=1.0, size=135)
448        else:
449            s = self.window.geometry()
450            x1, x2, x3 = split(s, '+')
451            x, y = split(x1, 'x')
452            y = `int(y)-self.win._size['prse']`
453            self.window.geometry('%sx%s+%s+%s' % (x, y, x2, x3))
454            self.win.configurepane(7, min=0.0, max=0.0, size=0.0)
455        self.win.updatelayout()
456       
457
458# -------------------------------------------------------------------------------------
459
460class GameList(v.ScrolledList):
461    """ This is a scrolled list which shows the game list. All the underlying data
462        is contained in self.DBlist, which is a list of dictionaries containing the
463        information for the single databases. self.DBlist[i] will contain the keys
464        'name': name of the database, i.e. path to the *.db files
465        'sgfpath': path to the SGF files
466        'data': list of all games in the database.
467                This is a list of tuples of the form
468                (filename, namelistIndex, PB, PW, result, signature)
469        'current': a list of the indices of the games in data which are in the current
470                   game list
471        'results': for each game in current, the list of matches found in the previous
472                   search (If there are lots of matches, this list will consume a lot
473                   of memory. Maybe at some time I will get around to fix this ...)
474        'numNewGames': number of games which were "append"ed, but not yet inserted into
475                       the list (by addAppendedGames)
476        """
477   
478    def __init__(self, parent, master, noGamesLabel, winPercLabel, gameinfo):
479        v.ScrolledList.__init__(self, parent)
480        self.list.config(width=52, height=6) 
481
482        self.onSelectionChange = self.printGameInfo
483        self.list.bind('<Button-1>', self.onSelectionChange)
484
485        self.bind('<Up>', self.up)
486        self.bind('<Down>', self.down)
487        self.bind('<Prior>', self.pgup)
488        self.bind('<Next>', self.pgdown)
489        self.bind('<Return>', self.handleDoubleClick)
490        self.list.bind('<Double-1>', self.handleDoubleClick)
491        self.list.bind('<Shift-1>', self.handleShiftClick)
492        self.list.bind('<Button-3>', self.rightMouseButton)
493
494        self.mster = master
495       
496        self.DBlist = []      # list of dicts
497
498        self.noGamesLabel = noGamesLabel
499        self.winPercLabel = winPercLabel
500        self.gameinfo = gameinfo
501       
502        self.Bwins = 0
503        self.Wwins = 0
504        self.Owins = 0 # others: Jigo, Void, Left unfinished, ? (Unknown)
505
506        self.appendClist = []
507
508        self.references = {}
509
510        self.sort = None
511        self.gameIndex = []
512
513        self.showFilename = 1
514        self.showDate = 0
515
516
517    def rightMouseButton(self, event):
518       
519        index = self.list.nearest(event.y)
520        if index == -1: return
521
522        DBindex, index = self.getIndex(index)
523        if DBindex == -1: return
524
525        # print self.DBlist[DBindex]['data'][self.DBlist[DBindex]['current'][index]][5]
526        # return
527
528        f1 = strip(os.path.join(self.DBlist[DBindex]['sgfpath'],
529                                self.DBlist[DBindex]['data'][self.DBlist[DBindex]['current'][index]][0]))
530       
531        if find(f1, '[') != -1:
532            f1, f2 = split(f1, '[')
533            gameNumber = int(strip(f2)[:-1])
534        else:
535            gameNumber = 0
536
537        filename = getFilename(f1)
538
539        try:
540            file = open(filename)
541            sgf = file.read()
542            file.close()
543            c = Cursor(sgf, 1)
544            rootNode = c.getRootNode(gameNumber)
545        except IOError:
546            showwarning('Error', 'I/O Error')
547            return
548        except SGFError:
549            showwarning('Error', 'SGF error')
550            return
551       
552        backup = copy(rootNode)
553
554        newRootNode = self.mster.gameinfo(rootNode)
555        if backup != newRootNode:
556            c.updateRootNode(newRootNode, gameNumber)
557            try:
558                s = c.output()
559                file = open(filename, 'w')
560                file.write(s)
561                file.close()
562            except IOError:
563                showwarning('I/O Error', 'Could not write to file ' + filename)
564
565
566    def handleDoubleClick(self, event):
567        """ This is called upon double-clicks."""
568       
569        index = self.list.curselection()
570        if index:
571            label = self.list.get(index)
572            self.mster.openViewer(index, label)
573
574
575    def handleShiftClick(self, event):
576        index = self.list.nearest(event.y)
577        index1 = self.list.curselection()
578        if index1: self.list.select_clear(index1[0])
579        self.list.select_set(index)