PyScore

001: # -*- python -*-
002: ## entriesmod.py ## python package PyScore.timesheet module entries.modeller ##
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: ## entriesmod.py ## python package PyScore.timesheet module entries.modeller ##
013: 
014: import bisect
015: from timesheet import Entries,entriesview as view
016: 
017: ##### ##### private entry manipulation subroutines ##### #####
018: 
019: def init_history():
020:     assert not Entries.curpath
021:     do_new(next_available_number(0))
022: 
023: def delete_clicked():
024:     """Delete current entry and open a virgin entry."""
025:     assert Entries.curpath
026:     do_update()
027:     do_new(do_close(deletion=True))
028: 
029: def enter_clicked():
030:     """Close current entry and open a virgin entry."""
031:     assert Entries.curpath
032:     do_update()
033:     do_new(next_available_number(do_close(deletion=not nonempty_entry())))
034: 
035: def cursor_change():
036:     """Moving the entry cursor."""
037:     assert Entries.curpath
038:     oldr=Entries.curpath
039:     (r,c)=view.listview.get_cursor()
040:     assert isinstance(r,tuple) and len(r)==1
041:     if r<>oldr:
042:         do_update()
043:         deletion=not nonempty_entry()
044:         do_close(deletion)
045:         if deletion and r>oldr:
046:             (k,)=r
047:             r=(k-1,)
048:         do_open(r)
049:     return (Entries.curpath,c)
050: 
051: def on_do(doing):
052:     """The undo/redo actions."""
053:     import boats
054: 
055:     assert Entries.curpath
056:     do_update()
057:     lastnumber=do_close(deletion=not nonempty_entry())
058:     context=doing()
059:     if "openpath" in context:
060:         do_open(context["openpath"])
061:         view.listview.set_cursor(Entries.curpath)
062:         view.listview.grab_focus()
063:     else:
064:         if "lastnumber" in context:
065:             do_new(next_available_number(context["lastnumber"]))
066:         elif "newnumber" in context:
067:             do_new(context["newnumber"])
068:         else:
069:             do_new(next_available_number(0))
070:         view.listview.set_cursor(Entries.curpath)
071:         boats.view.box.grab_focus()
072: 
073: ##### do with sufficent history to implement an undo stack #####
074: 
075: def transaction_tag():
076:     assert Entries.curpath
077:     #currenti=Entries.model.get_iter(Entries.curpath)
078:     #row=[Entries.model.get_value(currenti,f) for f in range(0,11)]
079:     #return (Entries.curpath,tuple(row))
080:     return (Entries.curpath,tuple(Entries.model[Entries.curpath]))
081: 
082: def do_new(newnumber):
083:     import boats
084: 
085:     assert not Entries.curpath
086:     assert isinstance(newnumber,int)
087:     def redo(*p,**pp):
088:         """Redo: create a new entry"""
089:         assert not Entries.curpath
090:         new_entry(newnumber)
091:         view.listview.set_cursor(Entries.curpath)
092:         boats.view.box.grab_focus()
093:     def undo(*p,**pp):
094:         """Undo: forget a created entry"""
095:         assert Entries.curpath
096:         close_entry(deletion=True)
097:         return {"newnumber":newnumber}
098:     redo()
099:     Entries.history.truncate(tag=transaction_tag())
100:     Entries.history.append(undo=undo,redo=redo)
101: 
102: def do_open(openpath):
103:     assert not Entries.curpath
104:     assert isinstance(openpath,tuple) and len(openpath)==1
105:     assert isinstance(openpath[0],int)
106:     def redo(*p,**pp):
107:         """Redo: open an existing entry"""
108:         assert not Entries.curpath
109:         open_entry(openpath)
110:         view.listview.set_cursor(Entries.curpath)
111:         view.listview.grab_focus()
112:     def undo(*p,**pp):
113:         """Undo: reclose an opened entry"""
114:         assert Entries.curpath
115:         close_entry(deletion=False)
116:         return {"openpath":openpath}
117:     redo()
118:     Entries.history.truncate(tag=transaction_tag())
119:     Entries.history.append(undo=undo,redo=redo)
120: 
121: def do_update():
122:     assert Entries.curpath
123:     (beforepath,beforerow)=Entries.history.transaction()
124:     (afterpath,afterrow)=transaction_tag()
125:     def redo(*p,**pp):
126:         """Redo: update an entry to new values"""
127:         assert Entries.curpath==beforepath
128:         Entries.model[beforepath]=afterrow
129:         move_entry(beforepath,afterpath)
130:         Entries.curpath=afterpath
131:     def undo(*p,**pp):
132:         """Undo: revert an entry to old values"""
133:         assert Entries.curpath==afterpath
134:         Entries.model[afterpath]=beforerow
135:         move_entry(afterpath,beforepath)
136:         Entries.curpath=beforepath
137:     Entries.history.append(undo=undo,redo=redo)
138: 
139: def do_close(deletion):
140:     assert Entries.curpath
141:     (closepath,closerow)=transaction_tag()
142:     closenumber=closerow[0]
143:     if deletion:
144:         closerow=(closerow[0],-1,"","","","",False,False,False,False,False,False,"","","","","")
145:         def redo(*p,**pp):
146:             """Redo: close and delete an entry"""
147:             close_entry(deletion=True)
148:             return {"newnumber":closenumber}
149:         def undo(*p,**pp):
150:             """Undo: recreate a deleted entry"""
151:             assert not Entries.curpath
152:             new_entry(closenumber)
153:             move_entry(Entries.curpath,closepath)
154:     else:
155:         def redo(*p,**pp):
156:             """Redo: close an entry"""
157:             close_entry(deletion=False)
158:             return {"openpath":closepath}
159:         def undo(*p,**pp):
160:             """Undo: reopen a closed entry"""
161:             assert not Entries.curpath
162:             open_entry(closepath)
163:     Entries.history.append(undo=undo,redo=redo)
164:     (transpath,transrow)=Entries.history.transaction()
165:     if (transpath,transrow)==(closepath,closerow)\
166:     or transrow[1:]==(-1,"","","","",False,False,False,False,False,False,"","","","","")==closerow[1:]:
167:         Entries.history.rollback()
168:     else:
169:         Entries.history.commit()
170:     lastnumber=close_entry(deletion)
171:     return lastnumber
172: 
173: ##### ##### ##### ##### new/open and close ##### ##### ##### #####
174: 
175: class NumberBisectAdapter(object):
176:     def __getitem__(self,k):
177:         if not isinstance(k,int): raise TypeError
178:         return Entries.model.get_value(Entries.model.get_iter((k,)),0)
179: entries_n=NumberBisectAdapter()
180: 
181: def next_available_number(lastnumber=0):
182:     """Figure out the next available entry number."""
183:     import numbers
184: 
185:     assert isinstance(lastnumber,int)
186:     return numbers.controller.default_number(lastnumber+1)
187: 
188: def new_entry(newnumber):
189:     """Stamp out a new row in the entry list."""
190:     import numbers,boats,finishes,classes
191: 
192:     assert not Entries.curpath
193:     assert isinstance(newnumber,int)
194:     n=newnumber
195:     k=bisect.bisect_right(entries_n,n,0,Entries.model.iter_n_children(None))
196:     Entries.curpath=(k,)
197:     Entries.model.insert(k,row=(
198:         n,
199:         -1,"","",
200:         "","",False,False,False,False,False,False,
201:         "","","","",
202:         "",
203:     ))
204:     #view.listview.set_cursor(Entries.curpath)
205:     numbers.controller.entry_open(n)
206:     boats.controller.entry_open(-1,None,"","")
207:     finishes.controller.entry_open(
208:         get_last_finish(),None,"","",False,False,False,False,False,False
209:     )
210:     classes.controller.entry_open(-1,None,"","","")
211:     Entries.commentisopen=True
212:     view.commentbox.set_text("")
213:     #boats.box.grab_focus()
214: 
215: def open_entry(openpath):
216:     """Start working on a newly opened entry."""
217:     import numbers,boats,finishes,classes
218: 
219:     assert not Entries.curpath
220:     assert isinstance(openpath,tuple) and len(openpath)==1
221:     assert isinstance(openpath[0],int)
222:     Entries.curpath=openpath
223:     #view.listview.set_cursor(Entries.curpath)
224:     currenti=Entries.model.get_iter(Entries.curpath)
225:     n=Entries.model.get_value(currenti,0)
226:     numbers.controller.number_rollback(n)
227:     numbers.controller.entry_open(n)
228:     zipnum=Entries.model.get_value(currenti,1)
229:     boats.controller.entry_open(
230:         zipnum,
231:         None,
232:         Entries.model.get_value(currenti,2),
233:         Entries.model.get_value(currenti,3),
234:     )
235:     finishes.controller.entry_open(
236:         get_last_finish(),
237:         None,
238:         Entries.model.get_value(currenti,4),
239:         Entries.model.get_value(currenti,5),
240:         Entries.model.get_value(currenti,6),
241:         Entries.model.get_value(currenti,7),
242:         Entries.model.get_value(currenti,8),
243:         Entries.model.get_value(currenti,9),
244:         Entries.model.get_value(currenti,10),
245:         Entries.model.get_value(currenti,11),
246:     )
247:     classes.controller.entry_open(
248:         zipnum,
249:         None,
250:         Entries.model.get_value(currenti,12),
251:         Entries.model.get_value(currenti,13),
252:         Entries.model.get_value(currenti,14),
253:         Entries.model.get_value(currenti,15),
254:     )
255:     # inline comment entry open
256:     comments=Entries.model.get_value(currenti,16)
257:     Entries.commentisopen=True
258:     view.commentbox.set_text(comments)
259:     #view.listview.grab_focus()
260: 
261: def nonempty_entry():
262:     """Check to see whether an open entry is significantly nonempty."""
263:     assert Entries.curpath
264:     currenti=Entries.model.get_iter(Entries.curpath)
265:     for f in range(2,11):
266:         if Entries.model.get_value(currenti,f):
267:             return True
268: 
269: def close_entry(deletion=False):
270:     """Finish working and commit edits on a previously opened entry."""
271:     import numbers,boats,finishes,classes
272: 
273:     assert Entries.curpath
274:     # deletion should evaluates to either true or false
275:     currenti=Entries.model.get_iter(Entries.curpath)
276:     n=Entries.model.get_value(currenti,0)
277:     numbers.controller.entry_close()
278:     zipnum=Entries.model.get_value(currenti,1)
279:     boats.controller.entry_close(zipnum)
280:     finishes.controller.entry_close()
281:     classes.controller.entry_close()
282:     Entries.commentisopen=False
283:     view.commentbox.set_text("")
284:     if deletion:
285:         Entries.model.remove(currenti)
286:     else:
287:         numbers.controller.number_enter(n)
288:     Entries.curpath=None
289:     return n
290: 
291: ##### ##### public interface for updating current entry ##### #####
292: 
293: def move_entry(pivot,dest):
294:     # this is here to support a correct undo/redo stack
295:     # with duplicate entrynumber sort order isn't fully determined
296:     # so must force the issue
297:     currenti=Entries.model.get_iter(pivot)
298:     destinationi=Entries.model.get_iter(dest)
299:     if dest<pivot:
300:         Entries.model.move_before(currenti,destinationi)
301:     elif dest>pivot:
302:         Entries.model.move_after(currenti,destinationi)
303: 
304: def resort_by_entry_number(n):
305:     assert isinstance(n,int)
306:     # only the current entry is potentially out of sort order
307:     currenti=Entries.model.get_iter(Entries.curpath)
308:     (pivot,)=Entries.curpath
309:     k=bisect.bisect_right(entries_n,n,0,pivot)
310:     # bubble down
311:     if k<pivot:
312:         destinationi=Entries.model.get_iter((k,))
313:         Entries.model.move_before(currenti,destinationi)
314:         Entries.curpath=(k,)
315:         view.listview.set_cursor(Entries.curpath)
316:         return
317:     # bubble up
318:     k=bisect.bisect_left(
319:         entries_n,n,pivot+1,Entries.model.iter_n_children(None)
320:     )-1
321:     if k>pivot:
322:         destinationi=Entries.model.get_iter((k,))
323:         Entries.model.move_after(currenti,destinationi)
324:         Entries.curpath=(k,)
325:         view.listview.set_cursor(Entries.curpath)
326:         return
327: 
328: def set_number_entry(n):
329:     import finishes
330: 
331:     assert isinstance(n,int)
332:     currenti=Entries.model.get_iter(Entries.curpath)
333:     if entries_model_change_value(currenti,0,n):
334:         resort_by_entry_number(n)
335:         finishes.controller.entry_tweak(get_last_finish())
336:     view.listview.scroll_to_cell(Entries.curpath)
337: 
338: def set_boat_entry(zipnum,*b):
339:     import classes
340: 
341:     assert len(b)==2
342:     assert isinstance(zipnum,int) and zipnum>=-1
343:     currenti=Entries.model.get_iter(Entries.curpath)
344:     changedzip=entries_model_change_value(currenti,1,zipnum)
345:     entries_model_change_value(currenti,2,b[0].strip())
346:     entries_model_change_value(currenti,3,b[1].strip())
347:     if changedzip:
348:         classes.controller.entry_tweak(zipnum)
349:     view.listview.scroll_to_cell(Entries.curpath)
350: 
351: def set_finish_entry(*f):
352:     assert len(f)==2
353:     currenti=Entries.model.get_iter(Entries.curpath)
354:     entries_model_change_value(currenti,4,f[0].strip())
355:     entries_model_change_value(currenti,5,f[1].strip())
356:     view.listview.scroll_to_cell(Entries.curpath)
357: 
358: def set_finish_subentry(i,t):
359:     assert 0<=i<=1
360:     currenti=Entries.model.get_iter(Entries.curpath)
361:     entries_model_change_value(currenti,4+i,t.strip())
362:     view.listview.scroll_to_cell(Entries.curpath)
363: 
364: def set_finish_toggle(i,b):
365:     assert 0<=i<=5
366:     currenti=Entries.model.get_iter(Entries.curpath)
367:     entries_model_change_value(currenti,6+i,bool(b))
368:     view.listview.scroll_to_cell(Entries.curpath)
369: 
370: def set_class_entry(*c):
371:     assert len(c)==4
372:     currenti=Entries.model.get_iter(Entries.curpath)
373:     entries_model_change_value(currenti,12,c[0].strip())
374:     entries_model_change_value(currenti,13,c[1].strip())
375:     entries_model_change_value(currenti,14,c[2].strip())
376:     entries_model_change_value(currenti,15,c[3].strip())
377:     view.listview.scroll_to_cell(Entries.curpath)
378: 
379: def set_class_subentry(i,*c):
380:     assert 0<=i<=2
381:     assert i==0 and len(c)==2 or i in [1,2] and len(c)==1
382:     currenti=Entries.model.get_iter(Entries.curpath)
383:     if i==0:
384:         entries_model_change_value(currenti,12,c[0].strip())
385:         entries_model_change_value(currenti,13,c[1].strip())
386:     else:
387:         entries_model_change_value(currenti,13+i,c[0].strip())
388:     view.listview.scroll_to_cell(Entries.curpath)
389: 
390: def set_comment_entry(comments):
391:     assert isinstance(comments,str)
392:     currenti=Entries.model.get_iter(Entries.curpath)
393:     entries_model_change_value(currenti,16,comments)
394:     view.listview.scroll_to_cell(Entries.curpath)
395: 
396: ##### ##### ##### ##### iterate over numbers ##### ##### ##### #####
397: 
398: # generate all the rows of the model
399: 
400: def iter_numbers():
401:     i=Entries.model.get_iter_first()
402:     while i:
403:         yield Entries.model.get_value(i,0)
404:         i=Entries.model.iter_next(i)
405: 
406: ##### ##### private subroutines ##### #####
407: 
408: # this is here for a sane undo/redo stack # or is it?
409: 
410: def entries_model_change_value(i,f,v):
411:     oldv=Entries.model.get_value(i,f)
412:     if oldv<>v:
413:         Entries.model.set_value(i,f,v)
414:         return True
415: 
416: # public interface for stealing info from the current entry
417: 
418: def get_boat_zip_entry():
419:     """Return a reference to an identified boat or -1 otherwise."""
420:     currenti=Entries.model.get_iter(Entries.curpath)
421:     return Entries.model.get_value(currenti,1)
422: 
423: # public interface for stealing info from preceeding entries
424: 
425: def get_last_finish(defaultt="20:00:00"):
426:     """
427:     Return time for last preceeding entry which has time defined
428:     or default time of 20:00:00 otherwise.
429:     """
430:     if Entries.curpath:
431:         (i,)=Entries.curpath
432:         while i:
433:             i-=1
434:             iter=Entries.model.get_iter((i,))
435:             finish=Entries.model.get_value(iter,5)
436:             if finish:
437:                 return finish
438:     return defaultt
439: 
440: ## entriesmod.py ## python package PyScore.timesheet module entries.modeller ##