class ShiftReduceApp(object): """ A graphical tool for exploring the shift-reduce parser. The tool displays the parser's stack and the remaining text, and allows the user to control the parser's operation. In particular, the user can shift tokens onto the stack, and can perform reductions on the top elements of the stack. A "step" button simply steps through the parsing process, performing the operations that ``nltk.parse.ShiftReduceParser`` would use. """ def __init__(self, grammar, sent, trace=0): self._sent = sent self._parser = SteppingShiftReduceParser(grammar, trace) # Set up the main window. self._top = Tk() self._top.title('Shift Reduce Parser Application') # Animations. animating_lock is a lock to prevent the demo # from performing new operations while it's animating. self._animating_lock = 0 self._animate = IntVar(self._top) self._animate.set(10) # = medium # The user can hide the grammar. self._show_grammar = IntVar(self._top) self._show_grammar.set(1) # Initialize fonts. self._init_fonts(self._top) # Set up key bindings. self._init_bindings() # Create the basic frames. self._init_menubar(self._top) self._init_buttons(self._top) self._init_feedback(self._top) self._init_grammar(self._top) self._init_canvas(self._top) # A popup menu for reducing. self._reduce_menu = Menu(self._canvas, tearoff=0) # Reset the demo, and set the feedback frame to empty. self.reset() self._lastoper1['text'] = '' ######################################### ## Initialization Helpers ######################################### def _init_fonts(self, root): # See: <http://www.astro.washington.edu/owen/ROTKFolklore.html> self._sysfont = tkinter.font.Font(font=Button()["font"]) root.option_add("*Font", self._sysfont) # TWhat's our font size (default=same as sysfont) self._size = IntVar(root) self._size.set(self._sysfont.cget('size')) self._boldfont = tkinter.font.Font(family='helvetica', weight='bold', size=self._size.get()) self._font = tkinter.font.Font(family='helvetica', size=self._size.get()) def _init_grammar(self, parent): # Grammar view. self._prodframe = listframe = Frame(parent) self._prodframe.pack(fill='both', side='left', padx=2) self._prodlist_label = Label(self._prodframe, font=self._boldfont, text='Available Reductions') self._prodlist_label.pack() self._prodlist = Listbox(self._prodframe, selectmode='single', relief='groove', background='white', foreground='#909090', font=self._font, selectforeground='#004040', selectbackground='#c0f0c0') self._prodlist.pack(side='right', fill='both', expand=1) self._productions = list(self._parser.grammar().productions()) for production in self._productions: self._prodlist.insert('end', (' %s' % production)) self._prodlist.config(height=min(len(self._productions), 25)) # Add a scrollbar if there are more than 25 productions. if 1:#len(self._productions) > 25: listscroll = Scrollbar(self._prodframe, orient='vertical') self._prodlist.config(yscrollcommand = listscroll.set) listscroll.config(command=self._prodlist.yview) listscroll.pack(side='left', fill='y') # If they select a production, apply it. self._prodlist.bind('<<ListboxSelect>>', self._prodlist_select) # When they hover over a production, highlight it. self._hover = -1 self._prodlist.bind('<Motion>', self._highlight_hover) self._prodlist.bind('<Leave>', self._clear_hover) def _init_bindings(self): # Quit self._top.bind('<Control-q>', self.destroy) self._top.bind('<Control-x>', self.destroy) self._top.bind('<Alt-q>', self.destroy) self._top.bind('<Alt-x>', self.destroy) # Ops (step, shift, reduce, undo) self._top.bind('<space>', self.step) self._top.bind('<s>', self.shift) self._top.bind('<Alt-s>', self.shift) self._top.bind('<Control-s>', self.shift) self._top.bind('<r>', self.reduce) self._top.bind('<Alt-r>', self.reduce) self._top.bind('<Control-r>', self.reduce) self._top.bind('<Delete>', self.reset) self._top.bind('<u>', self.undo) self._top.bind('<Alt-u>', self.undo) self._top.bind('<Control-u>', self.undo) self._top.bind('<Control-z>', self.undo) self._top.bind('<BackSpace>', self.undo) # Misc self._top.bind('<Control-p>', self.postscript) self._top.bind('<Control-h>', self.help) self._top.bind('<F1>', self.help) self._top.bind('<Control-g>', self.edit_grammar) self._top.bind('<Control-t>', self.edit_sentence) # Animation speed control self._top.bind('-', lambda e,a=self._animate:a.set(20)) self._top.bind('=', lambda e,a=self._animate:a.set(10)) self._top.bind('+', lambda e,a=self._animate:a.set(4)) def _init_buttons(self, parent): # Set up the frames. self._buttonframe = buttonframe = Frame(parent) buttonframe.pack(fill='none', side='bottom') Button(buttonframe, text='Step', background='#90c0d0', foreground='black', command=self.step,).pack(side='left') Button(buttonframe, text='Shift', underline=0, background='#90f090', foreground='black', command=self.shift).pack(side='left') Button(buttonframe, text='Reduce', underline=0, background='#90f090', foreground='black', command=self.reduce).pack(side='left') Button(buttonframe, text='Undo', underline=0, background='#f0a0a0', foreground='black', command=self.undo).pack(side='left') def _init_menubar(self, parent): menubar = Menu(parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Reset Parser', underline=0, command=self.reset, accelerator='Del') filemenu.add_command(label='Print to Postscript', underline=0, command=self.postscript, accelerator='Ctrl-p') filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Menu(menubar, tearoff=0) editmenu.add_command(label='Edit Grammar', underline=5, command=self.edit_grammar, accelerator='Ctrl-g') editmenu.add_command(label='Edit Text', underline=5, command=self.edit_sentence, accelerator='Ctrl-t') menubar.add_cascade(label='Edit', underline=0, menu=editmenu) rulemenu = Menu(menubar, tearoff=0) rulemenu.add_command(label='Step', underline=1, command=self.step, accelerator='Space') rulemenu.add_separator() rulemenu.add_command(label='Shift', underline=0, command=self.shift, accelerator='Ctrl-s') rulemenu.add_command(label='Reduce', underline=0, command=self.reduce, accelerator='Ctrl-r') rulemenu.add_separator() rulemenu.add_command(label='Undo', underline=0, command=self.undo, accelerator='Ctrl-u') menubar.add_cascade(label='Apply', underline=0, menu=rulemenu) viewmenu = Menu(menubar, tearoff=0) viewmenu.add_checkbutton(label="Show Grammar", underline=0, variable=self._show_grammar, command=self._toggle_grammar) viewmenu.add_separator() viewmenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) viewmenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) viewmenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) viewmenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=18, command=self.resize) viewmenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=24, command=self.resize) menubar.add_cascade(label='View', underline=0, menu=viewmenu) animatemenu = Menu(menubar, tearoff=0) animatemenu.add_radiobutton(label="No Animation", underline=0, variable=self._animate, value=0) animatemenu.add_radiobutton(label="Slow Animation", underline=0, variable=self._animate, value=20, accelerator='-') animatemenu.add_radiobutton(label="Normal Animation", underline=0, variable=self._animate, value=10, accelerator='=') animatemenu.add_radiobutton(label="Fast Animation", underline=0, variable=self._animate, value=4, accelerator='+') menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) helpmenu.add_command(label='Instructions', underline=0, command=self.help, accelerator='F1') menubar.add_cascade(label='Help', underline=0, menu=helpmenu) parent.config(menu=menubar) def _init_feedback(self, parent): self._feedbackframe = feedbackframe = Frame(parent) feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3) self._lastoper_label = Label(feedbackframe, text='Last Operation:', font=self._font) self._lastoper_label.pack(side='left') lastoperframe = Frame(feedbackframe, relief='sunken', border=1) lastoperframe.pack(fill='x', side='right', expand=1, padx=5) self._lastoper1 = Label(lastoperframe, foreground='#007070', background='#f0f0f0', font=self._font) self._lastoper2 = Label(lastoperframe, anchor='w', width=30, foreground='#004040', background='#f0f0f0', font=self._font) self._lastoper1.pack(side='left') self._lastoper2.pack(side='left', fill='x', expand=1) def _init_canvas(self, parent): self._cframe = CanvasFrame(parent, background='white', width=525, closeenough=10, border=2, relief='sunken') self._cframe.pack(expand=1, fill='both', side='top', pady=2) canvas = self._canvas = self._cframe.canvas() self._stackwidgets = [] self._rtextwidgets = [] self._titlebar = canvas.create_rectangle(0,0,0,0, fill='#c0f0f0', outline='black') self._exprline = canvas.create_line(0,0,0,0, dash='.') self._stacktop = canvas.create_line(0,0,0,0, fill='#408080') size = self._size.get()+4 self._stacklabel = TextWidget(canvas, 'Stack', color='#004040', font=self._boldfont) self._rtextlabel = TextWidget(canvas, 'Remaining Text', color='#004040', font=self._boldfont) self._cframe.add_widget(self._stacklabel) self._cframe.add_widget(self._rtextlabel) ######################################### ## Main draw procedure ######################################### def _redraw(self): scrollregion = self._canvas['scrollregion'].split() (cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion] # Delete the old stack & rtext widgets. for stackwidget in self._stackwidgets: self._cframe.destroy_widget(stackwidget) self._stackwidgets = [] for rtextwidget in self._rtextwidgets: self._cframe.destroy_widget(rtextwidget) self._rtextwidgets = [] # Position the titlebar & exprline (x1, y1, x2, y2) = self._stacklabel.bbox() y = y2-y1+10 self._canvas.coords(self._titlebar, -5000, 0, 5000, y-4) self._canvas.coords(self._exprline, 0, y*2-10, 5000, y*2-10) # Position the titlebar labels.. (x1, y1, x2, y2) = self._stacklabel.bbox() self._stacklabel.move(5-x1, 3-y1) (x1, y1, x2, y2) = self._rtextlabel.bbox() self._rtextlabel.move(cx2-x2-5, 3-y1) # Draw the stack. stackx = 5 for tok in self._parser.stack(): if isinstance(tok, Tree): attribs = {'tree_color': '#4080a0', 'tree_width': 2, 'node_font': self._boldfont, 'node_color': '#006060', 'leaf_color': '#006060', 'leaf_font':self._font} widget = tree_to_treesegment(self._canvas, tok, **attribs) widget.label()['color'] = '#000000' else: widget = TextWidget(self._canvas, tok, color='#000000', font=self._font) widget.bind_click(self._popup_reduce) self._stackwidgets.append(widget) self._cframe.add_widget(widget, stackx, y) stackx = widget.bbox()[2] + 10 # Draw the remaining text. rtextwidth = 0 for tok in self._parser.remaining_text(): widget = TextWidget(self._canvas, tok, color='#000000', font=self._font) self._rtextwidgets.append(widget) self._cframe.add_widget(widget, rtextwidth, y) rtextwidth = widget.bbox()[2] + 4 # Allow enough room to shift the next token (for animations) if len(self._rtextwidgets) > 0: stackx += self._rtextwidgets[0].width() # Move the remaining text to the correct location (keep it # right-justified, when possible); and move the remaining text # label, if necessary. stackx = max(stackx, self._stacklabel.width()+25) rlabelwidth = self._rtextlabel.width()+10 if stackx >= cx2-max(rtextwidth, rlabelwidth): cx2 = stackx + max(rtextwidth, rlabelwidth) for rtextwidget in self._rtextwidgets: rtextwidget.move(4+cx2-rtextwidth, 0) self._rtextlabel.move(cx2-self._rtextlabel.bbox()[2]-5, 0) midx = (stackx + cx2-max(rtextwidth, rlabelwidth))/2 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) (x1, y1, x2, y2) = self._stacklabel.bbox() # Set up binding to allow them to shift a token by dragging it. if len(self._rtextwidgets) > 0: def drag_shift(widget, midx=midx, self=self): if widget.bbox()[0] < midx: self.shift() else: self._redraw() self._rtextwidgets[0].bind_drag(drag_shift) self._rtextwidgets[0].bind_click(self.shift) # Draw the stack top. self._highlight_productions() def _draw_stack_top(self, widget): # hack.. midx = widget.bbox()[2]+50 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) def _highlight_productions(self): # Highlight the productions that can be reduced. self._prodlist.selection_clear(0, 'end') for prod in self._parser.reducible_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) ######################################### ## Button Callbacks ######################################### def destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None def reset(self, *e): self._parser.initialize(self._sent) self._lastoper1['text'] = 'Reset App' self._lastoper2['text'] = '' self._redraw() def step(self, *e): if self.reduce(): return True elif self.shift(): return True else: if list(self._parser.parses()): self._lastoper1['text'] = 'Finished:' self._lastoper2['text'] = 'Success' else: self._lastoper1['text'] = 'Finished:' self._lastoper2['text'] = 'Failure' def shift(self, *e): if self._animating_lock: return if self._parser.shift(): tok = self._parser.stack()[-1] self._lastoper1['text'] = 'Shift:' self._lastoper2['text'] = '%r' % tok if self._animate.get(): self._animate_shift() else: self._redraw() return True return False def reduce(self, *e): if self._animating_lock: return production = self._parser.reduce() if production: self._lastoper1['text'] = 'Reduce:' self._lastoper2['text'] = '%s' % production if self._animate.get(): self._animate_reduce() else: self._redraw() return production def undo(self, *e): if self._animating_lock: return if self._parser.undo(): self._redraw() def postscript(self, *e): self._cframe.print_to_file() def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) ######################################### ## Menubar callbacks ######################################### def resize(self, size=None): if size is not None: self._size.set(size) size = self._size.get() self._font.configure(size=-(abs(size))) self._boldfont.configure(size=-(abs(size))) self._sysfont.configure(size=-(abs(size))) #self._stacklabel['font'] = ('helvetica', -size-4, 'bold') #self._rtextlabel['font'] = ('helvetica', -size-4, 'bold') #self._lastoper_label['font'] = ('helvetica', -size) #self._lastoper1['font'] = ('helvetica', -size) #self._lastoper2['font'] = ('helvetica', -size) #self._prodlist['font'] = ('helvetica', -size) #self._prodlist_label['font'] = ('helvetica', -size-2, 'bold') self._redraw() def help(self, *e): # The default font's not very legible; try using 'fixed' instead. try: ShowText(self._top, 'Help: Shift-Reduce Parser Application', (__doc__ or '').strip(), width=75, font='fixed') except: ShowText(self._top, 'Help: Shift-Reduce Parser Application', (__doc__ or '').strip(), width=75) def about(self, *e): ABOUT = ("NLTK Shift-Reduce Parser Application\n"+ "Written by Edward Loper") TITLE = 'About: Shift-Reduce Parser Application' try: from tkinter.messagebox import Message Message(message=ABOUT, title=TITLE).show() except: ShowText(self._top, TITLE, ABOUT) def edit_grammar(self, *e): CFGEditor(self._top, self._parser.grammar(), self.set_grammar) def set_grammar(self, grammar): self._parser.set_grammar(grammar) self._productions = list(grammar.productions()) self._prodlist.delete(0, 'end') for production in self._productions: self._prodlist.insert('end', (' %s' % production)) def edit_sentence(self, *e): sentence = " ".join(self._sent) title = 'Edit Text' instr = 'Enter a new sentence to parse.' EntryDialog(self._top, sentence, instr, self.set_sentence, title) def set_sentence(self, sent): self._sent = sent.split() #[XX] use tagged? self.reset() ######################################### ## Reduce Production Selection ######################################### def _toggle_grammar(self, *e): if self._show_grammar.get(): self._prodframe.pack(fill='both', side='left', padx=2, after=self._feedbackframe) self._lastoper1['text'] = 'Show Grammar' else: self._prodframe.pack_forget() self._lastoper1['text'] = 'Hide Grammar' self._lastoper2['text'] = '' def _prodlist_select(self, event): selection = self._prodlist.curselection() if len(selection) != 1: return index = int(selection[0]) production = self._parser.reduce(self._productions[index]) if production: self._lastoper1['text'] = 'Reduce:' self._lastoper2['text'] = '%s' % production if self._animate.get(): self._animate_reduce() else: self._redraw() else: # Reset the production selections. self._prodlist.selection_clear(0, 'end') for prod in self._parser.reducible_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) def _popup_reduce(self, widget): # Remove old commands. productions = self._parser.reducible_productions() if len(productions) == 0: return self._reduce_menu.delete(0, 'end') for production in productions: self._reduce_menu.add_command(label=str(production), command=self.reduce) self._reduce_menu.post(self._canvas.winfo_pointerx(), self._canvas.winfo_pointery()) ######################################### ## Animations ######################################### def _animate_shift(self): # What widget are we shifting? widget = self._rtextwidgets[0] # Where are we shifting from & to? right = widget.bbox()[0] if len(self._stackwidgets) == 0: left = 5 else: left = self._stackwidgets[-1].bbox()[2]+10 # Start animating. dt = self._animate.get() dx = (left-right)*1.0/dt self._animate_shift_frame(dt, widget, dx) def _animate_shift_frame(self, frame, widget, dx): if frame > 0: self._animating_lock = 1 widget.move(dx, 0) self._top.after(10, self._animate_shift_frame, frame-1, widget, dx) else: # but: stacktop?? # Shift the widget to the stack. del self._rtextwidgets[0] self._stackwidgets.append(widget) self._animating_lock = 0 # Display the available productions. self._draw_stack_top(widget) self._highlight_productions() def _animate_reduce(self): # What widgets are we shifting? numwidgets = len(self._parser.stack()[-1]) # number of children widgets = self._stackwidgets[-numwidgets:] # How far are we moving? if isinstance(widgets[0], TreeSegmentWidget): ydist = 15 + widgets[0].label().height() else: ydist = 15 + widgets[0].height() # Start animating. dt = self._animate.get() dy = ydist*2.0/dt self._animate_reduce_frame(dt/2, widgets, dy) def _animate_reduce_frame(self, frame, widgets, dy): if frame > 0: self._animating_lock = 1 for widget in widgets: widget.move(0, dy) self._top.after(10, self._animate_reduce_frame, frame-1, widgets, dy) else: del self._stackwidgets[-len(widgets):] for widget in widgets: self._cframe.remove_widget(widget) tok = self._parser.stack()[-1] if not isinstance(tok, Tree): raise ValueError() label = TextWidget(self._canvas, str(tok.label()), color='#006060', font=self._boldfont) widget = TreeSegmentWidget(self._canvas, label, widgets, width=2) (x1, y1, x2, y2) = self._stacklabel.bbox() y = y2-y1+10 if not self._stackwidgets: x = 5 else: x = self._stackwidgets[-1].bbox()[2] + 10 self._cframe.add_widget(widget, x, y) self._stackwidgets.append(widget) # Display the available productions. self._draw_stack_top(widget) self._highlight_productions() # # Delete the old widgets.. # del self._stackwidgets[-len(widgets):] # for widget in widgets: # self._cframe.destroy_widget(widget) # # # Make a new one. # tok = self._parser.stack()[-1] # if isinstance(tok, Tree): # attribs = {'tree_color': '#4080a0', 'tree_width': 2, # 'node_font': bold, 'node_color': '#006060', # 'leaf_color': '#006060', 'leaf_font':self._font} # widget = tree_to_treesegment(self._canvas, tok.type(), # **attribs) # widget.node()['color'] = '#000000' # else: # widget = TextWidget(self._canvas, tok.type(), # color='#000000', font=self._font) # widget.bind_click(self._popup_reduce) # (x1, y1, x2, y2) = self._stacklabel.bbox() # y = y2-y1+10 # if not self._stackwidgets: x = 5 # else: x = self._stackwidgets[-1].bbox()[2] + 10 # self._cframe.add_widget(widget, x, y) # self._stackwidgets.append(widget) #self._redraw() self._animating_lock = 0 ######################################### ## Hovering. ######################################### def _highlight_hover(self, event): # What production are we hovering over? index = self._prodlist.nearest(event.y) if self._hover == index: return # Clear any previous hover highlighting. self._clear_hover() # If the production corresponds to an available reduction, # highlight the stack. selection = [int(s) for s in self._prodlist.curselection()] if index in selection: rhslen = len(self._productions[index].rhs()) for stackwidget in self._stackwidgets[-rhslen:]: if isinstance(stackwidget, TreeSegmentWidget): stackwidget.label()['color'] = '#00a000' else: stackwidget['color'] = '#00a000' # Remember what production we're hovering over. self._hover = index def _clear_hover(self, *event): # Clear any previous hover highlighting. if self._hover == -1: return self._hover = -1 for stackwidget in self._stackwidgets: if isinstance(stackwidget, TreeSegmentWidget): stackwidget.label()['color'] = 'black' else: stackwidget['color'] = 'black'
class ShiftReduceApp(object): """ A graphical tool for exploring the shift-reduce parser. The tool displays the parser's stack and the remaining text, and allows the user to control the parser's operation. In particular, the user can shift tokens onto the stack, and can perform reductions on the top elements of the stack. A "step" button simply steps through the parsing process, performing the operations that ``nltk.parse.ShiftReduceParser`` would use. """ def __init__(self, grammar, sent, trace=0): self._sent = sent self._parser = SteppingShiftReduceParser(grammar, trace) # Set up the main window. self._top = Tk() self._top.title('Shift Reduce Parser Application') # Animations. animating_lock is a lock to prevent the demo # from performing new operations while it's animating. self._animating_lock = 0 self._animate = IntVar(self._top) self._animate.set(10) # = medium # The user can hide the grammar. self._show_grammar = IntVar(self._top) self._show_grammar.set(1) # Initialize fonts. self._init_fonts(self._top) # Set up key bindings. self._init_bindings() # Create the basic frames. self._init_menubar(self._top) self._init_buttons(self._top) self._init_feedback(self._top) self._init_grammar(self._top) self._init_canvas(self._top) # A popup menu for reducing. self._reduce_menu = Menu(self._canvas, tearoff=0) # Reset the demo, and set the feedback frame to empty. self.reset() self._lastoper1['text'] = '' ######################################### ## Initialization Helpers ######################################### def _init_fonts(self, root): # See: <http://www.astro.washington.edu/owen/ROTKFolklore.html> self._sysfont = tkinter.font.Font(font=Button()["font"]) root.option_add("*Font", self._sysfont) # TWhat's our font size (default=same as sysfont) self._size = IntVar(root) self._size.set(self._sysfont.cget('size')) self._boldfont = tkinter.font.Font(family='helvetica', weight='bold', size=self._size.get()) self._font = tkinter.font.Font(family='helvetica', size=self._size.get()) def _init_grammar(self, parent): # Grammar view. self._prodframe = listframe = Frame(parent) self._prodframe.pack(fill='both', side='left', padx=2) self._prodlist_label = Label(self._prodframe, font=self._boldfont, text='Available Reductions') self._prodlist_label.pack() self._prodlist = Listbox(self._prodframe, selectmode='single', relief='groove', background='white', foreground='#909090', font=self._font, selectforeground='#004040', selectbackground='#c0f0c0') self._prodlist.pack(side='right', fill='both', expand=1) self._productions = list(self._parser.grammar().productions()) for production in self._productions: self._prodlist.insert('end', (' %s' % production)) self._prodlist.config(height=min(len(self._productions), 25)) # Add a scrollbar if there are more than 25 productions. if 1: #len(self._productions) > 25: listscroll = Scrollbar(self._prodframe, orient='vertical') self._prodlist.config(yscrollcommand=listscroll.set) listscroll.config(command=self._prodlist.yview) listscroll.pack(side='left', fill='y') # If they select a production, apply it. self._prodlist.bind('<<ListboxSelect>>', self._prodlist_select) # When they hover over a production, highlight it. self._hover = -1 self._prodlist.bind('<Motion>', self._highlight_hover) self._prodlist.bind('<Leave>', self._clear_hover) def _init_bindings(self): # Quit self._top.bind('<Control-q>', self.destroy) self._top.bind('<Control-x>', self.destroy) self._top.bind('<Alt-q>', self.destroy) self._top.bind('<Alt-x>', self.destroy) # Ops (step, shift, reduce, undo) self._top.bind('<space>', self.step) self._top.bind('<s>', self.shift) self._top.bind('<Alt-s>', self.shift) self._top.bind('<Control-s>', self.shift) self._top.bind('<r>', self.reduce) self._top.bind('<Alt-r>', self.reduce) self._top.bind('<Control-r>', self.reduce) self._top.bind('<Delete>', self.reset) self._top.bind('<u>', self.undo) self._top.bind('<Alt-u>', self.undo) self._top.bind('<Control-u>', self.undo) self._top.bind('<Control-z>', self.undo) self._top.bind('<BackSpace>', self.undo) # Misc self._top.bind('<Control-p>', self.postscript) self._top.bind('<Control-h>', self.help) self._top.bind('<F1>', self.help) self._top.bind('<Control-g>', self.edit_grammar) self._top.bind('<Control-t>', self.edit_sentence) # Animation speed control self._top.bind('-', lambda e, a=self._animate: a.set(20)) self._top.bind('=', lambda e, a=self._animate: a.set(10)) self._top.bind('+', lambda e, a=self._animate: a.set(4)) def _init_buttons(self, parent): # Set up the frames. self._buttonframe = buttonframe = Frame(parent) buttonframe.pack(fill='none', side='bottom') Button( buttonframe, text='Step', background='#90c0d0', foreground='black', command=self.step, ).pack(side='left') Button(buttonframe, text='Shift', underline=0, background='#90f090', foreground='black', command=self.shift).pack(side='left') Button(buttonframe, text='Reduce', underline=0, background='#90f090', foreground='black', command=self.reduce).pack(side='left') Button(buttonframe, text='Undo', underline=0, background='#f0a0a0', foreground='black', command=self.undo).pack(side='left') def _init_menubar(self, parent): menubar = Menu(parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Reset Parser', underline=0, command=self.reset, accelerator='Del') filemenu.add_command(label='Print to Postscript', underline=0, command=self.postscript, accelerator='Ctrl-p') filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Menu(menubar, tearoff=0) editmenu.add_command(label='Edit Grammar', underline=5, command=self.edit_grammar, accelerator='Ctrl-g') editmenu.add_command(label='Edit Text', underline=5, command=self.edit_sentence, accelerator='Ctrl-t') menubar.add_cascade(label='Edit', underline=0, menu=editmenu) rulemenu = Menu(menubar, tearoff=0) rulemenu.add_command(label='Step', underline=1, command=self.step, accelerator='Space') rulemenu.add_separator() rulemenu.add_command(label='Shift', underline=0, command=self.shift, accelerator='Ctrl-s') rulemenu.add_command(label='Reduce', underline=0, command=self.reduce, accelerator='Ctrl-r') rulemenu.add_separator() rulemenu.add_command(label='Undo', underline=0, command=self.undo, accelerator='Ctrl-u') menubar.add_cascade(label='Apply', underline=0, menu=rulemenu) viewmenu = Menu(menubar, tearoff=0) viewmenu.add_checkbutton(label="Show Grammar", underline=0, variable=self._show_grammar, command=self._toggle_grammar) viewmenu.add_separator() viewmenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) viewmenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) viewmenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) viewmenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=18, command=self.resize) viewmenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=24, command=self.resize) menubar.add_cascade(label='View', underline=0, menu=viewmenu) animatemenu = Menu(menubar, tearoff=0) animatemenu.add_radiobutton(label="No Animation", underline=0, variable=self._animate, value=0) animatemenu.add_radiobutton(label="Slow Animation", underline=0, variable=self._animate, value=20, accelerator='-') animatemenu.add_radiobutton(label="Normal Animation", underline=0, variable=self._animate, value=10, accelerator='=') animatemenu.add_radiobutton(label="Fast Animation", underline=0, variable=self._animate, value=4, accelerator='+') menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) helpmenu.add_command(label='Instructions', underline=0, command=self.help, accelerator='F1') menubar.add_cascade(label='Help', underline=0, menu=helpmenu) parent.config(menu=menubar) def _init_feedback(self, parent): self._feedbackframe = feedbackframe = Frame(parent) feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3) self._lastoper_label = Label(feedbackframe, text='Last Operation:', font=self._font) self._lastoper_label.pack(side='left') lastoperframe = Frame(feedbackframe, relief='sunken', border=1) lastoperframe.pack(fill='x', side='right', expand=1, padx=5) self._lastoper1 = Label(lastoperframe, foreground='#007070', background='#f0f0f0', font=self._font) self._lastoper2 = Label(lastoperframe, anchor='w', width=30, foreground='#004040', background='#f0f0f0', font=self._font) self._lastoper1.pack(side='left') self._lastoper2.pack(side='left', fill='x', expand=1) def _init_canvas(self, parent): self._cframe = CanvasFrame(parent, background='white', width=525, closeenough=10, border=2, relief='sunken') self._cframe.pack(expand=1, fill='both', side='top', pady=2) canvas = self._canvas = self._cframe.canvas() self._stackwidgets = [] self._rtextwidgets = [] self._titlebar = canvas.create_rectangle(0, 0, 0, 0, fill='#c0f0f0', outline='black') self._exprline = canvas.create_line(0, 0, 0, 0, dash='.') self._stacktop = canvas.create_line(0, 0, 0, 0, fill='#408080') size = self._size.get() + 4 self._stacklabel = TextWidget(canvas, 'Stack', color='#004040', font=self._boldfont) self._rtextlabel = TextWidget(canvas, 'Remaining Text', color='#004040', font=self._boldfont) self._cframe.add_widget(self._stacklabel) self._cframe.add_widget(self._rtextlabel) ######################################### ## Main draw procedure ######################################### def _redraw(self): scrollregion = self._canvas['scrollregion'].split() (cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion] # Delete the old stack & rtext widgets. for stackwidget in self._stackwidgets: self._cframe.destroy_widget(stackwidget) self._stackwidgets = [] for rtextwidget in self._rtextwidgets: self._cframe.destroy_widget(rtextwidget) self._rtextwidgets = [] # Position the titlebar & exprline (x1, y1, x2, y2) = self._stacklabel.bbox() y = y2 - y1 + 10 self._canvas.coords(self._titlebar, -5000, 0, 5000, y - 4) self._canvas.coords(self._exprline, 0, y * 2 - 10, 5000, y * 2 - 10) # Position the titlebar labels.. (x1, y1, x2, y2) = self._stacklabel.bbox() self._stacklabel.move(5 - x1, 3 - y1) (x1, y1, x2, y2) = self._rtextlabel.bbox() self._rtextlabel.move(cx2 - x2 - 5, 3 - y1) # Draw the stack. stackx = 5 for tok in self._parser.stack(): if isinstance(tok, Tree): attribs = { 'tree_color': '#4080a0', 'tree_width': 2, 'node_font': self._boldfont, 'node_color': '#006060', 'leaf_color': '#006060', 'leaf_font': self._font } widget = tree_to_treesegment(self._canvas, tok, **attribs) widget.label()['color'] = '#000000' else: widget = TextWidget(self._canvas, tok, color='#000000', font=self._font) widget.bind_click(self._popup_reduce) self._stackwidgets.append(widget) self._cframe.add_widget(widget, stackx, y) stackx = widget.bbox()[2] + 10 # Draw the remaining text. rtextwidth = 0 for tok in self._parser.remaining_text(): widget = TextWidget(self._canvas, tok, color='#000000', font=self._font) self._rtextwidgets.append(widget) self._cframe.add_widget(widget, rtextwidth, y) rtextwidth = widget.bbox()[2] + 4 # Allow enough room to shift the next token (for animations) if len(self._rtextwidgets) > 0: stackx += self._rtextwidgets[0].width() # Move the remaining text to the correct location (keep it # right-justified, when possible); and move the remaining text # label, if necessary. stackx = max(stackx, self._stacklabel.width() + 25) rlabelwidth = self._rtextlabel.width() + 10 if stackx >= cx2 - max(rtextwidth, rlabelwidth): cx2 = stackx + max(rtextwidth, rlabelwidth) for rtextwidget in self._rtextwidgets: rtextwidget.move(4 + cx2 - rtextwidth, 0) self._rtextlabel.move(cx2 - self._rtextlabel.bbox()[2] - 5, 0) midx = (stackx + cx2 - max(rtextwidth, rlabelwidth)) / 2 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) (x1, y1, x2, y2) = self._stacklabel.bbox() # Set up binding to allow them to shift a token by dragging it. if len(self._rtextwidgets) > 0: def drag_shift(widget, midx=midx, self=self): if widget.bbox()[0] < midx: self.shift() else: self._redraw() self._rtextwidgets[0].bind_drag(drag_shift) self._rtextwidgets[0].bind_click(self.shift) # Draw the stack top. self._highlight_productions() def _draw_stack_top(self, widget): # hack.. midx = widget.bbox()[2] + 50 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) def _highlight_productions(self): # Highlight the productions that can be reduced. self._prodlist.selection_clear(0, 'end') for prod in self._parser.reducible_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) ######################################### ## Button Callbacks ######################################### def destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None def reset(self, *e): self._parser.initialize(self._sent) self._lastoper1['text'] = 'Reset App' self._lastoper2['text'] = '' self._redraw() def step(self, *e): if self.reduce(): return True elif self.shift(): return True else: if list(self._parser.parses()): self._lastoper1['text'] = 'Finished:' self._lastoper2['text'] = 'Success' else: self._lastoper1['text'] = 'Finished:' self._lastoper2['text'] = 'Failure' def shift(self, *e): if self._animating_lock: return if self._parser.shift(): tok = self._parser.stack()[-1] self._lastoper1['text'] = 'Shift:' self._lastoper2['text'] = '%r' % tok if self._animate.get(): self._animate_shift() else: self._redraw() return True return False def reduce(self, *e): if self._animating_lock: return production = self._parser.reduce() if production: self._lastoper1['text'] = 'Reduce:' self._lastoper2['text'] = '%s' % production if self._animate.get(): self._animate_reduce() else: self._redraw() return production def undo(self, *e): if self._animating_lock: return if self._parser.undo(): self._redraw() def postscript(self, *e): self._cframe.print_to_file() def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) ######################################### ## Menubar callbacks ######################################### def resize(self, size=None): if size is not None: self._size.set(size) size = self._size.get() self._font.configure(size=-(abs(size))) self._boldfont.configure(size=-(abs(size))) self._sysfont.configure(size=-(abs(size))) #self._stacklabel['font'] = ('helvetica', -size-4, 'bold') #self._rtextlabel['font'] = ('helvetica', -size-4, 'bold') #self._lastoper_label['font'] = ('helvetica', -size) #self._lastoper1['font'] = ('helvetica', -size) #self._lastoper2['font'] = ('helvetica', -size) #self._prodlist['font'] = ('helvetica', -size) #self._prodlist_label['font'] = ('helvetica', -size-2, 'bold') self._redraw() def help(self, *e): # The default font's not very legible; try using 'fixed' instead. try: ShowText(self._top, 'Help: Shift-Reduce Parser Application', (__doc__ or '').strip(), width=75, font='fixed') except: ShowText(self._top, 'Help: Shift-Reduce Parser Application', (__doc__ or '').strip(), width=75) def about(self, *e): ABOUT = ("NLTK Shift-Reduce Parser Application\n" + "Written by Edward Loper") TITLE = 'About: Shift-Reduce Parser Application' try: from tkinter.messagebox import Message Message(message=ABOUT, title=TITLE).show() except: ShowText(self._top, TITLE, ABOUT) def edit_grammar(self, *e): CFGEditor(self._top, self._parser.grammar(), self.set_grammar) def set_grammar(self, grammar): self._parser.set_grammar(grammar) self._productions = list(grammar.productions()) self._prodlist.delete(0, 'end') for production in self._productions: self._prodlist.insert('end', (' %s' % production)) def edit_sentence(self, *e): sentence = " ".join(self._sent) title = 'Edit Text' instr = 'Enter a new sentence to parse.' EntryDialog(self._top, sentence, instr, self.set_sentence, title) def set_sentence(self, sent): self._sent = sent.split() #[XX] use tagged? self.reset() ######################################### ## Reduce Production Selection ######################################### def _toggle_grammar(self, *e): if self._show_grammar.get(): self._prodframe.pack(fill='both', side='left', padx=2, after=self._feedbackframe) self._lastoper1['text'] = 'Show Grammar' else: self._prodframe.pack_forget() self._lastoper1['text'] = 'Hide Grammar' self._lastoper2['text'] = '' def _prodlist_select(self, event): selection = self._prodlist.curselection() if len(selection) != 1: return index = int(selection[0]) production = self._parser.reduce(self._productions[index]) if production: self._lastoper1['text'] = 'Reduce:' self._lastoper2['text'] = '%s' % production if self._animate.get(): self._animate_reduce() else: self._redraw() else: # Reset the production selections. self._prodlist.selection_clear(0, 'end') for prod in self._parser.reducible_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) def _popup_reduce(self, widget): # Remove old commands. productions = self._parser.reducible_productions() if len(productions) == 0: return self._reduce_menu.delete(0, 'end') for production in productions: self._reduce_menu.add_command(label=str(production), command=self.reduce) self._reduce_menu.post(self._canvas.winfo_pointerx(), self._canvas.winfo_pointery()) ######################################### ## Animations ######################################### def _animate_shift(self): # What widget are we shifting? widget = self._rtextwidgets[0] # Where are we shifting from & to? right = widget.bbox()[0] if len(self._stackwidgets) == 0: left = 5 else: left = self._stackwidgets[-1].bbox()[2] + 10 # Start animating. dt = self._animate.get() dx = (left - right) * 1.0 / dt self._animate_shift_frame(dt, widget, dx) def _animate_shift_frame(self, frame, widget, dx): if frame > 0: self._animating_lock = 1 widget.move(dx, 0) self._top.after(10, self._animate_shift_frame, frame - 1, widget, dx) else: # but: stacktop?? # Shift the widget to the stack. del self._rtextwidgets[0] self._stackwidgets.append(widget) self._animating_lock = 0 # Display the available productions. self._draw_stack_top(widget) self._highlight_productions() def _animate_reduce(self): # What widgets are we shifting? numwidgets = len(self._parser.stack()[-1]) # number of children widgets = self._stackwidgets[-numwidgets:] # How far are we moving? if isinstance(widgets[0], TreeSegmentWidget): ydist = 15 + widgets[0].label().height() else: ydist = 15 + widgets[0].height() # Start animating. dt = self._animate.get() dy = ydist * 2.0 / dt self._animate_reduce_frame(dt / 2, widgets, dy) def _animate_reduce_frame(self, frame, widgets, dy): if frame > 0: self._animating_lock = 1 for widget in widgets: widget.move(0, dy) self._top.after(10, self._animate_reduce_frame, frame - 1, widgets, dy) else: del self._stackwidgets[-len(widgets):] for widget in widgets: self._cframe.remove_widget(widget) tok = self._parser.stack()[-1] if not isinstance(tok, Tree): raise ValueError() label = TextWidget(self._canvas, str(tok.label()), color='#006060', font=self._boldfont) widget = TreeSegmentWidget(self._canvas, label, widgets, width=2) (x1, y1, x2, y2) = self._stacklabel.bbox() y = y2 - y1 + 10 if not self._stackwidgets: x = 5 else: x = self._stackwidgets[-1].bbox()[2] + 10 self._cframe.add_widget(widget, x, y) self._stackwidgets.append(widget) # Display the available productions. self._draw_stack_top(widget) self._highlight_productions() # # Delete the old widgets.. # del self._stackwidgets[-len(widgets):] # for widget in widgets: # self._cframe.destroy_widget(widget) # # # Make a new one. # tok = self._parser.stack()[-1] # if isinstance(tok, Tree): # attribs = {'tree_color': '#4080a0', 'tree_width': 2, # 'node_font': bold, 'node_color': '#006060', # 'leaf_color': '#006060', 'leaf_font':self._font} # widget = tree_to_treesegment(self._canvas, tok.type(), # **attribs) # widget.node()['color'] = '#000000' # else: # widget = TextWidget(self._canvas, tok.type(), # color='#000000', font=self._font) # widget.bind_click(self._popup_reduce) # (x1, y1, x2, y2) = self._stacklabel.bbox() # y = y2-y1+10 # if not self._stackwidgets: x = 5 # else: x = self._stackwidgets[-1].bbox()[2] + 10 # self._cframe.add_widget(widget, x, y) # self._stackwidgets.append(widget) #self._redraw() self._animating_lock = 0 ######################################### ## Hovering. ######################################### def _highlight_hover(self, event): # What production are we hovering over? index = self._prodlist.nearest(event.y) if self._hover == index: return # Clear any previous hover highlighting. self._clear_hover() # If the production corresponds to an available reduction, # highlight the stack. selection = [int(s) for s in self._prodlist.curselection()] if index in selection: rhslen = len(self._productions[index].rhs()) for stackwidget in self._stackwidgets[-rhslen:]: if isinstance(stackwidget, TreeSegmentWidget): stackwidget.label()['color'] = '#00a000' else: stackwidget['color'] = '#00a000' # Remember what production we're hovering over. self._hover = index def _clear_hover(self, *event): # Clear any previous hover highlighting. if self._hover == -1: return self._hover = -1 for stackwidget in self._stackwidgets: if isinstance(stackwidget, TreeSegmentWidget): stackwidget.label()['color'] = 'black' else: stackwidget['color'] = 'black'
class Window2(Frame): def __init__(self, parent, context): super(Window2, self).__init__(parent.Frame) context.add_observer(self) self.parent = parent self.context = context self.controller = parent.controller self._initialize() self.build() def _initialize(self): self.id = 'task' #画面遷移の順番を示す指示物 self.unit_name_list = [] def update(self, msg): self._gid = msg['gid'] self.unit_name_list = _global_group[msg['gid'] - 1] self._update_unit_name_list() self.task_lbx.delete(0, END) def convert(self): units = self._task_box.get(0, END) if not units: self.context.notify('root', 'info', "처리할 작업이 없습니다.") return working_dir = self.context.get('directory') if not working_dir: self.context.directory() working_dir = self.context.get('directory') if not working_dir: messagebox.showwarning("경고", '작업디렉토리가 설정되어 있지 않습니다.') return # html변환여부 체크옵션 _html = self.context.get('html') _gid = self._gid try: self.controller.convert(working_dir, _html, units, _gid) self.context.notify('root', 'info', "작업완료") messagebox.showinfo("알림", message="작업이 완료되었습니다.") except Exception as e: print(e) messagebox.showerror('포맷오류', '잘못된 포맷을 적용하였습니다.') self.context.notify('root', 'info', "작업오류") def build(self): componets = self.component_list() tasks = self.task_list() componets.pack(side="left", fill='y') componets.config(bg='steel blue3') tasks.pack(side='right', fill='both', expand=True) tasks.config(bg='light grey', padx=3) def _update_unit_name_list(self): self._list.delete(0, END) for i, name in enumerate(self.unit_name_list): self._list.insert(i, name) def component_list(self): #sandy brown fr1 = Frame(self) scrollbar = Scrollbar(fr1) scrollbar.pack(side="right", fill="y") self._list = Listbox(fr1, bg="dim grey", fg="white", width=20, yscrollcommand=scrollbar.set) mb1 = Menubutton(fr1, text='선택메뉴', relief='flat', bg='steel blue3') mb1.menu = Menu(mb1, tearoff=0) mb1['menu'] = mb1.menu mb1.menu.add_command(label='등록', command=self.choose_all) mb1.pack(side='bottom') mb1.menu.add_command(label='선택', command=lambda: self._list.select_set(0, END)) mb1.menu.add_command( label='해제', command=lambda: self._list.selection_clear(0, 'end')) self._list.pack(anchor="center", fill="both", expand=True, padx=3, pady=3) scrollbar.config(command=self._list.yview) self._list.config(highlightcolor='green', font=('나눔고딕', 10), activestyle='none', selectmode='extended') self._list.bind('<<ListboxSelect>>', self.select_item) self._list.bind('<Button-3>', self.sub_menu1) self._list.exportselection = 0 return fr1 def select_item(self, event): self.clear_submenu() widget = event.widget #print("select item",widget.curselection()) # if isinstance(widget,Listbox): v = widget.curselection() t = [widget.get(i) for i in v] self.context.notify('root', 'info', t) def clear_submenu(self): if hasattr(self, 'sub_fr'): self.sub_fr.destroy() def _setActivate(self, obj, index): obj.selection_set(index) # obj.see(index) # obj.activate(index) # obj.selection_anchor(index) def sub_menu1(self, event): self.clear_submenu() x, y = event.x, event.y self._setActivate(self._list, self._list.nearest(y)) self.sub_fr = Frame(self, height=10) b1 = Button(self.sub_fr, text='reg', command=self.choose_task, relief='flat') b1.pack(side='top', fill='both') self.sub_fr.place(x=x + 15, y=y) def choose_all(self): self.clear_submenu() for el in self._list.get(0, END): if not self._isduplicate(el): self._task_box.insert(END, el) def choose_task(self): self.clear_submenu() ixs = self._list.curselection() for ix in ixs: el = self._list.get(ix) if not self._isduplicate(el): self._task_box.insert(END, el) def _isduplicate(self, txt): for v in list(self._task_box.get(0, 'end')): if v == txt: return True return False def task_list(self): fr = Frame(self) fr_top = Frame(fr) task_lbx = Listbox(fr_top) task_lbx.pack(anchor="center", fill="both", expand=True, padx=3, pady=3) self._task_box = task_lbx fr_bot = Frame(fr, height='2') b1 = Menubutton(fr_bot, text='실행메뉴') b1.menu = Menu(b1, tearoff=0) b1['menu'] = b1.menu b1.menu.add_command(label='실행', command=self.convert) b1.menu.add_command(label='비우기', command=self.delete_task_item) b1.pack() fr_top.pack(side='top', fill='both', expand=True) fr_bot.pack(side='bottom', fill='x') task_lbx.config(highlightcolor='green', font=('굴림체', 10), activestyle='none', selectmode='extended') self.task_lbx = task_lbx return fr def delete_task_item(self): v = self.task_lbx.curselection() #print(v) if not v: self.task_lbx.delete(0, END) elif len(v) == 1: self.task_lbx.delete(v) else: self.task_lbx.delete(v[0], v[-1])
class ListboxVidget(Vidget, Eventor): """ ListboxVidget contains a Listbox widget. It adds the following abilities: - Store items of any type, unlike Listbox widget that only stores texts. - Remember selected item even if the listbox widget lost focus. - Notify pre-change and post-change events. """ # Error raised when trying to change the listbox while a change is going on class CircularCallError(ValueError): pass # Error raised when trying to change the listbox while it is disabled class DisabledError(ValueError): pass # Event notified when the listbox's items are to be changed ITEMS_CHANGE_SOON = 'ITEMS_CHANGE_SOON' # Event notified when the listbox's items are changed ITEMS_CHANGE_DONE = 'ITEMS_CHANGE_DONE' # Event notified when the listbox's active item is to be changed ITEMCUR_CHANGE_SOON = 'ITEMCUR_CHANGE_SOON' # Event notified when the listbox's active item is changed ITEMCUR_CHANGE_DONE = 'ITEMCUR_CHANGE_DONE' # Events list EVENTS = ( ITEMS_CHANGE_SOON, ITEMS_CHANGE_DONE, ITEMCUR_CHANGE_SOON, ITEMCUR_CHANGE_DONE, ) def __init__( self, items=None, item_to_text=None, normal_bg='', normal_fg='', active_bg='sky blue', active_fg='white', selected_bg='steel blue', selected_fg='white', master=None, ): """ Initialize object. @param items: Items list. @param item_to_text: Item-to-text function. Default is `str`. @param normal_bg: Unselected item background color. @param normal_fg: Unselected item foreground color. @param active_bg: Active item background color. `Active` means the item is selected (in general meaning) but the listbox has no focus. @param active_fg: Active item foreground color. `Active` means the item is selected (in general meaning) but the listbox has no focus. @param selected_bg: Selected item background color. `Selected` means the item is selected (in general meaning) and the listbox has focus. @param selected_fg: Selected item foreground color. `Selected` means the item is selected (in general meaning) and the listbox has focus. @param master: Master widget. @return: None. """ # Initialize Vidget. # Create main frame widget. Vidget.__init__( self, master=master, ) # Initialize Eventor Eventor.__init__(self) # If items list is given if items is not None: # If items list is not list if not isinstance(items, list): # Raise error raise TypeError(items) # If items list is list. # If items list is not given, or items list is given and is list # Items list self._items = items if items is not None else [] # Item-to-text function. Default is `str`. self._item_to_text = item_to_text if item_to_text is not None else str # Unselected item background color self._normal_fg = normal_fg # Unselected item foreground color self._normal_bg = normal_bg # Active item background color self._active_fg = active_fg # Active item foreground color self._active_bg = active_bg # Selected item background color self._selected_fg = selected_fg # Selected item foreground color self._selected_bg = selected_bg # Whether the listbox is changing self._is_changing = False # Active index. `-1` means void, i.e. no item is active. self._indexcur = -1 # Whether active index is being reset to same value self._is_resetting = False # Create listbox widget self._listbox = Listbox( master=self.widget(), relief='groove', activestyle='none', highlightthickness=0, # Active index cache only supports single-selection mode for now. # See 2N6OR. selectmode='single', ) # Set the listbox widget as config target self.config_target_set(self._listbox) # Create x-axis scrollbar self._scrollbar_xview = _HiddenScrollbar( self.widget(), orient=HORIZONTAL, ) # Create y-axis scrollbar self._scrollbar_yview = _HiddenScrollbar( self.widget(), orient=VERTICAL, ) # Mount scrollbars self._listbox.config(xscrollcommand=self._scrollbar_xview.set) self._listbox.config(yscrollcommand=self._scrollbar_yview.set) self._scrollbar_xview.config(command=self._listbox.xview) self._scrollbar_yview.config(command=self._listbox.yview) # Bind single-click event handler self._listbox.bind('<Button-1>', self._on_single_click) # Bind double-click event handler self._listbox.bind('<Double-Button-1>', self._on_double_click) # Update listbox widget self._listbox_widget_update(keep_active=False) # Update widget self._widget_update() def _widget_update(self): """ Update widget. @return: None. """ # Row 0 for listbox and y-axis scrollbar self.widget().rowconfigure(0, weight=1) # Row 1 for x-axis scrollbar self.widget().rowconfigure(1, weight=0) # Column 0 for listbox and x-axis scrollbar self.widget().columnconfigure(0, weight=1) # Column 1 for y-axis scrollbar self.widget().columnconfigure(1, weight=0) # Lay out listbox self._listbox.grid(row=0, column=0, sticky='NSEW') # Lay out x-axis scrollbar self._scrollbar_xview.grid(row=1, column=0, sticky='EW') # Lay out y-axis scrollbar self._scrollbar_yview.grid(row=0, column=1, sticky='NS') def is_enabled(self): """ Test whether the listbox is enabled. @return: Boolean. """ # Return whether the listbox is enabled return self._listbox.config('state')[4] != DISABLED def is_changing(self): """ Test whether the listbox is changing. @return: Boolean. """ # Return whether the listbox is changing return self._is_changing def is_resetting(self): """ Test whether the listbox is setting active index to the same value. @return: Boolean. """ # Return whether the listbox is setting active index to the same value return self._is_resetting def size(self): """ Get number of items. @return: Number of items. """ # Return number of items return len(self._items) def items(self): """ Get items list. Notice do not change the list outside. @return: Items list. """ # Return items list return self._items def items_set( self, items, notify=True, keep_active=False, ): """ Set items list. Notice do not change the list outside. @param items: Items list. @param notify: Whether notify pre-change and post-change events. @param keep_active: Whether keep or clear active index. @return: None. """ # If the items is not list if not isinstance(items, list): # Raise error raise TypeError(items) # If the items is list. # If the listbox is disabled if not self.is_enabled(): # Raise error raise ListboxVidget.DisabledError() # If the listbox is not disabled. # If the listbox is changing if self._is_changing: # Raise error raise ListboxVidget.CircularCallError() # If the listbox is not changing. # Set changing flag on self._is_changing = True # If notify events if notify: # Notify pre-change event self.handler_notify(self.ITEMS_CHANGE_SOON) # Store the new items self._items = items # Update listbox widget self._listbox_widget_update( keep_active=keep_active ) # If notify events if notify: # Notify post-change event self.handler_notify(self.ITEMS_CHANGE_DONE) # Set changing flag off self._is_changing = False def index_is_valid(self, index): """ Test whether given index is valid. Notice -1 is not valid. @param index: Index to test. @return: Boolean. """ # Test whether given index is valid return 0 <= index and index < self.size() def index_is_valid_or_void(self, index): """ Test whether given index is valid or is -1. @param index: Index to test. @return: Boolean. """ # Test whether given index is valid or is -1 return index == -1 or self.index_is_valid(index) def index_first(self): """ Get the first item's index. @return: First item's index, or -1 if the listbox is empty. """ # Return the first item's index return 0 if self.size() > 0 else -1 def index_last(self): """ Get the last item's index. @return: Last item's index, or -1 if the listbox is empty. """ # Return the last item's index return self.size() - 1 def indexcur(self, internal=False, raise_error=False): """ Get the active index. @param internal: See 2N6OR. @return: The active index. If no active active, either return -1, or raise IndexError if `raise_error` is True. """ # Get active indexes indexcurs = self._indexcurs(internal=internal) # If have active indexes if indexcurs: # Return the first active index return indexcurs[0] # If no active indexes else: # If raise error if raise_error: # Raise error raise IndexError(-1) # If not raise error else: # Return -1 return -1 def _indexcurs(self, internal=False): """ Get active indexes list. 2N6OR @param internal: Whether use listbox widget's selected indexes, instead of cached active index. Notice listbox widget has no selected indexes if it has no focus. Notice using cached active index only supports single-selection mode, which means the result list has at most one index. @return: Active indexes list. """ # If use listbox widget's selected indexes if internal: # Return listbox widget's selected indexes list return [int(x) for x in self._listbox.curselection()] # If not use listbox widget's selected indexes else: # If cached active index is valid if self.index_is_valid(self._indexcur): # Return a list with the cached active index return [self._indexcur] # If cached active index is not valid else: # Return empty list return [] def indexcur_set( self, index, focus=False, notify=True, notify_arg=None, ): """ Set active index. @param index: The index to set. @param focus: Whether set focus on the listbox widget. @param notify: Whether notify pre-change and post-change events. @param notify_arg: Event argument. @return: None. """ # If the index is not valid or -1 if not self.index_is_valid_or_void(index): # Raise error raise IndexError(index) # If the index is valid or is -1. # If the listbox is not enabled if not self.is_enabled(): # Raise error raise ListboxVidget.DisabledError() # If the listbox is enabled. # If the listbox is changing if self._is_changing: # Raise error raise ListboxVidget.CircularCallError() # If the listbox is not changing. # Set changing flag on self._is_changing = True # Get old active index old_indexcur = self._indexcur # Set resetting flag on if new and old indexes are equal self._is_resetting = (index == old_indexcur) # If notify events if notify: # Notify pre-change event self.handler_notify(self.ITEMCUR_CHANGE_SOON, notify_arg) # If old active index is valid if self.index_is_valid(old_indexcur): # Set old active item's background color to normal color self._listbox.itemconfig(old_indexcur, background=self._normal_bg) # Set old active item's foreground color to normal color self._listbox.itemconfig(old_indexcur, foreground=self._normal_fg) # Cache new active index self._indexcur = index # Clear listbox widget's selection self._listbox.selection_clear(0, END) # Set listbox widget's selection self._listbox.selection_set(index) # Set listbox widget's activated index self._listbox.activate(index) # If new active index is valid if index != -1: # Set new active item's background color to active color self._listbox.itemconfig(index, background=self._active_bg) # Set new active item's foreground color to active color self._listbox.itemconfig(index, foreground=self._active_fg) # If set focus if focus: # Set focus on the listbox widget self._listbox.focus_set() # If new active index is valid if index != -1: # Make the active item visible self._listbox.see(index) # If notify events if notify: # Notify post-change event self.handler_notify(self.ITEMCUR_CHANGE_DONE, notify_arg) # Set resetting flag off self._is_resetting = False # Set changing flag off self._is_changing = False def indexcur_set_by_event( self, event, focus=False, notify=True, notify_arg=None, ): """ Set active index using a Tkinter event object that contains coordinates of the active item. @param event: Tkinter event object. @param focus: Whether set focus on the listbox widget. @param notify: Whether notify pre-change and post-change events. @param notify_arg: Event argument. @return: None. """ # Get the event's y co-ordinate's nearest listbox item index index = self._listbox.nearest(event.y) # If the index is not valid if not self.index_is_valid_or_void(index): # Ignore the event return # If the index is valid else: # Set the index as active index self.indexcur_set( index=index, focus=focus, notify=notify, notify_arg=notify_arg, ) def item(self, index): """ Get item at given index. @return: Item at given index, or IndexError if the index is not valid. """ return self.items()[index] def itemcur(self, internal=False, raise_error=False): """ Get the active item. @param internal: See 2N6OR. @param raise_error: Whether raise error if no active item. @return: The active item. If no active item, if `raise_error` is True, raise IndexError, otherwise return None. """ # Get active index. # May raise IndexError if `raise_error` is True. indexcur = self.indexcur( internal=internal, raise_error=raise_error, ) # If no active index if indexcur == -1: # Return None return None # If have active index else: # Return the active item return self.items()[indexcur] def item_insert( self, item, index=None, notify=True, keep_active=True, ): """ Insert item at given index. @param item: Item to insert. @param index: Index to insert. `None` means active index, and if no active index, insert at the end. @param notify: Whether notify pre-change and post-change events. @param keep_active: Whether keep or clear active index. @return: None. """ # If notify events if notify: # Notify pre-change events self.handler_notify(self.ITEMCUR_CHANGE_SOON) self.handler_notify(self.ITEMS_CHANGE_SOON) # Get old active index active_index = self.indexcur() # If the index is None, # it means use active index. if index is None: # Use active index. # `-1` works and means appending. index = active_index # Insert the item to the items list self._items.insert(index, item) # If old active index is valid if active_index != -1: # If old active index is GE the inserted index if active_index >= index: # Shift active index by one active_index += 1 # If old active index is not GE the inserted index, use it as-is. # Set new active index self.indexcur_set(index=active_index, notify=False) # Update listbox widget self._listbox_widget_update( keep_active=keep_active ) # If notify events if notify: # Notify post-change events self.handler_notify(self.ITEMS_CHANGE_DONE) self.handler_notify(self.ITEMCUR_CHANGE_DONE) def item_remove( self, index, notify=True, keep_active=True, ): """ Remove item at given index. @param index: Index to remove. @param notify: Whether notify pre-change and post-change events. @param keep_active: Whether keep or clear active index. @return: None. """ # If the index is not valid if not self.index_is_valid(index): # Raise error raise ValueError(index) # If the index is valid. # If notify events if notify: # Notify pre-change events self.handler_notify(self.ITEMCUR_CHANGE_SOON) self.handler_notify(self.ITEMS_CHANGE_SOON) # Get old active index active_index = self.indexcur() # Remove item at the index del self._items[index] # If old active index is valid if active_index != -1: # Get the last index index_last = self.index_last() # If old active index is GT the last index if active_index > index_last: # Use the last index as new active index active_index = index_last # If old active index is not GT the last index, use it as-is. # Set new active index self.indexcur_set(index=active_index, notify=False) # Update listbox widget self._listbox_widget_update( keep_active=keep_active ) # If notify events if notify: # Notify post-change events self.handler_notify(self.ITEMS_CHANGE_DONE) self.handler_notify(self.ITEMCUR_CHANGE_DONE) def handler_add( self, event, handler, need_arg=False, ): """ Add event handler for an event. If the event is ListboxVidget event, add the event handler to Eventor. If the event is not ListboxVidget event, add the event handler to listbox widget. Notice this method overrides `Eventor.handler_add` in order to add non-ListboxVidget event handler to listbox widget. @param event: Event name. @param handler: Event handler. @param need_arg: Whether the event handler needs event argument. @return: None. """ # If the event is ListboxVidget event if event in self.EVENTS: # Add the event handler to Eventor return Eventor.handler_add( self, event=event, handler=handler, need_arg=need_arg, ) # If the event is not ListboxVidget event, # it is assumed to be Tkinter widget event. else: # Add the event handler to listbox widget return self.bind( event=event, handler=handler, ) def bind( self, event, handler, ): """ Add event handler to listbox widget. ListboxVidget internally uses `<Button-1>` and `<Double-Button-1>` to capture active index changes. So if the given event is `<Button-1>` or `<Double-Button-1>`, the given handler will be wrapped. @param event: Event name. @param handler: Event handler. @return: None. """ # If the event is not `<Button-1>` or `<Double-Button-1>` if event not in ['<Button-1>', '<Double-Button-1>']: # Add the event handler to listbox widget self._listbox.bind(event, handler) # If the event is `<Button-1>` or `<Double-Button-1>` else: # Create event handler wrapper def handler_wrapper(e): """ Event handler wrapper that sets new active index and then calls the wrapped event handler. Setting new active index is needed because when this handler is called by Tkinter, the active index of the listbox is still old. @param e: Tkinter event object. @return: None. """ # Set new active index self.indexcur_set_by_event(e, notify=True) # Call the wrapped event handler handler(e) # Add the event handler wrapper to the listbox widget self._listbox.bind(event, handler_wrapper) def _on_single_click(self, event): """ `<Button-1>` event handler that updates active index. @param event: Tkinter event object. @return: None. """ # Updates active index self.indexcur_set_by_event(event, notify=True) def _on_double_click(self, event): """ `<Double-Button-1>` event handler that updates active index. @param event: Tkinter event object. @return: None. """ # Updates active index self.indexcur_set_by_event(event, notify=True) def _listbox_widget_update( self, keep_active, ): """ Update listbox widget's items and selection. @param keep_active: Whether keep or clear active index. @return: None. """ # Remove old items from listbox widget self._listbox.delete(0, END) # Insert new items into listbox widget. # For each ListboxVidget items. for index, item in enumerate(self.items()): # Get item text item_text = self._item_to_text(item) # Insert the item text into listbox widget self._listbox.insert(index, item_text) # Set the item's normal background color self._listbox.itemconfig(index, background=self._normal_bg) # Set the item's normal foreground color self._listbox.itemconfig(index, foreground=self._normal_fg) # Set the item's selected background color self._listbox.itemconfig(index, selectbackground=self._selected_bg) # Set the item's selected foreground color self._listbox.itemconfig(index, selectforeground=self._selected_fg) # If keep active index if keep_active: # Use old active index indexcur = self._indexcur # If not keep active index else: # Set active index to -1 indexcur = self._indexcur = -1 # Clear old selection self._listbox.selection_clear(0, END) # Set new selection. # `-1` works. self._listbox.selection_set(indexcur) # Set new active index. # `-1` works. self._listbox.activate(indexcur) # If new active index is valid if indexcur != -1: # Set active background color self._listbox.itemconfig(indexcur, background=self._active_bg) # Set active foreground color self._listbox.itemconfig(indexcur, foreground=self._active_fg) # Make the active item visible self._listbox.see(indexcur)
class Application(Frame): """Container class, encapsulates app""" # this class inherits from Tkinter.parent def __init__(self, path, master=None): """Constructor""" # call parent class constructor Frame.__init__(self, master) self.path = path self.image_filenames = find_image_files(path) # necessary to make the application actually appear on the screen self.grid(sticky=N + S + E + W) # PIL image, for loading from file and for resizing self.image_pil = None # Tk photoimage, for display self.image_tk = None # list of coords (2-element lists) of current selection's pts self.points_orig = [] # points_canvas is 'points_orig', in canvas coordinates self.points_canvas = [] # x- and y-coords of displayed image in canvas coordinate frame self.x_offset = -1 self.y_offset = -1 # font in listbox text self.font = Font(family='Helvetica', size=10, weight='normal') # crosshair line size , as fraction of # min(displayed-imagewidth, displayed-image-height) self.crosshair_fraction = 0.05 # color for drawing first crosshair - the origin self.crosshair1_color = 'red' # color for drawing second crosshair - (together with the first # crosshair, these two points define a coordinate frame) - self.crosshair2_color = 'green' # color for drawing third and on crosshairs - all points other # than the first and second, have this color self.crosshair3_color = 'cyan' # the width, in pixels, of crosshairs self.crosshair_thickness = 2 # length of crosshair (updated upon display) self.crosshair_radius = -1 # the scale of currently displayed image (updated upon display) self.image_scaling = 1.0 # create all widges and set their initial conditions self.create_widgets() def create_widgets(self): """Set up all application graphics""" # get the top level winddow top = self.winfo_toplevel() # set the title of the top level window top.title('Image Point Tagging Tool') # make row 0 of the top level window's grid stretchable top.rowconfigure(0, weight=1) # make column 0 of the top level window's grid stretchable top.columnconfigure(0, weight=1) # bind keys for entire app top.bind_all('<Up>', self.select_prev) top.bind_all('<Down>', self.select_next) # make row 0 of Application's widget's grid stretchable self.rowconfigure(0, weight=1) # make column 0 of Application's widget's grid stretchable self.columnconfigure(0, weight=1) self.canvas = Canvas(self, bg='gray') self.canvas.grid(row=0, column=0, rowspan=2, sticky=N + S + E + W) self.canvas.rowconfigure(0, weight=1) self.canvas.columnconfigure(0, weight=1) # bind resize events (need -4 here bec. event gives 4+(real_size)) self.canvas.bind( '<Configure>', lambda e, s=self: s.on_resize_canvas(e.width - 2, e.height - 2)) # bind canvas mouse clicks self.canvas.bind('<Button-1>', self.on_click_button1) self.canvas.bind('<Button-3>', self.on_click_button3) # create scrollbars self.scrollbar_x = Scrollbar(self, orient=HORIZONTAL, width=10) self.scrollbar_y = Scrollbar(self, orient=VERTICAL, width=10) self.scrollbar_x.grid(row=1, column=1, columnspan=2, sticky=E + W) self.scrollbar_y.grid(row=0, column=3, sticky=N + S) # create lb for showing labeled/not-labeled images self.listbox_marks = Listbox(self, width=1, takefocus=0, exportselection=0, font=self.font) self.listbox_marks.grid(row=0, column=1, sticky=N + S + E + W) # create lb for showing image filenames self.lisbox_filenames = Listbox(self, width=30, selectmode=SINGLE, xscrollcommand=self.scrollbar_x.set, yscrollcommand=self.scrollbar_y.set, exportselection=0, font=self.font) self.lisbox_filenames.grid(row=0, column=2, sticky=N + S + E + W) # bind scrollbar movement self.scrollbar_x['command'] = self.lisbox_filenames.xview self.scrollbar_y['command'] = self.on_scrollbar_y # bind left mouse click selection self.lisbox_filenames.bind( '<Button-1>', lambda e, s=self: s.select(self.lisbox_filenames.nearest(e.y))) self.listbox_marks.bind( '<Button-1>', lambda e, s=self: s.select(self.lisbox_filenames.nearest(e.y))) # bind wheel scroll self.lisbox_filenames.bind( '<Button-4>', lambda e, s=self: on_mousewheel(self.listbox_marks, 4)) self.lisbox_filenames.bind( '<Button-5>', lambda e, s=self: on_mousewheel(self.listbox_marks, 5)) self.listbox_marks.bind( '<Button-4>', lambda e, s=self: on_mousewheel(self.lisbox_filenames, 4)) self.listbox_marks.bind( '<Button-5>', lambda e, s=self: on_mousewheel(self.lisbox_filenames, 5)) # skip is # of chars to skip in path string so that only the # part of the path that was not supplied is displayed skip = len(self.path) if self.path[skip - 1] != '/': skip += 1 # insert image filenames plus marks into lists and # select first image that does not have pts file i = 0 index_of_image_with_no_pts_file = -1 for image_filename in self.image_filenames: self.lisbox_filenames.insert(END, image_filename[skip:]) if self.has_pts_file(i): self.listbox_marks.insert(END, '+') else: self.listbox_marks.insert(END, '') if index_of_image_with_no_pts_file < 0: index_of_image_with_no_pts_file = i i += 1 if index_of_image_with_no_pts_file < 0: self.select(0) else: self.select(index_of_image_with_no_pts_file) def on_scrollbar_y(self, *args): """Vertical scrollbar motion callback""" apply(self.lisbox_filenames.yview, args) apply(self.listbox_marks.yview, args) def on_click_button1(self, event): """Button 1 click callback: adds a crosshair at click location""" if self.coord_in_img(event.x, event.y): point = [(event.x - self.x_offset) / self.image_scaling, (event.y - self.y_offset) / self.image_scaling] point_scaled = [float(event.x), float(event.y)] self.points_orig.append(point) self.points_canvas.append(point_scaled) if len(self.points_orig) == 1: self.mark_labeled() self.on_resize_canvas(int(self.canvas['width']), int(self.canvas['height'])) self.save_points() def on_click_button3(self, event): """Button 3 click callback: deletes landmark near click location""" if not self.coord_in_img(event.x, event.y): return i = self.find_point_near_crosshair(event.x, event.y) if i >= 0: del self.points_orig[i] del self.points_canvas[i] if len(self.points_orig) == 0: self.mark_unlabeled() self.on_resize_canvas(int(self.canvas['width']), int(self.canvas['height'])) self.save_points() def select(self, i): """Select the i'th image to work with - make current selection = i""" # uncomment the following line if you are only dealing with # faces that have three points labeled on them and you want to # automatically reorder a previously tagged database so that # the person's right eye is the first point, left eye is # second point and mouth is third point self.sort_points() self.lisbox_filenames.selection_clear(0, END) self.listbox_marks.selection_clear(0, END) self.lisbox_filenames.selection_set(i) self.listbox_marks.selection_set(i) self.lisbox_filenames.see(i) self.listbox_marks.see(i) self.image_pil = PIL.Image.open(self.get_image_filename()) self.points_orig = self.read_pts_file() self.on_resize_canvas(int(self.canvas['width']), int(self.canvas['height'])) def select_prev(self, *args): #pylint: disable=unused-argument """Select entry that comes before current selection""" i = self.get_selected_index() if i > 0: self.select(i - 1) def select_next(self, *args): #pylint: disable=unused-argument """Select entry that comes after current selection""" i = self.get_selected_index() if i < len(self.image_filenames) - 1: self.select(i + 1) def on_resize_canvas(self, width, height): """Called when canvas is resized""" if width <= 0 or height <= 0: return # maximize image width or height depending on aspect ratios image_width = self.image_pil.size[0] image_height = self.image_pil.size[1] image_aspect_ratio = float(image_width) / float(image_height) self.canvas['width'] = width self.canvas['height'] = height canvas_width = int(self.canvas['width']) canvas_height = int(self.canvas['height']) canvas_aspect_ratio = float(canvas_width) / float(canvas_height) if image_aspect_ratio < canvas_aspect_ratio: new_image_width = int(image_aspect_ratio * float(canvas_height)) new_image_height = canvas_height else: new_image_width = canvas_width new_image_height = int(float(canvas_width) / image_aspect_ratio) self.image_tk = PhotoImage( self.image_pil.resize((new_image_width, new_image_height), PIL.Image.BILINEAR)) self.x_offset = 0.5 * (float(canvas_width) - float(new_image_width)) self.y_offset = 0.5 * (float(canvas_height) - float(new_image_height)) self.crosshair_radius = 0.5 * self.crosshair_fraction * float( min(new_image_width, new_image_height)) self.canvas.delete('image') self.canvas.create_image(self.x_offset, self.y_offset, anchor=NW, image=self.image_tk, tags='image') width_scale = float(new_image_width) / float(image_width) height_scale = float(new_image_height) / float(image_height) self.image_scaling = 0.5 * (width_scale + height_scale) self.points_canvas = [[ x[0] * self.image_scaling + self.x_offset, x[1] * self.image_scaling + self.y_offset ] for x in self.points_orig] self.redraw_points() def redraw_points(self): """redraw points in current entry's .pts file""" self.canvas.delete('line') # draw first crosshair in color1 if len(self.points_canvas) > 0: point1 = self.points_canvas[0] self.draw_crosshair(point1[0], point1[1], self.crosshair1_color) # draw second crosshair in color2 if len(self.points_canvas) > 1: point2 = self.points_canvas[1] self.draw_crosshair(point2[0], point2[1], self.crosshair2_color) # draw third or higher crosshair in color3 if len(self.points_canvas) > 2: for point in self.points_canvas[2:]: self.draw_crosshair(point[0], point[1], self.crosshair3_color) def draw_crosshair(self, x_coord, y_coord, fill_color): """Draw a cross at (x_coord, y_coord) in the currently selected image""" start_x = x_coord - self.crosshair_radius start_y = y_coord - self.crosshair_radius end_x = x_coord + self.crosshair_radius end_y = y_coord + self.crosshair_radius min_x = self.x_offset min_y = self.y_offset max_x = self.x_offset + self.image_tk.width() - 1 max_y = self.y_offset + self.image_tk.height() - 1 if start_x < min_x: start_x = min_x if start_y < min_y: start_y = min_y if end_x > max_x: end_x = max_x if end_y > max_y: end_y = max_y self.canvas.create_line(x_coord, start_y, x_coord, end_y, width=self.crosshair_thickness, tags='line', fill=fill_color) self.canvas.create_line(start_x, y_coord, end_x, y_coord, width=self.crosshair_thickness, tags='line', fill=fill_color) def get_selected_index(self): """Returns index of current selection""" return int(self.lisbox_filenames.curselection()[0]) def coord_in_img(self, x_coord, y_coord): """Returns whether (x_coord, y_coord) is inside the shown image""" return (x_coord >= self.x_offset and y_coord >= self.y_offset and x_coord < self.x_offset + self.image_tk.width() and y_coord < self.y_offset + self.image_tk.height()) def find_point_near_crosshair(self, x_coord, y_coord): """Returns index of landmark point near (x_coord, y_coord), or -1""" i = 0 i_min = -1 min_dist = self.image_tk.width() + self.image_tk.height() for pair in self.points_canvas: x_dist = x_coord - pair[0] y_dist = y_coord - pair[1] dist = sqrt(x_dist * x_dist + y_dist * y_dist) if dist <= self.crosshair_radius and dist < min_dist: i_min = i i += 1 return i_min def save_points(self): """Save current points to pts file""" # remove whatever was there before if self.has_pts_file(): os.remove(self.get_pts_filename()) # save current result if len(self.points_orig) > 0: filehandle = open(self.get_pts_filename(), 'w') for pair in self.points_orig: message = str(pair[0]) + ', ' + str(pair[1]) + '\n' filehandle.write(message) filehandle.close() def sort_points(self): """ Reorder points, assuming face labeling, so that the first point is always the person's right eye, the second point is the person's left eye and the third point is the mouth. NB: this function only (destructively) works on self.points_orig """ if len(self.points_orig) != 3: return # step 1 sort the points according to y-value self.points_orig.sort(key=lambda pt: pt[1]) # step 2: from the top-most two points, call the leftmost one # the person's right eye and call the other the person's left eye if self.points_orig[0][0] > self.points_orig[1][0]: # swap first and second points' x-coordinate tmp = self.points_orig[0][0] self.points_orig[0][0] = self.points_orig[1][0] self.points_orig[1][0] = tmp # swap first and second points' y-coordinate tmp = self.points_orig[0][1] self.points_orig[0][1] = self.points_orig[1][1] self.points_orig[1][1] = tmp # order changed, so re-save self.save_points() def has_pts_file(self, i=None): """Returns whether (i'th) selection has a pts file with landmarks""" if i is None: i = self.get_selected_index() return os.path.exists(self.get_pts_filename(i)) def get_pts_filename(self, i=None): """Returns filename of selected (or i'th) .pts file""" if i is None: i = self.get_selected_index() image_filename = self.image_filenames[i] return os.path.splitext(image_filename)[0] + '.pts' def get_image_filename(self, i=None): """Returns filename of (i'th) selection's image""" if i is None: i = self.get_selected_index() return self.image_filenames[i] def read_pts_file(self, i=None): """Returns list of points (lists) in (i'th) selection's .pts file""" if i is None: i = self.get_selected_index() if self.has_pts_file(i): filehandle = open(self.get_pts_filename(i), 'r') lines = filehandle.readlines() filehandle.close() return [[float(pair[0]), float(pair[1])] for pair in [line.split(',') for line in lines]] else: return [] def mark_labeled(self, i=None): """Mark (i'th) selection as having a .pts file""" if i is None: i = self.get_selected_index() self.listbox_marks.insert(i, '+') self.listbox_marks.delete(i + 1) self.listbox_marks.selection_set(i) def mark_unlabeled(self, i=None): """Unmark (i'th) selection as having a .pts file""" if i is None: i = self.get_selected_index() self.listbox_marks.insert(i, '') self.listbox_marks.delete(i + 1) self.listbox_marks.selection_set(i)
class FontChooser(Toplevel): """Font chooser dialog.""" def __init__(self, master, font_dict={}, text="Abcd", title="Font Chooser", **kwargs): """ Create a new FontChooser instance. Arguments: master : Tk or Toplevel instance master window font_dict : dict dictionnary, like the one returned by the ``actual`` method of a ``Font`` object: :: {'family': str, 'size': int, 'weight': 'bold'/'normal', 'slant': 'italic'/'roman', 'underline': bool, 'overstrike': bool} text : str text to be displayed in the preview label title : str window title kwargs : dict additional keyword arguments to be passed to ``Toplevel.__init__`` """ Toplevel.__init__(self, master, **kwargs) self.title(title) self.resizable(False, False) self.protocol("WM_DELETE_WINDOW", self.quit) self._validate_family = self.register(self.validate_font_family) self._validate_size = self.register(self.validate_font_size) # --- variable storing the chosen font self.res = "" style = Style(self) style.configure("prev.TLabel", background="white") bg = style.lookup("TLabel", "background") self.configure(bg=bg) # --- family list self.fonts = list(set(families())) self.fonts.append("TkDefaultFont") self.fonts.sort() for i in range(len(self.fonts)): self.fonts[i] = self.fonts[i].replace(" ", "\ ") max_length = int(2.5 * max([len(font) for font in self.fonts])) // 3 self.sizes = ["%i" % i for i in (list(range(6, 17)) + list(range(18, 32, 2)))] # --- font default font_dict["weight"] = font_dict.get("weight", "normal") font_dict["slant"] = font_dict.get("slant", "roman") font_dict["underline"] = font_dict.get("underline", False) font_dict["overstrike"] = font_dict.get("overstrike", False) font_dict["family"] = font_dict.get("family", self.fonts[0].replace('\ ', ' ')) font_dict["size"] = font_dict.get("size", 10) # --- creation of the widgets # ------ style parameters (bold, italic ...) options_frame = Frame(self, relief='groove', borderwidth=2) self.font_family = StringVar(self, " ".join(self.fonts)) self.font_size = StringVar(self, " ".join(self.sizes)) self.var_bold = BooleanVar(self, font_dict["weight"] == "bold") b_bold = Checkbutton(options_frame, text=TR["Bold"], command=self.toggle_bold, variable=self.var_bold) b_bold.grid(row=0, sticky="w", padx=4, pady=(4, 2)) self.var_italic = BooleanVar(self, font_dict["slant"] == "italic") b_italic = Checkbutton(options_frame, text=TR["Italic"], command=self.toggle_italic, variable=self.var_italic) b_italic.grid(row=1, sticky="w", padx=4, pady=2) self.var_underline = BooleanVar(self, font_dict["underline"]) b_underline = Checkbutton(options_frame, text=TR["Underline"], command=self.toggle_underline, variable=self.var_underline) b_underline.grid(row=2, sticky="w", padx=4, pady=2) self.var_overstrike = BooleanVar(self, font_dict["overstrike"]) b_overstrike = Checkbutton(options_frame, text=TR["Overstrike"], variable=self.var_overstrike, command=self.toggle_overstrike) b_overstrike.grid(row=3, sticky="w", padx=4, pady=(2, 4)) # ------ Size and family self.var_size = StringVar(self) self.entry_family = Entry(self, width=max_length, validate="key", validatecommand=(self._validate_family, "%d", "%S", "%i", "%s", "%V")) self.entry_size = Entry(self, width=4, validate="key", textvariable=self.var_size, validatecommand=(self._validate_size, "%d", "%P", "%V")) self.list_family = Listbox(self, selectmode="browse", listvariable=self.font_family, highlightthickness=0, exportselection=False, width=max_length) self.list_size = Listbox(self, selectmode="browse", listvariable=self.font_size, highlightthickness=0, exportselection=False, width=4) scroll_family = Scrollbar(self, orient='vertical', command=self.list_family.yview) scroll_size = Scrollbar(self, orient='vertical', command=self.list_size.yview) self.preview_font = Font(self, **font_dict) if len(text) > 30: text = text[:30] self.preview = Label(self, relief="groove", style="prev.TLabel", text=text, font=self.preview_font, anchor="center") # --- widget configuration self.list_family.configure(yscrollcommand=scroll_family.set) self.list_size.configure(yscrollcommand=scroll_size.set) self.entry_family.insert(0, font_dict["family"]) self.entry_family.selection_clear() self.entry_family.icursor("end") self.entry_size.insert(0, font_dict["size"]) try: i = self.fonts.index(self.entry_family.get().replace(" ", "\ ")) except ValueError: # unknown font i = 0 self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) self.list_family.see(i) try: i = self.sizes.index(self.entry_size.get()) self.list_size.selection_clear(0, "end") self.list_size.selection_set(i) self.list_size.see(i) except ValueError: # size not in list pass self.entry_family.grid(row=0, column=0, sticky="ew", pady=(10, 1), padx=(10, 0)) self.entry_size.grid(row=0, column=2, sticky="ew", pady=(10, 1), padx=(10, 0)) self.list_family.grid(row=1, column=0, sticky="nsew", pady=(1, 10), padx=(10, 0)) self.list_size.grid(row=1, column=2, sticky="nsew", pady=(1, 10), padx=(10, 0)) scroll_family.grid(row=1, column=1, sticky='ns', pady=(1, 10)) scroll_size.grid(row=1, column=3, sticky='ns', pady=(1, 10)) options_frame.grid(row=0, column=4, rowspan=2, padx=10, pady=10, ipadx=10) self.preview.grid(row=2, column=0, columnspan=5, sticky="eswn", padx=10, pady=(0, 10), ipadx=4, ipady=4) button_frame = Frame(self) button_frame.grid(row=3, column=0, columnspan=5, pady=(0, 10), padx=10) Button(button_frame, text="Ok", command=self.ok).grid(row=0, column=0, padx=4, sticky='ew') Button(button_frame, text=TR["Cancel"], command=self.quit).grid(row=0, column=1, padx=4, sticky='ew') self.list_family.bind('<<ListboxSelect>>', self.update_entry_family) self.list_size.bind('<<ListboxSelect>>', self.update_entry_size, add=True) self.list_family.bind("<KeyPress>", self.keypress) self.entry_family.bind("<Return>", self.change_font_family) self.entry_family.bind("<Tab>", self.tab) self.entry_size.bind("<Return>", self.change_font_size) self.entry_family.bind("<Down>", self.down_family) self.entry_size.bind("<Down>", self.down_size) self.entry_family.bind("<Up>", self.up_family) self.entry_size.bind("<Up>", self.up_size) # bind Ctrl+A to select all instead of go to beginning self.bind_class("TEntry", "<Control-a>", self.select_all) self.wait_visibility(self) self.grab_set() self.entry_family.focus_set() self.lift() def select_all(self, event): """Select all entry content.""" event.widget.selection_range(0, "end") def keypress(self, event): """Select the first font whose name begin by the key pressed.""" key = event.char.lower() l = [i for i in self.fonts if i[0].lower() == key] if l: i = self.fonts.index(l[0]) self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) self.list_family.see(i) self.update_entry_family() def up_family(self, event): """Navigate in the family listbox with up key.""" try: i = self.list_family.curselection()[0] self.list_family.selection_clear(0, "end") if i <= 0: i = len(self.fonts) self.list_family.see(i - 1) self.list_family.select_set(i - 1) except TclError: self.list_family.selection_clear(0, "end") i = len(self.fonts) self.list_family.see(i - 1) self.list_family.select_set(i - 1) self.list_family.event_generate('<<ListboxSelect>>') def up_size(self, event): """Navigate in the size listbox with up key.""" try: s = self.var_size.get() if s in self.sizes: i = self.sizes.index(s) elif s: sizes = list(self.sizes) sizes.append(s) sizes.sort(key=lambda x: int(x)) i = sizes.index(s) else: i = 0 self.list_size.selection_clear(0, "end") if i <= 0: i = len(self.sizes) self.list_size.see(i - 1) self.list_size.select_set(i - 1) except TclError: i = len(self.sizes) self.list_size.see(i - 1) self.list_size.select_set(i - 1) self.list_size.event_generate('<<ListboxSelect>>') def down_family(self, event): """Navigate in the family listbox with down key.""" try: i = self.list_family.curselection()[0] self.list_family.selection_clear(0, "end") if i >= len(self.fonts): i = -1 self.list_family.see(i + 1) self.list_family.select_set(i + 1) except TclError: self.list_family.selection_clear(0, "end") self.list_family.see(0) self.list_family.select_set(0) self.list_family.event_generate('<<ListboxSelect>>') def down_size(self, event): """Navigate in the size listbox with down key.""" try: s = self.var_size.get() if s in self.sizes: i = self.sizes.index(s) elif s: sizes = list(self.sizes) sizes.append(s) sizes.sort(key=lambda x: int(x)) i = sizes.index(s) - 1 else: s = len(self.sizes) - 1 self.list_size.selection_clear(0, "end") if i < len(self.sizes) - 1: self.list_size.selection_set(i + 1) self.list_size.see(i + 1) else: self.list_size.see(0) self.list_size.select_set(0) except TclError: self.list_size.selection_set(0) self.list_size.event_generate('<<ListboxSelect>>') def toggle_bold(self): """Update font preview weight.""" b = self.var_bold.get() self.preview_font.configure(weight=["normal", "bold"][b]) def toggle_italic(self): """Update font preview slant.""" b = self.var_italic.get() self.preview_font.configure(slant=["roman", "italic"][b]) def toggle_underline(self): """Update font preview underline.""" b = self.var_underline.get() self.preview_font.configure(underline=b) def toggle_overstrike(self): """Update font preview overstrike.""" b = self.var_overstrike.get() self.preview_font.configure(overstrike=b) def change_font_family(self, event=None): """Update font preview family.""" family = self.entry_family.get() if family.replace(" ", "\ ") in self.fonts: self.preview_font.configure(family=family) def change_font_size(self, event=None): """Update font preview size.""" size = int(self.var_size.get()) self.preview_font.configure(size=size) def validate_font_size(self, d, ch, V): """Validation of the size entry content.""" l = [i for i in self.sizes if i[:len(ch)] == ch] i = None if l: i = self.sizes.index(l[0]) elif ch.isdigit(): sizes = list(self.sizes) sizes.append(ch) sizes.sort(key=lambda x: int(x)) i = min(sizes.index(ch), len(self.sizes)) if i is not None: self.list_size.selection_clear(0, "end") self.list_size.selection_set(i) deb = self.list_size.nearest(0) fin = self.list_size.nearest(self.list_size.winfo_height()) if V != "forced": if i < deb or i > fin: self.list_size.see(i) return True if d == '1': return ch.isdigit() else: return True def tab(self, event): """Move at the end of selected text on tab press.""" self.entry_family = event.widget self.entry_family.selection_clear() self.entry_family.icursor("end") return "break" def validate_font_family(self, action, modif, pos, prev_txt, V): """Completion of the text in the entry with existing font names.""" if self.entry_family.selection_present(): sel = self.entry_family.selection_get() txt = prev_txt.replace(sel, '') else: txt = prev_txt if action == "0": txt = txt[:int(pos)] + txt[int(pos) + 1:] return True else: txt = txt[:int(pos)] + modif + txt[int(pos):] ch = txt.replace(" ", "\ ") l = [i for i in self.fonts if i[:len(ch)] == ch] if l: i = self.fonts.index(l[0]) self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) deb = self.list_family.nearest(0) fin = self.list_family.nearest(self.list_family.winfo_height()) index = self.entry_family.index("insert") self.entry_family.delete(0, "end") self.entry_family.insert(0, l[0].replace("\ ", " ")) self.entry_family.selection_range(index + 1, "end") self.entry_family.icursor(index + 1) if V != "forced": if i < deb or i > fin: self.list_family.see(i) return True else: return False def update_entry_family(self, event=None): """Update family entry when an item is selected in the family listbox.""" # family = self.list_family.get("@%i,%i" % (event.x , event.y)) family = self.list_family.get(self.list_family.curselection()[0]) self.entry_family.delete(0, "end") self.entry_family.insert(0, family) self.entry_family.selection_clear() self.entry_family.icursor("end") self.change_font_family() def update_entry_size(self, event): """Update size entry when an item is selected in the size listbox.""" # size = self.list_size.get("@%i,%i" % (event.x , event.y)) size = self.list_size.get(self.list_size.curselection()[0]) self.var_size.set(size) self.change_font_size() def ok(self): """Validate choice.""" self.res = self.preview_font.actual() self.quit() def get_res(self): """Return chosen font.""" return self.res def quit(self): self.destroy()
class _select_recent_file(_Dialog): def body(self, master): dialogframe = Frame(master, width=248, height=293) self.dialogframe = dialogframe dialogframe.pack() self.make_Label_1( self.dialogframe) # Label: Select Recent File : at Main(1,1) self.make_Label_2(self.dialogframe) # Label: at Main(2,1) self.make_Listbox_1(self.dialogframe) # Listbox: at Main(3,1) # make a Status Bar self.statusMessage = StringVar() self.statusMessage.set("") self.statusbar = Label(self.dialogframe, textvariable=self.statusMessage, bd=1, relief=SUNKEN) self.statusbar.grid(row=99, column=0, columnspan=99, sticky='ew') # >>>>>>insert any user code below this comment for section "top_of_init" self.statusMessage.set("Select File from Recent List") # TkGridGUI generated code. DO NOT EDIT THE FOLLOWING. section "make_Label_1" def make_Label_1(self, frame): """ Label: Select Recent File : at Main(1,1)""" self.Label_1 = Label(frame, text="Select Recent File", width="30") self.Label_1.grid(row=1, column=1) # >>>>>>insert any user code below this comment for section "make_Label_1" # TkGridGUI generated code. DO NOT EDIT THE FOLLOWING. section "make_Label_2" def make_Label_2(self, frame): """ Label: at Main(2,1)""" self.Label_2 = Label(frame, text="", width="30") self.Label_2.grid(row=2, column=1) # >>>>>>insert any user code below this comment for section "make_Label_2" # TkGridGUI generated code. DO NOT EDIT THE FOLLOWING. section "make_Listbox_1" def make_Listbox_1(self, frame): """ Listbox: at Main(3,1)""" lbframe = Frame(frame) self.Listbox_1_frame = lbframe vbar = Scrollbar(lbframe, orient=VERTICAL) self.Listbox_1 = Listbox(lbframe, width="30", height="12", yscrollcommand=vbar.set) vbar.config(command=self.Listbox_1.yview) vbar.grid(row=0, column=1, sticky='ns') self.Listbox_1.grid(row=0, column=0) self.Listbox_1_frame.grid(row=3, column=1) # >>>>>>insert any user code below this comment for section "make_Listbox_1" self.input_fileL = [] # list of (head, tail) # Edit the Listbox Entries self.Listbox_1.configure( exportselection=False) # stay highlighted after focus leaves for i, name in enumerate(self.dialogOptions['fileL']): head, tail = os.path.split(name) self.input_fileL.append((head, tail)) self.Listbox_1.insert(END, tail) self.Listbox_1.bind("<ButtonRelease-1>", self.Listbox_1_Click) self.Listbox_1.bind('<Double-1>', self.double_click) # bind double left clicks self.Listbox_1.bind('<Enter>', self.snapHighlightToMouse) self.Listbox_1.bind('<Motion>', self.snapHighlightToMouse) self.Listbox_1.bind('<Leave>', self.unhighlight) def snapHighlightToMouse(self, event): #self.Listbox_1.selection_clear(0, END) #self.Listbox_1.selection_set( self.Listbox_1.nearest(event.y) ) #print( self.Listbox_1.get( self.Listbox_1.nearest(event.y) ), self.Listbox_1.nearest(event.y) ) self.statusMessage.set(self.input_fileL[self.Listbox_1.nearest( event.y)][0]) def unhighlight(self, event=None): #self.Listbox_1.selection_clear(0, END) pass # TkGridGUI generated code. DO NOT EDIT THE FOLLOWING. section "Listbox_1_Click" def Listbox_1_Click(self, event): #bind method for component ID=Listbox_1 """ Listbox: at Main(3,1)""" pass # >>>>>>insert any user code below this comment for section "Listbox_1_Click" # replace, delete, or comment-out the following #print( "executed method Listbox_1_Click" ) #print(event) #print( "current selection(s) =",self.Listbox_1.curselection() ) labelL = [] isel = -1 for i in self.Listbox_1.curselection(): isel = i labelL.append(self.Listbox_1.get(i)) #print( "current label(s) =",labelL ) if isel >= 0: self.Label_1.config(text=self.input_fileL[isel][0]) self.Label_2.config(text=self.input_fileL[isel][1]) def double_click(self, event): self.Listbox_1_Click(event) self.ok() # TkGridGUI generated code. DO NOT EDIT THE FOLLOWING. section "dialog_validate" def validate(self): self.result = {} # return a dictionary of results # >>>>>>insert any user code below this comment for section "dialog_validate" # set values in "self.result" dictionary for return # for example... # self.result["age"] = self.Entry_2_StringVar.get() self.result["file_name"] = '' for i in self.Listbox_1.curselection(): self.result["file_name"] = self.dialogOptions['fileL'][i] break return 1 """ Use this for testing dialog fileL = ['c:/myhome/file 1', 'd:/really/deep/path/to/file/Really_Important_File','e:/yesterdays/news/YesterdayAtNoon'] for i in range(30): fileL.append('f:/dummy%i/area%i/Dummy_File_%i'%(i,i,i) + 'x'*i) dialog = _select_recent_file(self.master, "Pick Recent File", dialogOptions={'fileL':fileL}) """ # TkGridGUI generated code. DO NOT EDIT THE FOLLOWING. section "end" def apply(self): pass
class FontChooser(Toplevel): """ Font chooser toplevel """ def __init__(self, master, font_dict={}, text="Abcd", title="Font Chooser", **kwargs): """ Create a new FontChooser instance. font: dictionnary, like the one returned by the .actual method of a Font object {'family': 'DejaVu Sans', 'overstrike':False, 'size': 12, 'slant': 'italic' or 'roman', 'underline': False, 'weight': 'bold' or 'normal'} text: text to be displayed in the preview label title: window title **kwargs: additional keyword arguments to be passed to Toplevel.__init__ """ Toplevel.__init__(self, master, **kwargs) self.title(title) self.resizable(False, False) self.protocol("WM_DELETE_WINDOW", self.quit) self._validate_family = self.register(self.validate_font_family) self._validate_size = self.register(self.validate_font_size) # variable storing the chosen font self.res = "" style = Style(self) style.configure("prev.TLabel", background="white") bg = style.lookup("TLabel", "background") self.configure(bg=bg) # family list self.fonts = list(set(families())) self.fonts.append("TkDefaultFont") self.fonts.sort() for i in range(len(self.fonts)): self.fonts[i] = self.fonts[i].replace(" ", "\ ") max_length = int(2.5 * max([len(font) for font in self.fonts])) // 3 self.sizes = [ "%i" % i for i in (list(range(6, 17)) + list(range(18, 32, 2))) ] # font default font_dict["weight"] = font_dict.get("weight", "normal") font_dict["slant"] = font_dict.get("slant", "roman") font_dict["family"] = font_dict.get("family", self.fonts[0].replace('\ ', ' ')) font_dict["size"] = font_dict.get("size", 10) # Widgets creation options_frame = Frame(self, relief='groove', borderwidth=2) self.font_family = StringVar(self, " ".join(self.fonts)) self.font_size = StringVar(self, " ".join(self.sizes)) self.var_bold = BooleanVar(self, font_dict["weight"] == "bold") b_bold = Checkbutton(options_frame, text=TR["Bold"], command=self.toggle_bold, variable=self.var_bold) b_bold.grid(row=0, sticky="w", padx=4, pady=(4, 2)) self.var_italic = BooleanVar(self, font_dict["slant"] == "italic") b_italic = Checkbutton(options_frame, text=TR["Italic"], command=self.toggle_italic, variable=self.var_italic) b_italic.grid(row=1, sticky="w", padx=4, pady=2) self.var_size = StringVar(self) self.entry_family = Entry(self, width=max_length, validate="key", validatecommand=(self._validate_family, "%d", "%S", "%i", "%s", "%V")) entry_size = Entry(self, width=4, validate="key", textvariable=self.var_size, validatecommand=(self._validate_size, "%d", "%P", "%V")) self.list_family = Listbox(self, selectmode="browse", listvariable=self.font_family, highlightthickness=0, exportselection=False, width=max_length) self.list_size = Listbox(self, selectmode="browse", listvariable=self.font_size, highlightthickness=0, exportselection=False, width=4) scroll_family = Scrollbar(self, orient='vertical', command=self.list_family.yview) scroll_size = Scrollbar(self, orient='vertical', command=self.list_size.yview) self.preview_font = Font(self, **font_dict) if len(text) > 30: text = text[:30] self.preview = Label(self, relief="groove", style="prev.TLabel", text=text, font=self.preview_font, anchor="center") # Widget configuration self.list_family.configure(yscrollcommand=scroll_family.set) self.list_size.configure(yscrollcommand=scroll_size.set) self.entry_family.insert(0, font_dict["family"]) self.entry_family.selection_clear() self.entry_family.icursor("end") entry_size.insert(0, font_dict["size"]) i = self.fonts.index(self.entry_family.get().replace(" ", "\ ")) self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) self.list_family.see(i) i = self.sizes.index(entry_size.get()) self.list_size.selection_clear(0, "end") self.list_size.selection_set(i) self.list_size.see(i) self.entry_family.grid(row=0, column=0, sticky="ew", pady=(10, 1), padx=(10, 0)) entry_size.grid(row=0, column=2, sticky="ew", pady=(10, 1), padx=(10, 0)) self.list_family.grid(row=1, column=0, sticky="nsew", pady=(1, 10), padx=(10, 0)) self.list_size.grid(row=1, column=2, sticky="nsew", pady=(1, 10), padx=(10, 0)) scroll_family.grid(row=1, column=1, sticky='ns', pady=(1, 10)) scroll_size.grid(row=1, column=3, sticky='ns', pady=(1, 10)) options_frame.grid(row=0, column=4, rowspan=2, padx=10, pady=10, ipadx=10) self.preview.grid(row=2, column=0, columnspan=5, sticky="eswn", padx=10, pady=(0, 10), ipadx=4, ipady=4) button_frame = Frame(self) button_frame.grid(row=3, column=0, columnspan=5, pady=(0, 10), padx=10) Button(button_frame, text="Ok", command=self.ok).grid(row=0, column=0, padx=4, sticky='ew') Button(button_frame, text=TR["Cancel"], command=self.quit).grid(row=0, column=1, padx=4, sticky='ew') self.list_family.bind('<<ListboxSelect>>', self.update_entry_family) self.list_size.bind('<<ListboxSelect>>', self.update_entry_size, add=True) self.list_family.bind("<KeyPress>", self.keypress) self.entry_family.bind("<Return>", self.change_font_family) self.entry_family.bind("<Tab>", self.tab) entry_size.bind("<Return>", self.change_font_size) self.entry_family.bind("<Down>", self.down_family) entry_size.bind("<Down>", self.down_size) self.entry_family.bind("<Up>", self.up_family) entry_size.bind("<Up>", self.up_size) # bind Ctrl+A to select all instead of go to beginning self.bind_class("TEntry", "<Control-a>", self.select_all) self.update_idletasks() self.grab_set() self.entry_family.focus_set() self.lift() def select_all(self, event): event.widget.selection_range(0, "end") def keypress(self, event): key = event.char.lower() l = [i for i in self.fonts if i[0].lower() == key] if l: i = self.fonts.index(l[0]) self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) self.list_family.see(i) self.update_entry_family() def up_family(self, event): try: txt = self.entry_family.get().replace(" ", "\ ") l = [i for i in self.fonts if i[:len(txt)] == txt] if l: self.list_family.selection_clear(0, "end") i = self.fonts.index(l[0]) if i > 0: self.list_family.selection_set(i - 1) self.list_family.see(i - 1) else: i = len(self.fonts) self.list_family.see(i - 1) self.list_family.select_set(i - 1) except TclError: i = len(self.fonts) self.list_family.see(i - 1) self.list_family.select_set(i - 1) self.list_family.event_generate('<<ListboxSelect>>') def up_size(self, event): try: s = self.var_size.get() i = self.sizes.index(s) self.list_size.selection_clear(0, "end") if i > 0: self.list_size.selection_set(i - 1) self.list_size.see(i - 1) else: i = len(self.sizes) self.list_size.see(i - 1) self.list_size.select_set(i - 1) except TclError: i = len(self.sizes) self.list_size.see(i - 1) self.list_size.select_set(i - 1) self.list_size.event_generate('<<ListboxSelect>>') def down_family(self, event): try: txt = self.entry_family.get().replace(" ", "\ ") l = [i for i in self.fonts if i[:len(txt)] == txt] if l: self.list_family.selection_clear(0, "end") i = self.fonts.index(l[0]) if i < len(self.fonts) - 1: self.list_family.selection_set(i + 1) self.list_family.see(i + 1) else: self.list_family.see(0) self.list_family.select_set(0) except TclError: self.list_family.selection_set(0) self.list_family.event_generate('<<ListboxSelect>>') def down_size(self, event): try: s = self.var_size.get() i = self.sizes.index(s) self.list_size.selection_clear(0, "end") if i < len(self.sizes) - 1: self.list_size.selection_set(i + 1) self.list_size.see(i + 1) else: self.list_size.see(0) self.list_size.select_set(0) except TclError: self.list_size.selection_set(0) self.list_size.event_generate('<<ListboxSelect>>') def toggle_bold(self): b = self.var_bold.get() self.preview_font.configure(weight=["normal", "bold"][b]) def toggle_italic(self): b = self.var_italic.get() self.preview_font.configure(slant=["roman", "italic"][b]) def toggle_underline(self): b = self.var_underline.get() self.preview_font.configure(underline=b) def toggle_overstrike(self): b = self.var_overstrike.get() self.preview_font.configure(overstrike=b) def change_font_family(self, event=None): family = self.entry_family.get() if family.replace(" ", "\ ") in self.fonts: self.preview_font.configure(family=family) def change_font_size(self, event=None): size = int(self.var_size.get()) self.preview_font.configure(size=size) def validate_font_size(self, d, ch, V): ''' Validation of the size entry content ''' l = [i for i in self.sizes if i[:len(ch)] == ch] if l: i = self.sizes.index(l[0]) self.list_size.selection_clear(0, "end") self.list_size.selection_set(i) deb = self.list_size.nearest(0) fin = self.list_size.nearest(self.list_size.winfo_height()) if V != "forced": if i < deb or i > fin: self.list_size.see(i) return True if d == '1': return ch.isdigit() else: return True def tab(self, event): self.entry_family = event.widget self.entry_family.selection_clear() self.entry_family.icursor("end") return "break" def validate_font_family(self, action, modif, pos, prev_txt, V): """ completion of the text in the path entry with existing folder/file names """ if self.entry_family.selection_present(): sel = self.entry_family.selection_get() txt = prev_txt.replace(sel, '') else: txt = prev_txt if action == "0": txt = txt[:int(pos)] + txt[int(pos) + 1:] return True else: txt = txt[:int(pos)] + modif + txt[int(pos):] ch = txt.replace(" ", "\ ") l = [i for i in self.fonts if i[:len(ch)] == ch] if l: i = self.fonts.index(l[0]) self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) deb = self.list_family.nearest(0) fin = self.list_family.nearest(self.list_family.winfo_height()) index = self.entry_family.index("insert") self.entry_family.delete(0, "end") self.entry_family.insert(0, l[0].replace("\ ", " ")) self.entry_family.selection_range(index + 1, "end") self.entry_family.icursor(index + 1) if V != "forced": if i < deb or i > fin: self.list_family.see(i) return True else: return False def update_entry_family(self, event=None): # family = self.list_family.get("@%i,%i" % (event.x , event.y)) family = self.list_family.get(self.list_family.curselection()[0]) self.entry_family.delete(0, "end") self.entry_family.insert(0, family) self.entry_family.selection_clear() self.entry_family.icursor("end") self.change_font_family() def update_entry_size(self, event): # size = self.list_size.get("@%i,%i" % (event.x , event.y)) size = self.list_size.get(self.list_size.curselection()[0]) self.var_size.set(size) self.change_font_size() def ok(self): self.res = self.preview_font.actual() self.quit() def get_res(self): return self.res def quit(self): self.destroy()
class MainWindow(Tk): def __init__(self): Tk.__init__(self) self.title('Jeu de la Vie') # Index on the object selected in the listbox (-1 if no object is selected) self._objectSelected = -1 self._fieldWidth = self.winfo_screenwidth() - 240 self._fieldHeight = self.winfo_screenheight() - 90 # Definition of widgets self._noteBook = ttk.Notebook(self) self._rightCommandsTab = Frame(self._noteBook) self._noteBook.add(self._rightCommandsTab, text="Commandes") self._objectsTab = Frame(self._noteBook) self._noteBook.add(self._objectsTab, text='Objets') self._objectsListbox = Listbox(self._objectsTab, selectmode=SINGLE, height=0, cursor='pencil') self._cancelObjectSelectionButton = Button(self._objectsTab, text='Annuler', command=self.unselectObject) self._field = Field(self, width=self._fieldWidth, height=self._fieldHeight, bg='grey', cursor='tcross') self._informationFrame = LabelFrame(self._rightCommandsTab, text="Infos", labelanchor='n') self._commandFrame = LabelFrame(self._rightCommandsTab, text="Commandes", labelanchor='n') self._nbCellsScaleFrame = LabelFrame(self._commandFrame, text="Taille des cellules") self._nbCellsScale = Scale(self._nbCellsScaleFrame, variable=self._field._cellSize, from_=cellSizeMin, to=cellSizeMax, command=self._field.updateField, orient='horizontal', length=100) self._speedScaleFrame = LabelFrame(self._commandFrame, text="Vitesse") self._speedScale = Scale(self._speedScaleFrame, variable=self._field._speed, from_=1, orient='horizontal', length=100) self._rectSelectButton = Button(self._commandFrame, text="Sélection\nRectangle", command=self.rectSelection, relief=RAISED) self._fieldShowCheckButton = Checkbutton( self._commandFrame, text="Grille", variable=self._field._isShowedField, command=self.showOrHideField) self._eraseButton = Button(self._commandFrame, text="Effacer", command=self.erase, relief=RAISED) self._generationCounterFrame = LabelFrame(self._informationFrame, text="Génération") self._generationCounter = Label(self._generationCounterFrame, textvariable=self._field._generation) self._cellsCounterFrame = LabelFrame(self._informationFrame, text="Population") self._cellsCounter = Label(self._cellsCounterFrame, textvariable=self._field._nbCells) self._bottomCommands = Frame(self) self._start = Button(self._bottomCommands, text='Démarrer', command=self.start) self._stop = Button(self._bottomCommands, text='Stop', command=self._field.stop) self._quit = Button(self._bottomCommands, text='Quitter', command=self.quit) self._noteBook.select(self._rightCommandsTab) # Packing # self._objectsTab self._noteBook.pack(padx=10, side=RIGHT) self._objectsListbox.pack(side=TOP) self._cancelObjectSelectionButton.pack(pady=10, side=TOP) self._informationFrame.pack(side=TOP, pady=30) self._generationCounterFrame.pack(side=TOP, pady=10) self._generationCounter.pack() self._cellsCounterFrame.pack(side=TOP, pady=10) self._cellsCounter.pack() self._commandFrame.pack(side=TOP, pady=30) self._nbCellsScaleFrame.pack(side=TOP, pady=10) self._nbCellsScale.pack() self._speedScaleFrame.pack(side=TOP, pady=10) self._speedScale.pack() self._rectSelectButton.pack(side=TOP, pady=10) self._fieldShowCheckButton.pack(side=TOP, pady=10) self._eraseButton.pack(side=TOP, pady=10) self._bottomCommands.pack(padx=10, pady=10, side=BOTTOM) self._start.pack(side=LEFT, padx=10) self._stop.pack(side=LEFT, padx=10) self._quit.pack(side=LEFT, padx=10) self._field.pack(padx=10, pady=10, side=TOP, anchor='center', expand=False) # Binding events self._field.bind("<Button-1>", self.leftClickOnField) self._field.bind("<Button-3>", self.rightClickOnField) self._field.bind("<B1-Motion>", self.leftClickOnField) self._field.bind("<B3-Motion>", self.rightClickOnField) self._objectsListbox.bind("<Button-1>", self.leftClickOnMadeObject) self._noteBook.bind("<Button-1>", self.unselectObject) # Getting objects from the madeObjects.py file self.importObjects() def start(self): # Start button if self._field._stopped: self._field._stopped = False self._field.updateCellsState() def quit(self): # Quit button self.destroy() def convertCoordinates(self, event): # Converts the output of the mouse click event (x,y) into a cell row and column i = (event.y + self._field._yMin) // self._field._cellSize.get() j = (event.x + self._field._xMin) // self._field._cellSize.get() # print("event.x : "+str(event.x), "event.y : "+str(event.y)) # print("(i,j) = " + str(i) + "," + str(j)) return i, j def leftClickOnField(self, event): # event.x and event.y return coordinates with origin in the corner upper left ((x,y)!=(i,j)) i, j = self.convertCoordinates(event) if self._field._rectSelectActivated.get(): if not self._field._rectSelectedOneCell.get(): # Just color with red the first cell self._field._rectSelectedOneCell.set(1) self._field.drawRedCell(i, j) self._field._redCell_i = i self._field._redCell_j = j else: # Awake each cell in the rectangle self._field._rectSelectedOneCell.set(0) self._field.deleteRedCell() i1, i2 = sorted((self._field._redCell_i, i)) j1, j2 = sorted((self._field._redCell_j, j)) for lig in range(i1, i2 + 1): for col in range(j1, j2 + 1): awaked = self._field._game_state.awake(lig, col) if awaked: self._field.drawAliveCell(lig, col) elif self._objectSelected >= 0: for (di, dj ) in mo.objectsList[self._objectSelected].getAliveCellsList(): awaked = self._field._game_state.awake(i + di, j + dj) if awaked: # The cell has been awaked self._field.drawAliveCell(i + di, j + dj) else: awaked = self._field._game_state.awake(i, j) if awaked: # The cell has been awaked self._field.drawAliveCell(i, j) def rightClickOnField(self, event): i, j = self.convertCoordinates(event) killed = self._field._game_state.kill(i, j) if killed: self._field.eraseKilledCell(i, j) def rectSelection(self): if self._field._rectSelectActivated.get( ): # Rectangle selection was activated self.cancelRectSelection() else: # Rectangle selection wasn't activated self._field._rectSelectActivated.set(1) self._rectSelectButton.config(relief=SUNKEN) def cancelRectSelection(self): self._field._rectSelectActivated.set(0) self._rectSelectButton.config(relief=RAISED) if self._field._rectSelectedOneCell.get(): self._field.deleteRedCell() self._field._rectSelectedOneCell.set(0) def showOrHideField(self): # Linked to the checkbox, hides of shows the field if self._field._isShowedField.get(): self._field.showField() else: self._field.hideField() def erase(self): # Kills every cells cellsAlive = self._field._game_state._cellsAlive.copy() for i, j in cellsAlive: killed = self._field._game_state.kill(i, j) if killed: self._field.eraseKilledCell(i, j) self._field.stop() self._field._generation.set(0) def importObjects(self): for obj in mo.objectsList: self._objectsListbox.insert('end', obj.getName()) def leftClickOnMadeObject(self, event): self._objectSelected = self._objectsListbox.nearest(event.y) def unselectObject(self, *event): # Delete th selected object if it exists self._objectsListbox.selection_clear(0, 'end') self._objectSelected = -1 # If rectangle selection was activated if self._field._rectSelectActivated.get(): self.cancelRectSelection()