import curses from library.exceptions import LibrarianException from copy import copy import library.database as db class TextEntry: """A part of a window that handles text entry. Properties: value holds the string that was entered Public Methods: set_geom(row,column,width) Sets the geometry in the window set_value(string) Set the value and redraw gain_focus() Gives it focus, moving cursor and changing the drawing lose_focus() Takes focus, moving cursor to start, changing drawing handle_input(ch) Pass this the ncurses key, and it manages input redraw() Redraw the text entry (should never need to do this """ # Public value = "" # Use the set_value function to set, but retrieve with value # Should be Private cursor = 0 start = 0 focus = False x = 0 y = 0 width = 10 # Public methods def __init__(self, parent_window, value=""): self.w = parent_window self.value = value def set_geom(self,y,x,width): self.x = x self.y = y self.width = width def set_value(self,v): self.value=v self.cursor=len(v) self.redraw() def gain_focus(self): self.focus = True self._mv_cursor(+len(self.value)) self.start = max(0,self.cursor-self.width) self.redraw() def lose_focus(self): self.focus = False self.cursor = 0 self.start = 0 self.redraw() def handle_input(self,ch): if ch==curses.KEY_LEFT: self._mv_cursor(-1) elif ch==curses.KEY_HOME: self._set_cursor(0) elif ch==curses.KEY_RIGHT: self._mv_cursor(+1) elif ch==curses.KEY_END: self._set_cursor(len(self.value)) elif ch>=32 and ch<=126: self._insert(curses.keyname(ch).decode('utf-8')) elif ch==curses.KEY_BACKSPACE: self._backspace() elif ch==curses.KEY_DC: self._delete() def redraw(self): self.w.addnstr(self.y,self.x, self.value[self.start:]+" "*self.width, self.width) if self.focus: self.w.chgat(self.y, self.x, self.width, curses.A_UNDERLINE) curses.curs_set(1) # Private functions def _mv_cursor(self,delta): self._set_cursor(self.cursor + delta) def _set_cursor(self, new_c): self.cursor = max(0, min(len(self.value), new_c)) self.start = max(0,self.cursor-self.width+1) self.redraw() # Place the drawn cursor in the correct spot col = self.x + self.cursor - self.start self.w.move(self.y,col) def _insert(self,ch): c = self.cursor self.value = self.value[:c] +ch+ self.value[c:] self._mv_cursor(+1) def _backspace(self): if self.cursor>0: c = self.cursor self.value=self.value[:c-1] + self.value[c:] self._mv_cursor(-1) def _delete(self): c = self.cursor self.value = self.value[:c] + self.value[c+1:] self._mv_cursor(0) class PasswordEntry(TextEntry): def redraw(self): self.w.addnstr(self.y,self.x, " "*self.width, self.width) if self.focus: self.w.chgat(self.y, self.x, self.width, curses.A_UNDERLINE) curses.curs_set(1) class FormWindow: """General class for a Form Window. To use, make the window for it, call the constructor, then call event_loop. """ # Private variables mx = my = 0 hl = 0 bt = -1 left = 0 top = 2 row = 2 buttononly = False caption = "Form" blabel = "Done" labels = ["label1"] commands = [('pU', 'top'),('pD', 'bottom'),('Es', 'cancel')] # Public functions def __init__(self,window,helpbar,book={}, width=50): self.w = window self.w.resize(len(self.labels)+6,width) self.hb = helpbar self._make_entries() self._update_geometry() self._set_entries(book) def clear(self): self.w.erase() self.w.refresh() def event_loop(self): self.w.keypad(1) self.refresh() self.hl=0 if self.buttononly: self.bt = 1 self._highlight_button() else: self.entries[self.hl].gain_focus() ch = self.w.getch() while ch != 27: self.handle_input(ch) if ch==10 or ch==curses.KEY_ENTER: if self.bt==0: return {} elif self.bt==1: return self._return_values() else: self._mv_focus(+1) self.w.refresh() ch = self.w.getch() curses.curs_set(0) return {} def _make_entries(self): self.entries = [] for e in range(len(self.labels)): self.entries.append(TextEntry(self.w)) def _update_geometry(self): (self.my, self.mx) = self.w.getmaxyx() self.left=0 for l in self.labels: self.left = max(len(l),self.left) self.left += 4 width = self.mx-self.left-2 self.top = 2 for r in range(len(self.entries)): self.entries[r].set_geom(r+self.top, self.left, width) # next, the buttons self.brow = self.top+len(self.labels)+1 self.bcol = [self.mx-len(self.blabel)-14, self.mx-len(self.blabel)-4] self.bwidth = [8,len(self.blabel)+2] def _set_entries(self,book): for (e, l) in enumerate(self.labels): if l.lower() in book: self.entries[e].value = str(book[l.lower()]) else: self.entries[e].value = "" def redraw(self): self.w.box() self.w.addstr(0,(self.mx-len(self.caption))//2,self.caption) r=0 for l in self.labels: c = self.left-len(l)-2 self.w.addstr(r+self.top,c,l+":") self.entries[r].redraw() r+=1 self.w.addstr(self.brow,self.bcol[0], " <"+self.blabel+">") self.w.refresh() def refresh(self): self.hb.commands = self.commands self.hb.refresh() self._update_geometry() self.redraw() def _highlight_button(self): self.w.chgat(self.brow, self.bcol[self.bt], self.bwidth[self.bt], curses.A_REVERSE) curses.curs_set(0) def _unhighlight_button(self): self.w.chgat(self.brow,1,self.mx-2,curses.A_NORMAL) def _mv_focus(self,delta): if self.buttononly: return if self.bt==-1: self.entries[self.hl].lose_focus() else: self._unhighlight_button() new = self.hl+delta new = max(0, min(len(self.labels), new)) # the extra is for the buttons self.hl = new if new == len(self.labels): self.bt = 1 self._highlight_button() else: self.bt=-1 self.entries[self.hl].gain_focus() def _return_values(self): book = {} for k,e in zip(self.labels, self.entries): if e!="" and k.lower()!="publish date": book[k.lower()]=e.value return book def handle_input(self,ch): if ch==curses.KEY_UP: self._mv_focus(-1) elif ch==curses.KEY_PPAGE: self._mv_focus(-len(self.labels)) elif ch==curses.KEY_DOWN: self._mv_focus(+1) elif ch==curses.KEY_NPAGE: self._mv_focus(+len(self.labels)) elif ch==curses.KEY_LEFT: if self.bt==-1: self.entries[self.hl].handle_input(ch) else: self._unhighlight_button() self.bt=0 self._highlight_button() elif ch==curses.KEY_HOME: if self.bt==-1: self._mv_cursor(-len(self.entries[self.hl])) elif ch==curses.KEY_RIGHT: if self.bt==-1: self.entries[self.hl].handle_input(ch) else: self._unhighlight_button() self.bt=1 self._highlight_button() else: if self.bt==-1: self.entries[self.hl].handle_input(ch) class BookForm(FormWindow): caption = "Add a Book" blabel = "Add" labels = ["ISBN", "LCCN", "Title", "Subtitle", "Authors", "Edition", "Publisher", "Publish Date", "Publish Year", "Publish Month", "Publish location", "Pages", "Pagination", "Weight"] # redefineable functions lookup is called when 'enter' is pressed on ISBN # and returns the looked-up book. Default returns nothing def lookup_isbn(self,isbn): return {'isbn':isbn} def lookup_lccn(self,lccn): return {'lccn':lccn} def return_book(self): return self._return_values() def handle_input(self,ch): if ch==10 or ch==curses.KEY_ENTER: if self.hl==0: # lookup by isbn book = self.lookup_isbn(self.entries[0].value) if book != {}: self._set_entries(book) self.refresh() self._mv_focus(+12) if self.hl==1: # lookup by lccn book = self.lookup_lccn(self.entries[1].value) if book != {}: self._set_entries(book) self.refresh() self._mv_focus(+11) else: FormWindow.handle_input(self,ch) class BookView(BookForm): blabel = "Done" buttononly = True labels = ["ISBN", "LCCN", "Title", "Subtitle", "Authors", "Edition", "Publisher", "Publish Date", "Publish Year", "Publish Month", "Publish location", "Pages", "Pagination", "Weight", "Categories", "Status"] def __init__(self, window, helpbar, book, width=50): book = copy(book) book["categories"] = " - ".join([cat["category"] for cat in db.getBookCategories(book)]) status = db.get_checkout_status(book["id"]) if status: book["status"] = "Checked out by %s (%s)" % (status["uwid"], status["date_out"]) else: book["status"] = "On shelf" self.caption = "Viewing Book " + str(book["id"]) super().__init__(window, helpbar, book, width) class CategoryForm(FormWindow): caption = "Add a Category" blabel = "Add" labels = ["Category"] def _return_values(self): return self.entries[0].value class InfoForm(FormWindow): caption = "Info" blabel = "OK" buttononly = True def __init__(self,caption,window,helpbar,errortext,width=50): self.labels = errortext.split("\n") self.caption = caption super().__init__(window, helpbar, width=width) def redraw(self): self.w.box() self.w.addstr(0,(self.mx-len(self.caption))//2,self.caption) r=0 for l in self.labels: c = 2 self.w.addstr(r+self.top,c,l) r+=1 self.w.addstr(self.brow,self.bcol[1], "<"+self.blabel+">") self.w.chgat(self.brow, self.bcol[1], len(self.blabel)+2, curses.A_REVERSE) self.w.refresh() def _mv_focus(self,delta): pass class ErrorForm(InfoForm): def __init__(self,window,helpbar,errortext,width=50): super().__init__("Error", window, helpbar, errortext, width) class SuccessForm(InfoForm): def __init__(self,window,helpbar,errortext,width=50): super().__init__("Success", window, helpbar, errortext, width) # Any good news is great news in the world of the librarian. self.blabel = "Great News!" def error_form(text, w, hb): width = max([len(l) for l in text.split("\n")]) + 4 child=curses.newwin(1,1) f = ErrorForm(child, hb, text, width) (my,mx)=w.getmaxyx() (r,c)=child.getmaxyx() child.mvwin((my-r)//2,(mx-c)//2) w.refresh() f.event_loop() f.clear() def catch_error_with(getwhb): def decorator(fn): def wrapper_fun(*args, **kwd): try: return fn(*args, **kwd) except LibrarianException as e: w, hb, cleanup = getwhb(*args, **kwd) error_form(str(e), w, hb) if cleanup: cleanup() return wrapper_fun return decorator catch_error = catch_error_with(lambda self, *args, **kwd : (self.w, self.hb, self.refresh))