#!/usr/bin/python import curses import curses.ascii import sys import locale #import curses.wrapper from . import wgwidget as widget from . import npysGlobalOptions as GlobalOptions class TextfieldBase(widget.Widget): ENSURE_STRING_VALUE = True def __init__(self, screen, value='', highlight_color='CURSOR', highlight_whole_widget=False, invert_highlight_color=True, **keywords): try: self.value = value or "" except: self.value = "" super(TextfieldBase, self).__init__(screen, **keywords) if GlobalOptions.ASCII_ONLY or locale.getpreferredencoding() == 'US-ASCII': self._force_ascii = True else: self._force_ascii = False self.cursor_position = False self.highlight_color = highlight_color self.highlight_whole_widget = highlight_whole_widget self.invert_highlight_color = invert_highlight_color self.show_bold = False self.highlight = False self.important = False self.syntax_highlighting = False self._highlightingdata = None self.left_margin = 0 self.begin_at = 0 # Where does the display string begin? self.set_text_widths() self.update() def set_text_widths(self): if self.on_last_line: self.maximum_string_length = self.width - 2 # Leave room for the cursor else: self.maximum_string_length = self.width - 1 # Leave room for the cursor at the end of the string. def resize(self): self.set_text_widths() def calculate_area_needed(self): "Need one line of screen, and any width going" return 1,0 def update(self, clear=True, cursor=True): """Update the contents of the textbox, without calling the final refresh to the screen""" # cursor not working. See later for a fake cursor #if self.editing: pmfuncs.show_cursor() #else: pmfuncs.hide_cursor() # Not needed here -- gets called too much! #pmfuncs.hide_cursor() if clear: self.clear() if self.hidden: return True value_to_use_for_calculations = self.value if self.ENSURE_STRING_VALUE: if value_to_use_for_calculations in (None, False, True): value_to_use_for_calculations = '' self.value = '' if self.begin_at < 0: self.begin_at = 0 if self.left_margin >= self.maximum_string_length: raise ValueError if self.editing: if isinstance(self.value, bytes): # use a unicode version of self.value to work out where the cursor is. # not always accurate, but better than the bytes value_to_use_for_calculations = self.display_value(self.value).decode(self.encoding, 'replace') if cursor: if self.cursor_position is False: self.cursor_position = len(value_to_use_for_calculations) elif self.cursor_position > len(value_to_use_for_calculations): self.cursor_position = len(value_to_use_for_calculations) elif self.cursor_position < 0: self.cursor_position = 0 if self.cursor_position < self.begin_at: self.begin_at = self.cursor_position while self.cursor_position > self.begin_at + self.maximum_string_length - self.left_margin: # -1: self.begin_at += 1 else: if self.do_colors(): self.parent.curses_pad.bkgdset(' ', self.parent.theme_manager.findPair(self, self.highlight_color) | curses.A_STANDOUT) else: self.parent.curses_pad.bkgdset(' ',curses.A_STANDOUT) # Do this twice so that the _print method can ignore it if needed. if self.highlight: if self.do_colors(): if self.invert_highlight_color: attributes=self.parent.theme_manager.findPair(self, self.highlight_color) | curses.A_STANDOUT else: attributes=self.parent.theme_manager.findPair(self, self.highlight_color) self.parent.curses_pad.bkgdset(' ', attributes) else: self.parent.curses_pad.bkgdset(' ',curses.A_STANDOUT) if self.show_bold: self.parent.curses_pad.attron(curses.A_BOLD) if self.important and not self.do_colors(): self.parent.curses_pad.attron(curses.A_UNDERLINE) self._print() # reset everything to normal self.parent.curses_pad.attroff(curses.A_BOLD) self.parent.curses_pad.attroff(curses.A_UNDERLINE) self.parent.curses_pad.bkgdset(' ',curses.A_NORMAL) self.parent.curses_pad.attrset(0) if self.editing and cursor: self.print_cursor() def print_cursor(self): # This needs fixing for Unicode multi-width chars. # Cursors do not seem to work on pads. #self.parent.curses_pad.move(self.rely, self.cursor_position - self.begin_at) # let's have a fake cursor _cur_loc_x = self.cursor_position - self.begin_at + self.relx + self.left_margin # The following two lines work fine for ascii, but not for unicode #char_under_cur = self.parent.curses_pad.inch(self.rely, _cur_loc_x) #self.parent.curses_pad.addch(self.rely, self.cursor_position - self.begin_at + self.relx, char_under_cur, curses.A_STANDOUT) #The following appears to work for unicode as well. try: #char_under_cur = self.value[self.cursor_position] #use the real value char_under_cur = self._get_string_to_print()[self.cursor_position] char_under_cur = self.safe_string(char_under_cur) except IndexError: char_under_cur = ' ' except TypeError: char_under_cur = ' ' if self.do_colors(): self.parent.curses_pad.addstr(self.rely, self.cursor_position - self.begin_at + self.relx + self.left_margin, char_under_cur, self.parent.theme_manager.findPair(self, 'CURSOR_INVERSE')) else: self.parent.curses_pad.addstr(self.rely, self.cursor_position - self.begin_at + self.relx + self.left_margin, char_under_cur, curses.A_STANDOUT) def print_cursor_pre_unicode(self): # Cursors do not seem to work on pads. #self.parent.curses_pad.move(self.rely, self.cursor_position - self.begin_at) # let's have a fake cursor _cur_loc_x = self.cursor_position - self.begin_at + self.relx + self.left_margin # The following two lines work fine for ascii, but not for unicode #char_under_cur = self.parent.curses_pad.inch(self.rely, _cur_loc_x) #self.parent.curses_pad.addch(self.rely, self.cursor_position - self.begin_at + self.relx, char_under_cur, curses.A_STANDOUT) #The following appears to work for unicode as well. try: char_under_cur = self.display_value(self.value)[self.cursor_position] except: char_under_cur = ' ' self.parent.curses_pad.addstr(self.rely, self.cursor_position - self.begin_at + self.relx + self.left_margin, char_under_cur, curses.A_STANDOUT) def display_value(self, value): if value == None: return '' else: try: str_value = str(value) except UnicodeEncodeError: str_value = self.safe_string(value) return str_value except ReferenceError: return ">*ERROR*ERROR*ERROR*<" return self.safe_string(str_value) def find_width_of_char(self, ch): return 1 def _print_unicode_char(self, ch): # return the ch to print. For python 3 this is just ch if self._force_ascii: return ch.encode('ascii', 'replace') elif sys.version_info[0] >= 3: return ch else: return ch.encode('utf-8', 'strict') def _get_string_to_print(self): string_to_print = self.display_value(self.value) if not string_to_print: return None string_to_print = string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] if sys.version_info[0] >= 3: string_to_print = self.display_value(self.value)[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] else: # ensure unicode only here encoding here. dv = self.display_value(self.value) if isinstance(dv, bytes): dv = dv.decode(self.encoding, 'replace') string_to_print = dv[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] return string_to_print def _print(self): string_to_print = self._get_string_to_print() if not string_to_print: return None string_to_print = string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] if sys.version_info[0] >= 3: string_to_print = self.display_value(self.value)[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] else: # ensure unicode only here encoding here. dv = self.display_value(self.value) if isinstance(dv, bytes): dv = dv.decode(self.encoding, 'replace') string_to_print = dv[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] column = 0 place_in_string = 0 if self.syntax_highlighting: self.update_highlighting(start=self.begin_at, end=self.maximum_string_length+self.begin_at-self.left_margin) while column <= (self.maximum_string_length - self.left_margin): if not string_to_print or place_in_string > len(string_to_print)-1: break width_of_char_to_print = self.find_width_of_char(string_to_print[place_in_string]) if column - 1 + width_of_char_to_print > self.maximum_string_length: break try: highlight = self._highlightingdata[self.begin_at+place_in_string] except: highlight = curses.A_NORMAL self.parent.curses_pad.addstr(self.rely,self.relx+column+self.left_margin, self._print_unicode_char(string_to_print[place_in_string]), highlight ) column += self.find_width_of_char(string_to_print[place_in_string]) place_in_string += 1 else: if self.do_colors(): if self.show_bold and self.color == 'DEFAULT': color = self.parent.theme_manager.findPair(self, 'BOLD') | curses.A_BOLD elif self.show_bold: color = self.parent.theme_manager.findPair(self, self.color) | curses.A_BOLD elif self.important: color = self.parent.theme_manager.findPair(self, 'IMPORTANT') | curses.A_BOLD else: color = self.parent.theme_manager.findPair(self) else: if self.important or self.show_bold: color = curses.A_BOLD else: color = curses.A_NORMAL while column <= (self.maximum_string_length - self.left_margin): if not string_to_print or place_in_string > len(string_to_print)-1: if self.highlight_whole_widget: self.parent.curses_pad.addstr(self.rely,self.relx+column+self.left_margin, ' ', color ) column += width_of_char_to_print place_in_string += 1 continue else: break width_of_char_to_print = self.find_width_of_char(string_to_print[place_in_string]) if column - 1 + width_of_char_to_print > self.maximum_string_length: break self.parent.curses_pad.addstr(self.rely,self.relx+column+self.left_margin, self._print_unicode_char(string_to_print[place_in_string]), color ) column += width_of_char_to_print place_in_string += 1 def _print_pre_unicode(self): # This method was used to print the string before we became interested in unicode. string_to_print = self.display_value(self.value) if string_to_print == None: return if self.syntax_highlighting: self.update_highlighting(start=self.begin_at, end=self.maximum_string_length+self.begin_at-self.left_margin) for i in range(len(string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin])): try: highlight = self._highlightingdata[self.begin_at+i] except: highlight = curses.A_NORMAL self.parent.curses_pad.addstr(self.rely,self.relx+i+self.left_margin, string_to_print[self.begin_at+i], highlight ) elif self.do_colors(): coltofind = 'DEFAULT' if self.show_bold and self.color == 'DEFAULT': coltofind = 'BOLD' if self.show_bold: self.parent.curses_pad.addstr(self.rely,self.relx+self.left_margin, string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin], self.parent.theme_manager.findPair(self, coltofind) | curses.A_BOLD) elif self.important: coltofind = 'IMPORTANT' self.parent.curses_pad.addstr(self.rely,self.relx+self.left_margin, string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin], self.parent.theme_manager.findPair(self, coltofind) | curses.A_BOLD) else: self.parent.curses_pad.addstr(self.rely,self.relx+self.left_margin, string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin], self.parent.theme_manager.findPair(self)) else: if self.important: self.parent.curses_pad.addstr(self.rely,self.relx+self.left_margin, string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin], curses.A_BOLD) elif self.show_bold: self.parent.curses_pad.addstr(self.rely,self.relx+self.left_margin, string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin], curses.A_BOLD) else: self.parent.curses_pad.addstr(self.rely,self.relx+self.left_margin, string_to_print[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin]) def update_highlighting(self, start=None, end=None, clear=False): if clear or (self._highlightingdata == None): self._highlightingdata = [] string_to_print = self.display_value(self.value) class Textfield(TextfieldBase): def show_brief_message(self, message): curses.beep() keep_for_a_moment = self.value self.value = message self.editing=False self.display() curses.napms(1200) self.editing=True self.value = keep_for_a_moment def edit(self): self.editing = 1 if self.cursor_position is False: self.cursor_position = len(self.value or '') self.parent.curses_pad.keypad(1) self.old_value = self.value self.how_exited = False while self.editing: self.display() self.get_and_use_key_press() self.begin_at = 0 self.display() self.cursor_position = False return self.how_exited, self.value ########################################################################################### # Handlers and methods def set_up_handlers(self): super(Textfield, self).set_up_handlers() # For OS X del_key = curses.ascii.alt('~') self.handlers.update({curses.KEY_LEFT: self.h_cursor_left, curses.KEY_RIGHT: self.h_cursor_right, curses.KEY_DC: self.h_delete_right, curses.ascii.DEL: self.h_delete_left, curses.ascii.BS: self.h_delete_left, curses.KEY_BACKSPACE: self.h_delete_left, # mac os x curses reports DEL as escape oddly # no solution yet "^K": self.h_erase_right, "^U": self.h_erase_left, }) self.complex_handlers.extend(( (self.t_input_isprint, self.h_addch), # (self.t_is_ck, self.h_erase_right), # (self.t_is_cu, self.h_erase_left), )) def t_input_isprint(self, inp): if self._last_get_ch_was_unicode and inp not in '\n\t\r': return True if curses.ascii.isprint(inp) and \ (chr(inp) not in '\n\t\r'): return True else: return False def h_addch(self, inp): if self.editable: #self.value = self.value[:self.cursor_position] + curses.keyname(input) \ # + self.value[self.cursor_position:] #self.cursor_position += len(curses.keyname(input)) # workaround for the metamode bug: if self._last_get_ch_was_unicode == True and isinstance(self.value, bytes): # probably dealing with python2. ch_adding = inp self.value = self.value.decode() elif self._last_get_ch_was_unicode == True: ch_adding = inp else: try: ch_adding = chr(inp) except TypeError: ch_adding = input self.value = self.value[:self.cursor_position] + ch_adding \ + self.value[self.cursor_position:] self.cursor_position += len(ch_adding) # or avoid it entirely: #self.value = self.value[:self.cursor_position] + curses.ascii.unctrl(input) \ # + self.value[self.cursor_position:] #self.cursor_position += len(curses.ascii.unctrl(input)) def h_cursor_left(self, input): self.cursor_position -= 1 def h_cursor_right(self, input): self.cursor_position += 1 def h_delete_left(self, input): if self.editable and self.cursor_position > 0: self.value = self.value[:self.cursor_position-1] + self.value[self.cursor_position:] self.cursor_position -= 1 self.begin_at -= 1 def h_delete_right(self, input): if self.editable: self.value = self.value[:self.cursor_position] + self.value[self.cursor_position+1:] def h_erase_left(self, input): if self.editable: self.value = self.value[self.cursor_position:] self.cursor_position=0 def h_erase_right(self, input): if self.editable: self.value = self.value[:self.cursor_position] self.cursor_position = len(self.value) self.begin_at = 0 def handle_mouse_event(self, mouse_event): #mouse_id, x, y, z, bstate = mouse_event #rel_mouse_x = x - self.relx - self.parent.show_atx mouse_id, rel_x, rel_y, z, bstate = self.interpret_mouse_event(mouse_event) self.cursor_position = rel_x + self.begin_at self.display() class FixedText(TextfieldBase): def set_up_handlers(self): super(FixedText, self).set_up_handlers() self.handlers.update({curses.KEY_LEFT: self.h_cursor_left, curses.KEY_RIGHT: self.h_cursor_right, ord('k'): self.h_exit_up, ord('j'): self.h_exit_down, }) def h_cursor_left(self, input): if self.begin_at > 0: self.begin_at -= 1 def h_cursor_right(self, input): if len(self.value) - self.begin_at > self.maximum_string_length: self.begin_at += 1 def update(self, clear=True,): super(FixedText, self).update(clear=clear, cursor=False) def edit(self): self.editing = 1 self.highlight = False self.cursor_position = 0 self.parent.curses_pad.keypad(1) self.old_value = self.value self.how_exited = False while self.editing: self.display() self.get_and_use_key_press() self.begin_at = 0 self.highlight = False self.display() return self.how_exited, self.value