Made sizes of windows better, and easier to change
[library/.git] / browser.py
1 import sys
2 import curses
3 import db_layer as db
4 from form import BookForm,CategoryForm
5
6 class browserWindow:
7     hl=0
8     topline = 0
9     entries = []
10     selected = list()
11     commands = [(' /', 'search'), (' n', 'find next'), (' N', 'find previous'), 
12             ('F6', 'Sort Column'), (' q', 'quit')]
13     cs = []
14     # column definitions are in (label, weight, specified width) triples
15     columnDefs = [('something',1,None)]
16     mx = my = 0
17     # for searches
18     last_search = ""
19     found_index = 0
20
21     def __init__(self,window,helpbar, height=50, width=80):
22         self.w = window
23         self.hb = helpbar
24         self.w.resize(height,width)
25         self.updateGeometry()
26         self.commands = self.cs+self.commands
27
28     def sortByColumn(self, col):
29         self.entries.sort(key=lambda k: k.get(col,"")) # key=dict.get(col))
30         self.selected = list(map(lambda x: False, self.selected))
31
32
33     def updateGeometry(self):
34         (self.my,self.mx)=self.w.getmaxyx()
35         self.pageSize = self.my-4
36         self.calcColWidths()
37
38     def calcColWidths(self):
39         total_weights = 0
40         available_space = self.mx - len(self.columnDefs) -2
41         cols = []
42         for label,weight,value in self.columnDefs:
43             if value!=None:
44                 available_space -= value
45             else:
46                 total_weights+=weight
47
48         for label,weight,value in self.columnDefs:
49             if value!=None:
50                 cols.append((label,value))
51             else:
52                 cols.append((label,available_space*weight//total_weights))
53         self.columns=cols
54
55     def refresh(self):
56         self.hb.commands = self.commands
57         self.hb.refresh()
58         self.w.box()
59         self.displayHeader()
60         for r in range(0,self.pageSize):
61             self.displayRow(r)
62         self.w.refresh()
63         self.highlight()
64
65     def clear(self):
66         self.w.erase()
67         self.w.refresh()
68
69     def centreChild(self,child):
70         (y,x) = self.w.getbegyx()
71         (r,c) = child.getmaxyx()
72         child.mvwin( y+(self.my-r)//2,x+(self.mx-c)//2)
73
74
75     def displayHeader(self):
76         cursor = 2
77         for header,width in self.columns:
78             self.w.addnstr(1,cursor,header+" "*width,width)
79             self.w.addstr(2,cursor,"-"*width)
80             cursor += width+1
81
82     def displayRow(self,row):
83         if self.topline+row < len(self.entries):
84             entry = self.entries[self.topline+row]
85             cursor = 2
86             self.w.addnstr(row+3, 1, " "*self.mx,self.mx-2)
87             if self.selected[self.topline+row]:
88                 self.w.addstr(row+3, 1, "*")
89             else:
90                 self.w.addstr(row+3, 1, " ")
91             for k,width in self.columns:
92                 if k.lower() in entry:
93                     self.w.addnstr(row+3,cursor,str(entry[k.lower()])+" "*width,width)
94                 cursor += width+1
95         else:
96             self.w.addstr(row+3,1," "*(self.mx-2))
97
98     def highlight(self):
99         row = self.hl-self.topline+3
100         if row > 1 and row < self.my:
101             self.w.chgat(row,1,self.mx-2,curses.A_REVERSE)
102
103     def unHighlight(self):
104         row = self.hl-self.topline+3
105         if row > 1 and row < self.my:
106             self.w.chgat(row,1,self.mx-2,curses.A_NORMAL)
107
108     def mvHighlight(self,delta):
109         new = self.hl+delta
110         new = max(new,0)
111         new = min(new,len(self.entries)-1)
112         self.unHighlight()
113         self.hl = new
114         self.highlight()
115     
116     def scroll(self,delta):
117         self.unHighlight()
118         self.topline += delta
119         self.topline = min(self.topline,len(self.entries)-1)
120         self.topline = max(self.topline,0)
121         self.refresh()
122
123     def search(self, string):
124         case_sensitive = not(string.islower())
125         #sys.stderr.write(str(case_sensitive)+'\n')
126         i = 0
127         found = False
128         for e in self.entries:
129             for k,v in e.items():
130                 # we or with found to make sure it is never "unfound"
131                 if case_sensitive:
132                     found = str(v).find(string) != -1 or found
133                 else:
134                     found = str(v).lower().find(string) != -1 or found
135             if found:
136                 break
137             i += 1;
138         if found:
139             self.last_search = string
140             self.search_index = i
141             return i
142         else:
143             self.search_index = -1
144             return -1
145
146     def findNext(self):
147         if self.last_search == "" or self.search_index == -1:
148             return -1
149         found = False
150         for i in range(self.hl+1,len(self.entries)-1):
151             for k,v in self.entries[i].items():
152                 if str(v).find(self.last_search) != -1:
153                     found = True
154             if found:
155                 break
156         if found:
157             self.search_index = i
158             return i
159         else:
160             return -1
161
162     def findPrevious(self):
163         if self.last_search == "" or self.search_index == -1:
164             return -1
165         found = False
166         for i in range(self.hl-1, 0, -1):
167             for k,v in self.entries[i].items():
168                 if str(v).find(self.last_search) != -1:
169                     found = True
170             if found:
171                 break
172         if found:
173             self.search_index = i
174             return i
175         else:
176             return -1
177
178
179     def eventLoop(self):
180         self.w.keypad(1)
181         self.refresh()
182
183         ch = self.w.getch()
184         while ch != 27 and ch != 113:
185             ch = self.handleInput(ch)
186             if ch==113:
187                 return {}
188             self.w.refresh()
189             ch = self.w.getch()
190             self.hb.refresh()
191
192     def handleInput(self,ch):
193         if ch == curses.KEY_UP or ch == 107 or ch == 16:
194             if self.hl == self.topline:
195                 self.scroll(-self.pageSize//2-1)
196             self.mvHighlight(-1)
197         elif ch == curses.KEY_DOWN or ch == 106 or ch == 14:
198             if self.hl == self.topline+self.pageSize-1:
199                 self.scroll(+self.pageSize//2+1)
200             self.mvHighlight(+1)
201         elif ch == curses.KEY_PPAGE:
202             self.scroll(-self.pageSize)
203             self.mvHighlight(-self.pageSize)
204         elif ch == curses.KEY_NPAGE:
205             self.scroll(+self.pageSize)
206             self.mvHighlight(+self.pageSize)
207         elif ch == curses.KEY_HOME:
208             self.scroll(-len(self.entries))
209             self.mvHighlight(-len(self.entries))
210         elif ch == curses.KEY_END:
211             self.scroll(len(self.entries))
212             self.mvHighlight(len(self.entries))
213         elif ch == 47: # forward slash
214             string = self.hb.getSearch()
215             hl = self.search(string)
216             if hl != -1:
217                 delta = hl - self.hl
218                 self.scroll(delta)
219                 self.mvHighlight(delta)
220             else:
221                 self.hb.display(string+' not found')
222         elif ch == 110: # n
223             hl = self.findNext()
224             if hl != -1:
225                 delta = hl - self.hl
226                 self.scroll(delta)
227                 self.mvHighlight(delta)
228             else:
229                 self.hb.display(self.last_search+' not found')
230         elif ch == 78: # N
231             hl = self.findPrevious()
232             if hl != -1:
233                 delta = hl - self.hl
234                 self.scroll(delta)
235                 self.mvHighlight(delta)
236             else:
237                 self.hb.display(self.last_search+' not found')
238         elif ch == 270: # F6 Sorts
239             w = curses.newwin(1,1)
240             cl = columnSelector(w,self.hb,40,20)
241             self.centreChild(w)
242             col = cl.eventLoop()
243             cl.clear()
244             self.sortByColumn(col)
245             self.clear()
246             self.refresh()
247         elif ch == 32:
248             if len(self.selected)>0:
249                 self.selected[self.hl] = not self.selected[self.hl]
250             self.displayRow(self.hl-self.topline)
251             self.highlight()
252
253
254
255 class trashBrowser(browserWindow):
256     columnDefs = [('ID',0,3),
257                   ('ISBN',0,13),
258                   ('Authors',30,None),
259                   ('Title',60,None)]
260     
261     cs = [(' r', 'restore selected'), (' d', 'delete selected')]
262     
263     # redefinable functions
264     def viewSelection(self,book):
265         bookid = book['id']
266         w=curses.newwin(1,1)
267         bf = BookForm(w, self.hb, book, width=self.mx-10)
268         self.centreChild(w)
269         bf.caption='Viewing Book '+str(bookid)
270         bf.blabel='done'
271         bf.event_loop()
272         bf.clear()
273
274     def restoreSelected(self):
275         books = []
276         for sel,book in zip(self.selected, self.entries):
277             if sel:
278                 books.append(book)
279         db.restoreBooks(books)
280
281     def delSelected(self):
282         books = []
283         for sel,book in zip(self.selected, self.entries):
284             if sel:
285                 books.append(book)
286         db.deleteBooks(books)
287
288     def refreshBooks(self):
289         self.entries = db.getRemovedBooks()
290         self.selected = list(map(lambda x:False, self.entries))
291
292     def handleInput(self,ch):
293         browserWindow.handleInput(self,ch)
294         if ch == 10:
295             book = self.entries[self.hl]
296             self.viewSelection(book)
297             self.refresh()
298         if ch==114: #restore books
299             count=0
300             for s in self.selected[0:self.hl-1]:
301                 if s:
302                     count+=1
303             self.restoreSelected()
304             self.refreshBooks()
305             self.refresh()
306             self.scroll(-count)
307             self.mvHighlight(-count)
308         if ch==100: # delete books
309             count=0
310             for s in self.selected[0:self.hl-1]:
311                 if s:
312                     count+=1
313             self.delSelected()
314             self.refreshBooks()
315             self.refresh()
316             self.scroll(-count)
317             self.mvHighlight(-count)
318         return ch
319
320 class bookBrowser(browserWindow):
321     columnDefs = [('ID',0,3),
322                   ('ISBN',0,13),
323                   ('Authors',30,None),
324                   ('Title',60,None)]
325     
326     cs = [(' u', 'update'), (' d', 'delete selected')]
327     
328     # redefinable functions
329     def updateSelection(self,book):
330         bookid = book['id']
331         
332         w=curses.newwin(1,1)
333         bf = BookForm(w,self.hb,book, width=self.mx-20)
334         self.centreChild(w)
335         bf.caption='Update Book '+str(bookid)
336         bf.blabel='update'
337         newbook = bf.event_loop()
338         if len(newbook)!=0:
339             db.updateBook(newbook,bookid)
340         bf.clear()
341
342     def viewSelection(self,book):
343         bookid = book['id']
344         w=curses.newwin(1,1)
345         bf = BookForm(w,self.hb,book, width=self.mx-20)
346         self.centreChild(w)
347         bf.caption='Viewing Book '+str(bookid)
348         bf.blabel='done'
349         bf.event_loop()
350         bf.clear()
351
352     def categorizeSelection(self,book):
353         w = curses.newwin(1,1)
354         cs = categorySelector(w,self.hb,40,40)
355         self.centreChild(w)
356         cs.book = book
357         cs.refreshCategories()
358         cs.eventLoop()
359         cs.clear()
360     
361     def delSelected(self):
362         books = []
363         for sel,book in zip(self.selected, self.entries):
364             if sel:
365                 books.append(book)
366         db.removeBooks(books)
367
368     def refreshBooks(self):
369         self.entries = db.getBooks()
370         self.selected = list(map(lambda x:False, self.entries))
371
372     def refreshBooksInCategory(self,cat):
373         self.entries = db.getBooksByCategory(cat)
374         self.selected = list(map(lambda x:False, self.entries))
375
376     def handleInput(self,ch):
377         browserWindow.handleInput(self,ch)
378         if ch == 117: #update on 'u'
379             book = self.entries[self.hl]
380             self.updateSelection(book)
381             self.entries[self.hl]=db.getBookByID(book['id'])
382             self.refresh()
383         elif ch == 10:
384             book = self.entries[self.hl]
385             self.viewSelection(book)
386             self.refresh()
387         elif ch == 99:
388             book = self.entries[self.hl]
389             self.categorizeSelection(book)
390             self.refresh()
391         if ch==100:
392             count=0
393             for s in self.selected[0:self.hl-1]:
394                 if s:
395                     count+=1
396             self.delSelected()
397             self.refreshBooks()
398             self.refresh()
399             self.scroll(-count)
400             self.mvHighlight(-count)
401         return ch
402
403 class categoryBrowser(browserWindow):
404     columnDefs = [('Category',100,None)]
405     cs = [(' a', 'add category'), (' d', 'delete selected')]
406
407
408     def refreshCategories(self):
409         self.entries = db.getCategories()
410         self.sortByColumn('category')
411         self.selected = list(map(lambda x:False, self.entries))
412
413     def addCategory(self):
414         w = curses.newwin(1,1,10,10)
415         cf = CategoryForm(w,self.hb)
416         self.centreChild(w)
417         cats = cf.event_loop()
418         for c in cats:
419             db.addCategory(c)
420         cf.clear()
421
422     def viewCategory(self):
423         w = curses.newwin(20,80,20,20)
424         b = bookBrowser(w,self.hb)
425         self.centreChild(w)
426         b.refreshBooksInCategory(self.entries[self.hl])
427         b.eventLoop()
428         b.clear()
429
430     def delSelected(self):
431         categories = []
432         for sel,cat in zip(self.selected, self.entries):
433             if sel:
434                 categories.append(cat)
435         db.deleteCategories(categories)
436
437     def handleInput(self,ch):
438         browserWindow.handleInput(self,ch)
439         if ch==97:
440             self.addCategory()
441             self.refreshCategories()
442             self.refresh()
443         if ch ==10:
444             self.viewCategory()
445             self.refresh()
446         if ch==100:
447             count=0
448             for s in self.selected[0:self.hl-1]:
449                 if s:
450                     count+=1
451             self.delSelected()
452             self.refreshCategories()
453             self.refresh()
454             self.scroll(-count)
455             self.mvHighlight(-count)
456         return ch
457
458 class categorySelector(browserWindow):
459     columnDefs = [('Category',100,None)]
460     cs = [(' a', 'add category'), (' c', 'commit')]
461     book = {'id':''}
462     original=[]
463
464
465     def refreshCategories(self):
466         self.entries = db.getCategories()
467         self.sortByColumn('category')
468         self.refreshSelected()
469
470     def refreshSelected(self):
471         self.original = list(map(lambda x:False, self.entries))
472         cats = db.getBookCategories(self.book)
473         cats.sort()
474         cats.sort(key=lambda k: k.get('category')) # key=dict.get(col))
475         i = 0
476         j = 0
477         for cat in self.entries:
478             if i == len(cats):
479                 break
480             if cat['id']==cats[i]['cat_id']:
481                 self.original[j] = True;
482                 i+=1
483             j+=1
484         self.selected = self.original[:]
485
486
487     def addCategory(self):
488         w = curses.newwin(1,1,10,10)
489         cf = CategoryForm(w,self.hb)
490         self.centreChild(w)
491         cats = cf.event_loop()
492         for c in cats:
493             db.addCategory(c)
494         cf.clear()
495
496     def updateCategories(self):
497         # first removed the deselected ones
498         uncats = []
499         cats = []
500         for old, new, category in zip(self.original, self.selected, self.entries):
501             if old and (not new):
502                 uncats.append(category)
503             if (not old) and new:
504                 cats.append(category)
505         db.uncategorizeBook(self.book, uncats)
506         # add the newly selected categories
507         db.categorizeBook(self.book, cats)
508
509
510     def handleInput(self,ch):
511         browserWindow.handleInput(self,ch)
512         if ch==97:
513             self.addCategory()
514             self.refreshCategories()
515             self.refresh()
516         if ch==99:
517             self.updateCategories()
518             return 113
519
520
521
522 class columnSelector(browserWindow):
523     columnDefs = [('Column',100,None)]
524     entries = [{'column': 'id'}, {'column': 'isbn'}, {'column': 'lccn'},
525             {'column': 'title'}, {'column': 'subtitle'}, {'column': 'authors'}, 
526             {'column': 'edition'}, {'column': 'publisher'}, 
527             {'column': 'publish year'}, {'column': 'publish month'}, 
528             {'column': 'publish location'}, {'column': 'pages'}, {'column': 'pagination'}, 
529             {'column': 'weight'}, {'column': 'last updated'}]
530
531     def __init__(self,window,helpbar,height=40,width=20):
532         self.selected = [False,False,False,False,False,False,False,
533                          False,False,False,False,False,False,False,False]
534         browserWindow.__init__(self,window,helpbar,height,width)
535
536
537     def eventLoop(self):
538         self.w.keypad(1)
539         self.refresh()
540
541         ch = self.w.getch()
542         while ch != 27 and ch != 113:
543             ch = self.handleInput(ch)
544             if ch==10:
545                 col = self.entries[self.hl]
546                 return col['column']
547             self.w.refresh()
548             ch = self.w.getch()
549             self.hb.refresh()
550     
551     def handleInput(self,ch):
552         browserWindow.handleInput(self,ch)
553         return ch