fc8d422e95777d4db2b91b51e57fb69555330d7d
[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             self.case_sensitive = case_sensitive
142             return i
143         else:
144             self.search_index = -1
145             return -1
146
147     def _find_again(self, direction=1):
148         """Find the next match in the entries
149
150         direction = 1 means look ahead
151         direction = -1 means look back
152         """
153         if self.last_search == "" or self.search_index == -1:
154             return -1
155         found = False
156         if direction == 1:
157             last = len(self.entries) -1
158         elif direction == -1:
159             last = 0
160         for i in range(self.hl+direction, last, direction):
161             for k,v in self.entries[i].items():
162                 if self.case_sensitive:
163                     found = str(v).find(self.last_search) != -1 or found
164                 else:
165                     found = str(v).lower().find(self.last_search) != -1 or found
166             if found:
167                 break
168         if found:
169             self.search_index = i
170             return i
171         else:
172             return -1
173
174     def eventLoop(self):
175         self.w.keypad(1)
176         self.refresh()
177
178         ch = self.w.getch()
179         while ch != 27 and ch != 113:
180             ch = self.handleInput(ch)
181             if ch==113:
182                 return {}
183             self.w.refresh()
184             ch = self.w.getch()
185             self.hb.refresh()
186
187     def handleInput(self,ch):
188         if ch == curses.KEY_UP or ch == 107 or ch == 16:
189             if self.hl == self.topline:
190                 self.scroll(-self.pageSize//2-1)
191             self.mvHighlight(-1)
192         elif ch == curses.KEY_DOWN or ch == 106 or ch == 14:
193             if self.hl == self.topline+self.pageSize-1:
194                 self.scroll(+self.pageSize//2+1)
195             self.mvHighlight(+1)
196         elif ch == curses.KEY_PPAGE:
197             self.scroll(-self.pageSize)
198             self.mvHighlight(-self.pageSize)
199         elif ch == curses.KEY_NPAGE:
200             self.scroll(+self.pageSize)
201             self.mvHighlight(+self.pageSize)
202         elif ch == curses.KEY_HOME:
203             self.scroll(-len(self.entries))
204             self.mvHighlight(-len(self.entries))
205         elif ch == curses.KEY_END:
206             self.scroll(len(self.entries))
207             self.mvHighlight(len(self.entries))
208         elif ch == 47: # forward slash
209             string = self.hb.getSearch()
210             hl = self.search(string)
211             if hl != -1:
212                 delta = hl - self.hl
213                 self.scroll(delta)
214                 self.mvHighlight(delta)
215             else:
216                 self.hb.display(string+' not found')
217         elif ch == 110: # n
218             hl = self._find_again(+1)
219             if hl != -1:
220                 delta = hl - self.hl
221                 self.scroll(delta)
222                 self.mvHighlight(delta)
223             else:
224                 self.hb.display(self.last_search+' not found')
225         elif ch == 78: # N
226             hl = self._find_again(-1)
227             if hl != -1:
228                 delta = hl - self.hl
229                 self.scroll(delta)
230                 self.mvHighlight(delta)
231             else:
232                 self.hb.display(self.last_search+' not found')
233         elif ch == 270: # F6 Sorts
234             w = curses.newwin(1,1)
235             cl = columnSelector(w,self.hb,40,20)
236             self.centreChild(w)
237             col = cl.eventLoop()
238             cl.clear()
239             self.sortByColumn(col)
240             self.clear()
241             self.refresh()
242         elif ch == 32:
243             if len(self.selected)>0:
244                 self.selected[self.hl] = not self.selected[self.hl]
245             self.displayRow(self.hl-self.topline)
246             self.highlight()
247
248
249
250 class trashBrowser(browserWindow):
251     columnDefs = [('ID',0,3),
252                   ('ISBN',0,13),
253                   ('Authors',30,None),
254                   ('Title',60,None)]
255     
256     cs = [(' r', 'restore selected'), (' d', 'delete selected')]
257     
258     # redefinable functions
259     def viewSelection(self,book):
260         bookid = book['id']
261         w=curses.newwin(1,1)
262         bf = BookForm(w, self.hb, book, width=self.mx-10)
263         self.centreChild(w)
264         bf.caption='Viewing Book '+str(bookid)
265         bf.blabel='done'
266         bf.event_loop()
267         bf.clear()
268
269     def restoreSelected(self):
270         books = []
271         for sel,book in zip(self.selected, self.entries):
272             if sel:
273                 books.append(book)
274         db.restoreBooks(books)
275
276     def delSelected(self):
277         books = []
278         for sel,book in zip(self.selected, self.entries):
279             if sel:
280                 books.append(book)
281         db.deleteBooks(books)
282
283     def refreshBooks(self):
284         self.entries = db.getRemovedBooks()
285         self.selected = list(map(lambda x:False, self.entries))
286
287     def handleInput(self,ch):
288         browserWindow.handleInput(self,ch)
289         if ch == 10:
290             book = self.entries[self.hl]
291             self.viewSelection(book)
292             self.refresh()
293         if ch==114: #restore books
294             count=0
295             for s in self.selected[0:self.hl-1]:
296                 if s:
297                     count+=1
298             self.restoreSelected()
299             self.refreshBooks()
300             self.refresh()
301             self.scroll(-count)
302             self.mvHighlight(-count)
303         if ch==100: # delete books
304             count=0
305             for s in self.selected[0:self.hl-1]:
306                 if s:
307                     count+=1
308             self.delSelected()
309             self.refreshBooks()
310             self.refresh()
311             self.scroll(-count)
312             self.mvHighlight(-count)
313         return ch
314
315 class bookBrowser(browserWindow):
316     columnDefs = [('ID',0,3),
317                   ('ISBN',0,13),
318                   ('Authors',30,None),
319                   ('Title',60,None)]
320     
321     cs = [(' u', 'update'), (' d', 'delete selected')]
322     
323     # redefinable functions
324     def updateSelection(self,book):
325         bookid = book['id']
326         
327         w=curses.newwin(1,1)
328         bf = BookForm(w,self.hb,book, width=self.mx-20)
329         self.centreChild(w)
330         bf.caption='Update Book '+str(bookid)
331         bf.blabel='update'
332         newbook = bf.event_loop()
333         if len(newbook)!=0:
334             db.updateBook(newbook,bookid)
335         bf.clear()
336
337     def viewSelection(self,book):
338         bookid = book['id']
339         w=curses.newwin(1,1)
340         bf = BookForm(w,self.hb,book, width=self.mx-20)
341         self.centreChild(w)
342         bf.caption='Viewing Book '+str(bookid)
343         bf.blabel='done'
344         bf.event_loop()
345         bf.clear()
346
347     def categorizeSelection(self,book):
348         w = curses.newwin(1,1)
349         cs = categorySelector(w,self.hb,40,40)
350         self.centreChild(w)
351         cs.book = book
352         cs.refreshCategories()
353         cs.eventLoop()
354         cs.clear()
355     
356     def delSelected(self):
357         books = []
358         for sel,book in zip(self.selected, self.entries):
359             if sel:
360                 books.append(book)
361         db.removeBooks(books)
362
363     def refreshBooks(self):
364         self.entries = db.getBooks()
365         self.selected = list(map(lambda x:False, self.entries))
366
367     def refreshBooksInCategory(self,cat):
368         self.entries = db.getBooksByCategory(cat)
369         self.selected = list(map(lambda x:False, self.entries))
370
371     def handleInput(self,ch):
372         browserWindow.handleInput(self,ch)
373         if ch == 117: #update on 'u'
374             book = self.entries[self.hl]
375             self.updateSelection(book)
376             self.entries[self.hl]=db.getBookByID(book['id'])
377             self.refresh()
378         elif ch == 10:
379             book = self.entries[self.hl]
380             self.viewSelection(book)
381             self.refresh()
382         elif ch == 99:
383             book = self.entries[self.hl]
384             self.categorizeSelection(book)
385             self.refresh()
386         if ch==100:
387             count=0
388             for s in self.selected[0:self.hl-1]:
389                 if s:
390                     count+=1
391             self.delSelected()
392             self.refreshBooks()
393             self.refresh()
394             self.scroll(-count)
395             self.mvHighlight(-count)
396         return ch
397
398 class categoryBrowser(browserWindow):
399     columnDefs = [('Category',100,None)]
400     cs = [(' a', 'add category'), (' d', 'delete selected')]
401
402
403     def refreshCategories(self):
404         self.entries = db.getCategories()
405         self.sortByColumn('category')
406         self.selected = list(map(lambda x:False, self.entries))
407
408     def addCategory(self):
409         w = curses.newwin(1,1,10,10)
410         cf = CategoryForm(w,self.hb)
411         self.centreChild(w)
412         cats = cf.event_loop()
413         for c in cats:
414             db.addCategory(c)
415         cf.clear()
416
417     def viewCategory(self):
418         w = curses.newwin(20,80,20,20)
419         b = bookBrowser(w,self.hb)
420         self.centreChild(w)
421         b.refreshBooksInCategory(self.entries[self.hl])
422         b.eventLoop()
423         b.clear()
424
425     def delSelected(self):
426         categories = []
427         for sel,cat in zip(self.selected, self.entries):
428             if sel:
429                 categories.append(cat)
430         db.deleteCategories(categories)
431
432     def handleInput(self,ch):
433         browserWindow.handleInput(self,ch)
434         if ch==97:
435             self.addCategory()
436             self.refreshCategories()
437             self.refresh()
438         if ch ==10:
439             self.viewCategory()
440             self.refresh()
441         if ch==100:
442             count=0
443             for s in self.selected[0:self.hl-1]:
444                 if s:
445                     count+=1
446             self.delSelected()
447             self.refreshCategories()
448             self.refresh()
449             self.scroll(-count)
450             self.mvHighlight(-count)
451         return ch
452
453 class categorySelector(browserWindow):
454     columnDefs = [('Category',100,None)]
455     cs = [(' a', 'add category'), (' c', 'commit')]
456     book = {'id':''}
457     original=[]
458
459
460     def refreshCategories(self):
461         self.entries = db.getCategories()
462         self.sortByColumn('category')
463         self.refreshSelected()
464
465     def refreshSelected(self):
466         self.original = list(map(lambda x:False, self.entries))
467         cats = db.getBookCategories(self.book)
468         cats.sort()
469         cats.sort(key=lambda k: k.get('category')) # key=dict.get(col))
470         i = 0
471         j = 0
472         for cat in self.entries:
473             if i == len(cats):
474                 break
475             if cat['id']==cats[i]['cat_id']:
476                 self.original[j] = True;
477                 i+=1
478             j+=1
479         self.selected = self.original[:]
480
481
482     def addCategory(self):
483         w = curses.newwin(1,1,10,10)
484         cf = CategoryForm(w,self.hb)
485         self.centreChild(w)
486         cats = cf.event_loop()
487         for c in cats:
488             db.addCategory(c)
489         cf.clear()
490
491     def updateCategories(self):
492         # first removed the deselected ones
493         uncats = []
494         cats = []
495         for old, new, category in zip(self.original, self.selected, self.entries):
496             if old and (not new):
497                 uncats.append(category)
498             if (not old) and new:
499                 cats.append(category)
500         db.uncategorizeBook(self.book, uncats)
501         # add the newly selected categories
502         db.categorizeBook(self.book, cats)
503
504
505     def handleInput(self,ch):
506         browserWindow.handleInput(self,ch)
507         if ch==97:
508             self.addCategory()
509             self.refreshCategories()
510             self.refresh()
511         if ch==99:
512             self.updateCategories()
513             return 113
514
515
516
517 class columnSelector(browserWindow):
518     columnDefs = [('Column',100,None)]
519     entries = [{'column': 'id'}, {'column': 'isbn'}, {'column': 'lccn'},
520             {'column': 'title'}, {'column': 'subtitle'}, {'column': 'authors'}, 
521             {'column': 'edition'}, {'column': 'publisher'}, 
522             {'column': 'publish year'}, {'column': 'publish month'}, 
523             {'column': 'publish location'}, {'column': 'pages'}, {'column': 'pagination'}, 
524             {'column': 'weight'}, {'column': 'last updated'}]
525
526     def __init__(self,window,helpbar,height=40,width=20):
527         self.selected = [False,False,False,False,False,False,False,
528                          False,False,False,False,False,False,False,False]
529         browserWindow.__init__(self,window,helpbar,height,width)
530
531
532     def eventLoop(self):
533         self.w.keypad(1)
534         self.refresh()
535
536         ch = self.w.getch()
537         while ch != 27 and ch != 113:
538             ch = self.handleInput(ch)
539             if ch==10:
540                 col = self.entries[self.hl]
541                 return col['column']
542             self.w.refresh()
543             ch = self.w.getch()
544             self.hb.refresh()
545     
546     def handleInput(self,ch):
547         browserWindow.handleInput(self,ch)
548         return ch