PyScore

001: # -*- python -*-
002: ###### filttab.py ###### python package PyScore.treemodel module filttab ######
003: 
004: # PyScore
005: # a race scoring programme
006: # written by Matt Draisey
007: # 2004 April 6
008: 
009: reloadable=True
010: reloadables=[]
011: 
012: ###### filttab.py ###### python package PyScore.treemodel module filttab ######
013: 
014: import itertools
015: import gtk
016: 
017: # FiltTable and FiltCombo
018: 
019: def canonical(s):
020:     return s.lower().expandtabs(1)
021: 
022: # FiltTable
023: 
024: class FiltTable(gtk.ListStore):
025:     """
026:     List of tuples of strings as a treemodel filtered by input string.
027:     """
028: 
029:     def __init__(self,rows=[],ncols=None,wcols=None,xcols=[]):
030:         if ncols==None and wcols==None:
031:             ncols=1
032:             wcols=[False]
033:         elif ncols==None:
034:             assert isinstance(wcols,list)
035:             ncols=len(wcols)
036:         elif wcols==None:
037:             assert isinstance(ncols,int) and ncols>=1
038:             wcols=[False]*ncols
039:         else:
040:             assert isinstance(ncols,int) and ncols>=1
041:             assert isinstance(wcols,list) and len(wcols)==ncols
042:         assert isinstance(xcols,list)
043:         columns=(str,)*ncols+tuple([t for (n,t) in xcols])
044:         gtk.ListStore.__init__(self,*columns)
045:         self.ncols=ncols
046:         self.wcols=wcols
047:         self.xcolnil=tuple([n for (n,t) in xcols])
048:         if rows:
049:             assert isinstance(rows[0],tuple)
050:             mcols=len(rows[0])
051:             assert mcols==ncols+len(xcols)
052:         else:
053:             mcols=ncols+len(xcols)
054:         self.clear()
055:         # forces sort order
056:         presorted=zip(itertools.count(),rows) # list(enumerate(rows))
057:         for s in presorted:
058:             assert isinstance(s[1],tuple) and len(s[1])==mcols
059:             self.append(s[1])
060:         self.history=[(
061:             "",
062:             0,
063:             self.filtered_nil()+[
064:                 ((0,),(0,s[1][0]<>""),[],s,True,canonical(s[1][0]))
065:                 for s in presorted
066:             ],
067:         )]
068:         # history (filter string, current column number, filtered rows)
069:         # filtered rows designed to be lexicographically comparable
070:         # filtered rows (
071:         #       category for sorting,
072:         #       quality of match for this column,
073:         #       [quality of match for previous columns]
074:         #       row for liststore
075:         #       exact fit so far
076:         #       as yet unmatched string for each record in this column
077:         # )
078:         # I have noticed a flaw:
079:         #       exact matches should match first
080:         #   however:
081:         #       initial matches earlier in list will match first
082: 
083:     def filtered_nil(self):
084:         return []
085: 
086:     def filter_next_column(self,char):
087:         assert isinstance(char,str) and len(char)==1
088:         (prev_filter,prev_column,prev_filtered)=self.history[-1]
089:         (new_filter,new_column)=(prev_filter+char,prev_column+1)
090:         if new_column<self.ncols:
091:             new_filtered=[
092:                 (
093:                     c,(0,s[1][new_column]<>""),[x]+xs,
094:                     s,q and not x[1],canonical(s[1][new_column])
095:                 )
096:                 for (c,x,xs,s,q,y) in prev_filtered
097:             ]
098:         else:
099:             self.clear()
100:             new_filtered=self.filtered_nil()
101:         self.history.append((new_filter,new_column,new_filtered))
102: 
103:     def filter_append(self,char):
104:         assert isinstance(char,str) and len(char)==1
105:         self.clear()
106:         (prev_filter,column,prev_filtered)=self.history[-1]
107:         (new_filter,new_filtered)=(prev_filter+char,self.filtered_nil())
108:         for (c,x,xs,s,q,y) in prev_filtered:
109:             i=y.find(char)+1
110:             if i:
111:                 self.append(s[1])
112:                 new_y=y[i:]
113:                 if self.wcols[column]:
114:                     quality=(len(new_y)-new_y.rfind(char),new_y<>"",)
115:                 else:
116:                     quality=(x[0]+i,new_y<>"",)
117:                 new_filtered.append((c,quality,xs,s,q and i==1,new_y))
118:         self.history.append((new_filter,column,new_filtered))
119: 
120:     def filter_comprehend(self,comp,inpt=None):
121:         comp=canonical(comp)
122:         (xfilter,xcolumn,xfiltered)=self.history[-1]
123:         displayedfilter=xfilter
124:         while not comp.startswith(xfilter):
125:             self.history.pop()
126:             (xfilter,xcolumn,xfiltered)=self.history[-1]
127:         for char in comp[len(xfilter):]:
128:             #if char in ".:;," or char==" " and xcolumn==0:
129:             if char in ".:;,":
130:                 self.filter_next_column(char)
131:                 (xfilter,xcolumn,xfiltered)=self.history[-1]
132:             else:
133:                 self.filter_append(char)
134:                 (xfilter,xcolumn,xfiltered)=self.history[-1]
135:                 displayedfilter=xfilter
136:         if displayedfilter<>xfilter:
137:             # xfilter out of sync with display
138:             gtk.ListStore.clear(self)
139:             for (c,x,xs,s,q,y) in xfiltered:
140:                 self.append(s[1])
141:         if inpt<>None:
142:             for i in range(len(self.history)-1,0,-1):
143:                 (xfilter,xcolumn,xfiltered)=self.history[i]
144:                 if len(xfilter)<=inpt:
145:                     break
146:         return xcolumn
147: 
148:     def filter_nonempty(self):
149:         return len(self.history[-1][2])
150: 
151:     def filtered_size(self):
152:         return len(self.history[-1][2])
153: 
154:     def exact_fit(self,upto=0):
155:         (xfilter,xcolumn,xfiltered)=self.history[-1]
156:         mustbeblank=range(xcolumn+1,upto)
157:         for (i,(c,x,xs,s,q,y)) in enumerate(xfiltered):
158:             if q and not x[1]:
159:                 for b in mustbeblank:
160:                     if s[1][b]: break
161:                 else:
162:                     return (i,)
163: 
164:     def best_fit(self):
165:         (xfilter,xcolumn,xfiltered)=self.history[-1]
166:         xref=zip(xfiltered,itertools.count())
167:         if xref:
168:             return (min(xref)[1],)
169:         else:
170:             return None
171: 
172:     def get_row_selection(self,indexpath,selector):
173:         if isinstance(indexpath,tuple) and len(indexpath)==1:
174:             (index,)=indexpath
175:         else:
176:             index=indexpath
177:         if isinstance(index,int) and 0<=index<self.filtered_size():
178:             (xfilter,xcolumn,xfiltered)=self.history[-1]
179:             (c,x,xs,s,q,y)=xfiltered[index]
180:             return selector(c,x,xs,s,q,y)
181:         else:
182:             return None
183: 
184:     def get_row(self,indexpath):
185:         return self.get_row_selection(indexpath,lambda c,x,xs,s,q,y: s[1])
186: 
187:     def get_zipped_row(self,indexpath):
188:         return self.get_row_selection(indexpath,lambda c,x,xs,s,q,y: s)
189: 
190:     def find_exact_row(self,target):
191:         (xfilter,xcolumn,xfiltered)=self.history[-1]
192:         for (i,(c,x,xs,s,q,y)) in enumerate(xfiltered):
193:             if s[1]==target:
194:                 return (i,)
195:         else:
196:             return None
197: 
198:     def find_match_row(self,target):
199:         (xfilter,xcolumn,xfiltered)=self.history[-1]
200:         for (i,(c,x,xs,s,q,y)) in enumerate(xfiltered):
201:             if target(s[1]):
202:                 return (i,)
203:         else:
204:             return None
205: 
206: # SplitColumns # slightly more complex than a re.split but more on-theme
207: 
208: class SplitColumns(object):
209:     """A simple incremental splitter."""
210: 
211:     def __init__(self,*p,**pp):
212:         if p:
213:             self.cols=p
214:             if "ncols" in pp:
215:                 self.ncols=pp["ncols"]
216:             else:
217:                 self.ncols=len(self.cols)
218:         else:
219:             if "ncols" in pp:
220:                 self.ncols=pp["ncols"]
221:             else:
222:                 self.ncols=1
223:             self.cols = (1,)*self.ncols
224:         assert len(self.cols)==self.ncols
225:         self.history=[("",(),"",0,0)]
226: 
227:     def split_comprehend(self,comp,inpt=None):
228:         (xcomp,cs,c,ls,l)=self.history[-1]
229:         col=len(cs)
230:         while not comp.startswith(xcomp):
231:             self.history.pop()
232:             (xcomp,cs,c,ls,l)=self.history[-1]
233:             col=len(cs)
234:         for char in comp[len(xcomp):]:
235:             if char in ".:;," and col<self.ncols-1:
236:                 l+=1
237:                 ls+=1
238:                 if l==self.cols[col]:
239:                     self.history.append((xcomp+char,cs+(c,),"",ls,0))
240:                 else:
241:                     self.history.append((xcomp+char,cs,c+char,ls,l))
242:             else:
243:                 self.history.append((xcomp+char,cs,c+char,ls,l))
244:             (xcomp,cs,c,ls,l)=self.history[-1]
245:             col=len(cs)
246:         if inpt<>None: # insertionpoint
247:             for i in range(len(self.history)-1,0,-1):
248:                 (xcomp,cs,c,ls,l)=self.history[i]
249:                 col=len(cs)
250:                 if len(xcomp)<=inpt:
251:                     break
252:         return (col,ls)
253: 
254:     def get_split_padded(self):
255:         (xcomp,cs,c,ls,l)=self.history[-1]
256:         cs+=(c,)
257:         if self.ncols>len(cs):
258:             return cs+("",)*(self.ncols-len(cs))
259:         else:
260:             return cs[:self.ncols]
261: 
262:     def get_split(self):
263:         (xcomp,cs,c,ls,l)=self.history[-1]
264:         cs+=(c,)
265:         if self.ncols>len(cs):
266:             return cs+("",)*(self.ncols-len(cs))
267:         else:
268:             return cs[:self.ncols]
269: 
270:     def get_split_chain(self):
271:         (xcomp,cs,c,ls,l)=self.history[-1]
272:         cs+=(c,)
273:         return cs
274: 
275: # FiltCombo
276: # This is included so that we may have a simple idempotent completion scheme.
277: # i.e. the initial entry returns any valid input unmodified.
278: 
279: class FiltCombo(FiltTable):
280:     """A filter and trivial parser rolled into one."""
281: 
282:     def __init__(self,*p,**pp):
283:         FiltTable.__init__(self,*p,**pp)
284:         self.split_columns=SplitColumns(ncols=self.ncols)
285:         (xfilter,xcolumn,xfiltered)=self.history[-1]
286:         # force trivial filter best to be idempotent
287:         (c,x,xs,s,q,y)=xfiltered[0]
288:         xfiltered[0]=((-1,),x,xs,s,q,y)
289: 
290:     def clear(self):
291:         gtk.ListStore.clear(self)
292:         self.append(("",)*self.ncols+self.xcolnil)
293: 
294:     def filtered_nil(self):
295:         return [((1,),(0,0),[],(-1,("",)*self.ncols+self.xcolnil),True,"")]
296: 
297:     def filter_comprehend(self,comp,inpt=None):
298:         self.split_columns.split_comprehend(comp)
299:         comp=canonical(comp)
300:         (xfilter,xcolumn,xfiltered)=self.history[-1]
301:         displayedfilter=xfilter
302:         while not comp.startswith(xfilter):
303:             self.history.pop()
304:             (xfilter,xcolumn,xfiltered)=self.history[-1]
305:         for char in comp[len(xfilter):]:
306:             if char in ".:;,":
307:                 self.filter_next_column(char)
308:                 (xfilter,xcolumn,xfiltered)=self.history[-1]
309:             else:
310:                 self.filter_append(char)
311:                 (xfilter,xcolumn,xfiltered)=self.history[-1]
312:                 displayedfilter=xfilter
313:         if displayedfilter<>xfilter:
314:             # xfilter out of sync with display
315:             gtk.ListStore.clear(self)
316:             for (c,x,xs,s,q,y) in xfiltered:
317:                 self.append(s[1])
318:         self.show_filter()
319:         if inpt<>None:
320:             for i in range(len(self.history)-1,0,-1):
321:                 (xfilter,xcolumn,xfiltered)=self.history[i]
322:                 if len(xfilter)<=inpt:
323:                     break
324:         return xcolumn
325: 
326:     def show_filter(self):
327:         row=self.split_columns.get_split_padded()
328:         iter=self.get_iter_first()
329:         for i in range(len(row)):
330:             self.set_value(iter,i,row[i])
331:         (xfilter,xcolumn,xfiltered)=self.history[-1]
332:         (c,x,xs,s,q,y)=xfiltered[0]
333:         # force trivial filter best to be idempotent
334:         if xfilter=="": # changed in place
335:             xfiltered[0]=((-1,),x,xs,(-1,row+self.xcolnil),q,y)
336:         else:
337:             xfiltered[0]=((+1,),x,xs,(-1,row+self.xcolnil),q,y)
338: 
339: # FiltChainTable
340: 
341: class FiltChainTable(FiltTable):
342:     """First link in a chain."""
343: 
344:     def __init__(self,rows=[]):
345:         FiltTable.__init__(self,[(s,) for s in rows])
346: 
347:     def filter_comprehend(self,comp,inpt=None):
348:         comp=canonical(comp)
349:         (xfilter,x,xfiltered)=self.history[-1]
350:         assert x==0
351:         displayedfilter=xfilter
352:         while not comp.startswith(xfilter):
353:             self.history.pop()
354:             (xfilter,x,xfiltered)=self.history[-1]
355:             assert x==0
356:         for char in comp[len(xfilter):]:
357:             if char in " .:;,":
358:                 xchain=(xfilter,comp[len(xfilter)+1:])
359:                 break
360:             if char.isdigit():
361:                 xchain=(xfilter,comp[len(xfilter):])
362:                 break
363:             # filter ops sync display with xfilter
364:             self.filter_append(char)
365:             (xfilter,x,xfiltered)=self.history[-1]
366:             assert x==0
367:             displayedfilter=xfilter
368:         else:
369:             xchain=(xfilter,)
370:         if displayedfilter<>xfilter:
371:             # xfilter out of sync with display
372:             gtk.ListStore.clear(self)
373:             for (c,x,xs,s,q,y) in xfiltered:
374:                 self.append(s[1])
375:         return xchain
376: 
377: ###### filttab.py ###### python package PyScore.treemodel module filttab ######