def build_past_box(self): self.bottom_input_frame = ttk.Frame(self.frame) self.bottom_input_frame.pack(side="bottom", fill="x") self.past_box = TextAware(self.bottom_input_frame, bd=3, height=3) self.past_box.pack(expand=True, fill='x') self.past_box.configure( foreground='white', background='black', wrap="word", ) self.past_box.tag_configure("prompt", foreground="gray") self.past_box.configure(state="disabled")
def start_multi_edit(self, num_textboxes=5): assert self.mode == "Multi Edit" self.clear_story_frame() self.multi_edit_frame = ScrollableFrame(self.story_frame) self.multi_edit_frame.pack(expand=True, fill="both") self.multi_textboxes = [ TextAware(self.multi_edit_frame.scrollable_frame, height=5) for i in range(num_textboxes) ] for tb in self.multi_textboxes: tb.pack(expand=True, fill="both") readable_font = Font( family="Georgia", size=12) # Other nice options: Helvetica, Arial, Georgia tb.configure( font=readable_font, spacing1=10, foreground=text_color(), # Darkmode background=bg_color(), padx=2, pady=2, # spacing2=3, # Spacing between lines4 # spacing3=3, wrap="word", )
def edit_node(self, node_id, box, text): # self.select_node_func(node_id=node_id) self.delete_textbox() self.editing_node_id = node_id #fontheight = tkinter.font.Font(font=(self.font, self.get_text_size())).metrics('linespace') self.textbox = TextAware(self.canvas, bg=edit_color(), fg=active_text_color(), padx=10, pady=10, height=10, font=(self.font, self.get_text_size())) self.textbox.insert(tkinter.END, text) textbox_height = box[3] - box[1] if min_edit_box_height < box[3] - box[ 1] else min_edit_box_height textbox_width = self.state.visualization_settings['text_width'] self.textbox_id = self.canvas.create_window( box[0] + (box[2] - box[0]) / 2, box[1] + (box[3] - box[1]) / 2, window=self.textbox, height=textbox_height, width=textbox_width)
def refresh(self): if self.new_button: self.new_button.destroy() for memory in self.memories: memory.destroy() for check in self.checks: check.destroy() for edit_button in self.edit_buttons: edit_button.destroy() self.memories = [] self.checks = [] self.edit_buttons = [] for i, memory in enumerate(self.state.construct_memory(self.node)): if memory['text']: temp_check = tk.BooleanVar() temp_check.set(True) row = self.master.grid_size()[1] self.memories.append(TextAware(self.master, height=1)) self.memories[i].grid(row=row, column=0, columnspan=2, padx=5) self.memories[i].insert(tk.INSERT, memory['text']) self.memories[i].configure( state='disabled', foreground=text_color(), background=bg_color(), wrap="word", ) # FIXME checks are unchecked by default self.checks.append( tk.Checkbutton(self.master, variable=temp_check)) self.checks[i].grid(row=row, column=2, padx=3) self.edit_buttons.append( create_button( self.master, "Edit", lambda _memory=memory: self.edit_memory(_memory), width=4)) self.edit_buttons[i].grid(row=row, column=3) self.new_button = create_button(self.master, "Add memory", self.create_new, width=11)
def refresh(self): if self.new_button: self.new_button.destroy() for memory in self.memories: memory.destroy() for edit_button in self.edit_buttons: edit_button.destroy() self.memories = [] self.checks = [] self.edit_buttons = [] if 'memories' in self.node: for i, memory_id in enumerate(self.node['memories']): memory = self.state.memories[memory_id] if memory['text']: row = self.master.grid_size()[1] self.memories.append(TextAware(self.master, height=1)) self.memories[i].grid(row=row, column=0, columnspan=2, padx=5) self.memories[i].insert(tk.INSERT, memory['text']) self.memories[i].configure( state='disabled', foreground=text_color(), background=bg_color(), wrap="word", ) self.edit_buttons.append( create_button( self.master, "Edit", lambda _memory=memory: self.edit_memory(_memory), width=4)) self.edit_buttons[i].grid(row=row, column=3) self.new_button = create_button(self.master, "Add memory", self.create_new, width=11)
class TreeVis: def __init__(self, parent_frame, state, controller): self.parent_frame = parent_frame #self.select_node_func = select_node_func #self.save_edits_func = save_edits_func self.state = state self.controller = controller self.frame = None self.canvas = None self.textbox = None self.textbox_id = None self.editing_node_id = None self.node_coords = {} self.levels = {} self.nodes = {} self.lines = {} self.showtext = True self.root = None self.selected_node = None self.overflow_display = 'PAGE' #'FULL' or 'SCROLL' or 'PAGE' self.icons = None #self.resize_icon_events = [] #icon_size = 16 #self.old_icons = [] self.text_hidden = False self.buttons_hidden = False self.textbox_events = {} self.active = [] #TODO instead of root width, long textboxes should have scrollbars #if not possible, multiple pages (!) self.root_width = self.state.visualization_settings['text_width'] self.font = "Georgia" self.init_icons() self.build_canvas() self.scroll_ratio = 1 self.bind_mouse_controls() def init_icons(self): self.icons = Icons() def build_canvas(self): self.frame = ttk.Frame(self.parent_frame) background_color = vis_bg_color() self.canvas = tkinter.Canvas(self.frame, bg=background_color) self.canvas.bind('<Double-Button-1>', lambda event: self.delete_textbox()) hbar = tkinter.Scrollbar(self.frame, orient=tkinter.HORIZONTAL) hbar.pack(side=tkinter.BOTTOM, fill=tkinter.X) hbar.config(command=self.canvas.xview) vbar = tkinter.Scrollbar(self.frame, orient=tkinter.VERTICAL) vbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) vbar.config(command=self.canvas.yview) self.canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set) self.canvas.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH) def bind_mouse_controls(self): # FIXME # def _on_mousewheel(event): # self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") # self.frame.bind_all("<MouseWheel>", _on_mousewheel) # self.canvas.bind_all("<MouseWheel>", _on_mousewheel) # This is what enables scrolling with the mouse: def scroll_start(event): self.canvas.scan_mark(event.x, event.y) def scroll_move(event): self.canvas.scan_dragto(event.x, event.y, gain=1) self.canvas.bind("<ButtonPress-1>", scroll_start) self.canvas.bind("<B1-Motion>", scroll_move) # windows zoom def zoomer(event): if event.delta > 0: zoom_in(event) self.scroll_ratio *= 1.1 self.canvas.scale("all", event.x, event.y, 1.1, 1.1) elif event.delta < 0: zoom_out(event) self.scroll_ratio *= 0.9 self.canvas.scale("all", event.x, event.y, 0.9, 0.9) self.canvas.configure(scrollregion=self.canvas.bbox( "all")) #self.canvas_bbox_padding(self.canvas.bbox("all"))) self.fix_text_zoom() self.icon_visibility_due_to_zoom() # # linux zoom def zoom_in(event): self.scroll_ratio *= 1.1 self.canvas.scale("all", event.x, event.y, 1.1, 1.1) self.canvas.configure(scrollregion=self.canvas.bbox( "all")) #self.canvas_bbox_padding(self.canvas.bbox("all"))) self.fix_text_zoom() self.icon_visibility_due_to_zoom() def zoom_out(event): self.scroll_ratio *= 0.9 self.canvas.scale("all", event.x, event.y, 0.9, 0.9) self.canvas.configure(scrollregion=self.canvas.bbox( "all")) #self.canvas_bbox_padding(self.canvas.bbox("all"))) # self.showtext = event.text > 0.8 self.fix_text_zoom() self.icon_visibility_due_to_zoom() # Mac and then linux scrolls self.canvas.bind("<MouseWheel>", zoomer) self.canvas.bind("<Button-4>", zoom_in) self.canvas.bind("<Button-5>", zoom_out) # Hack to make zoom work on Windows # root.bind_all("<MouseWheel>", zoomer) def fix_text_zoom(self): size = self.get_text_size() if size == 0: if not self.text_hidden: self.text_hidden = True for item in self.canvas.find_withtag("text"): self.canvas.itemconfigure(item, state='hidden') else: if self.text_hidden: self.text_hidden = False for item in self.canvas.find_withtag("text"): self.canvas.itemconfigure(item, state='normal') for item in self.canvas.find_withtag("text"): self.canvas.itemconfig(item, font=(self.font, size), width=self.get_width(item)) def icon_visibility_due_to_zoom(self): approx_size = math.floor(self.scroll_ratio * 18) if approx_size < 12: if not self.buttons_hidden: self.buttons_hidden = True for item in self.canvas.find_withtag("image"): self.canvas.itemconfigure(item, state='hidden') else: if self.buttons_hidden: self.buttons_hidden = False for item in self.canvas.find_withtag("image"): self.canvas.itemconfigure(item, state='normal') # TODO save default widths (because some nodes have different widths) def get_width(self, item): #width = int(self.canvas.itemcget(item, "width")) width = self.state.visualization_settings['text_width'] return math.floor(width * self.scroll_ratio) def get_text_size(self): return math.floor(self.state.visualization_settings['textsize'] * self.scroll_ratio) ################################# # Drawing ################################# def redraw(self, root, selected_node): self.selected_node = selected_node self.canvas.delete('all') self.node_coords = {} self.nodes = {} self.lines = {} self.levels = {} filtered_tree = tree_subset(root, filter=self.controller.in_nav) ancestry = self.state.ancestry(selected_node) pruned_tree = limited_branching_tree(ancestry, filtered_tree, depth_limit=2) self.compute_tree_coordinates(pruned_tree, 400, 400, level=0) self.center_about_ancestry(ancestry, x_align=400) self.center_y(selected_node, 400) self.fix_orientation() self.draw_precomputed_tree(pruned_tree) self.color_selection(selected_node) self.center_view(*self.node_coords[selected_node["id"]]) def compute_tree_coordinates(self, root, x, y, level=0): self.node_coords[root["id"]] = (x, y) if level not in self.levels: self.levels[level] = [] self.levels[level].append(root["id"]) level_offset = self.state.visualization_settings['level_distance'] leaf_offset = self.state.visualization_settings['leaf_distance'] leaf_position = x next_child_position = x for child in root['children']: leaf_position = next_child_position subtree_offset = self.compute_tree_coordinates( child, next_child_position, y + level_offset, level + 1) leaf_position += subtree_offset next_child_position = leaf_position + leaf_offset return leaf_position - x def fix_orientation(self): if self.state.visualization_settings["horizontal"]: coords = {} # if the tree is horizontal, swap x and y coordinates for id, value in self.node_coords.items(): coords[id] = (value[1], value[0]) self.node_coords = coords def draw_precomputed_tree(self, root): root_x, root_y = self.node_coords[root["id"]] self.draw_node(root['id'], radius=15, x=root_x, y=root_y) for child in root['children']: child_x, child_y = self.node_coords[child["id"]] self.draw_connector(child['id'], root_x, root_y, child_x, child_y, fill='#000000', width=1, offset=30, connections='horizontal' if self.state.visualization_settings["horizontal"] else 'vertical') self.draw_precomputed_tree(child) def center_about_ancestry(self, ancestry, x_align, level=0): if level >= len(ancestry): return ancestor = ancestry[level] ancestor_x, _ = self.node_coords[ancestor['id']] offset = ancestor_x - x_align for node_id in self.levels[level]: self.node_coords[node_id] = (self.node_coords[node_id][0] - offset, self.node_coords[node_id][1]) if level + 1 < len(ancestry): self.center_about_ancestry(ancestry, x_align, level + 1) else: #shift all deeper levels by same offset remaining_levels = [ self.levels[i] for i in range(level + 1, len(self.levels)) ] for l in remaining_levels: for node_id in l: self.node_coords[node_id] = (self.node_coords[node_id][0] - offset, self.node_coords[node_id][1]) def center_y(self, selected_node, y_align): y = self.node_coords[selected_node["id"]][1] offset = y - y_align for node_id in self.node_coords: self.node_coords[node_id] = (self.node_coords[node_id][0], self.node_coords[node_id][1] - offset) def draw_circle(self, radius, x, y): return self.canvas.create_oval(x - radius, y - radius, x + radius, y + radius, fill="black") def draw_connector(self, child_id, x1, y1, x2, y2, fill, width=1, activefill=None, offset=0, smooth=True, connections='horizontal'): if connections == 'horizontal': self.lines[child_id] = self.canvas.create_line( x1, y1, x1 + offset, y1, x2 - offset, y2, x2, y2, smooth=smooth, fill=fill, activefill=activefill, width=width) else: self.lines[child_id] = self.canvas.create_line( x1, y1, x1, y1 + offset, x2, y2 - offset, x2, y2, smooth=smooth, fill=fill, activefill=activefill, width=width) self.canvas.tag_lower(self.lines[child_id]) def draw_node(self, node_id, radius, x, y): node = self.draw_circle(radius, x, y) self.nodes[node_id] = node self.canvas.tag_bind( node, "<Button-1>", lambda event, node_id=node_id: self.select_node(node_id)) def color_selection(self, selected_node): ancestry = self.state.ancestry(selected_node) # color all ancestry nodes blue for node in ancestry: self.canvas.itemconfig(self.nodes[node['id']], fill="blue") if node['id'] in self.lines: self.canvas.itemconfig(self.lines[node['id']], fill="blue", width=3) ################################# # Old ################################# # TODO Slow for big tree; do not redraw everything? def draw(self, root_node, selected_node, center_on_selection=False): # pprint(self.state.visualization_settings) if self.state.visualization_settings["chapter_mode"]: self.root = self.state.build_chapter_trees()[0][0] else: self.root = root_node self.canvas.delete('data') self.selected_node = selected_node self.delete_textbox() # TODO change this if not self.state.visualization_settings["chapter_mode"]: if not self.root.get('open', False): self.collapse_all() if not self.selected_node.get('open', False): #TODO also expand ancestors self.expand_node(self.selected_node) self.node_coords = {} self.resize_icon_events = [] self.active = self.get_active() self.compute_tree_coordinates(self.root, 100, 100, level=0) self.center_about_ancestry(self.state.ancestry(self.selected_node)) self.draw_precomputed_tree(self.root) #self.draw_tree(self.root, 100, 100) # self.canvas.scale("all", 0, 0, self.scroll_ratio, self.scroll_ratio) # region = self.canvas_bbox_padding(self.canvas.bbox("all")) # self.canvas.configure(scrollregion=region) # self.fix_text_zoom() # self.icon_visibility_due_to_zoom() # if center_on_selection: # self.center_view_on_node(self.selected_node) def refresh_selection(self, root_node, selected_node): if self.selected_node["id"] not in self.node_coords: self.draw(self.root, self.selected_node, center_on_selection=True) self.selected_node = selected_node if not self.selected_node.get("open", False): self.expand_node(self.selected_node) self.draw(self.root, self.selected_node, center_on_selection=True) return self.delete_textbox() old_active = self.active self.active = self.get_active() for node in old_active: if node not in self.active: self.canvas.itemconfig(f'lines-{node["id"]}', fill=inactive_line_color(), width=1) self.canvas.itemconfig(f'ghostlines-{node["id"]}', fill=inactive_line_color()) self.canvas.itemconfig(f'text-{node["id"]}', fill=inactive_text_color()) self.canvas.itemconfig(f'box-{node["id"]}', outline=inactive_line_color(), width=1) for node in self.active: self.canvas.itemconfig(f'text-{node["id"]}', fill=active_text_color()) if self.node_selected(node): self.canvas.itemconfig(f'box-{node["id"]}', outline=selected_line_color(), width=2) self.canvas.itemconfig(f'lines-{node["id"]}', fill=selected_line_color(), width=2) self.canvas.itemconfig(f'ghostlines-{node["id"]}', fill=selected_line_color()) else: self.canvas.itemconfig(f'box-{node["id"]}', outline=active_line_color(), width=1) self.canvas.itemconfig(f'lines-{node["id"]}', fill=active_line_color(), width=1) if not self.state.visualization_settings["chapter_mode"] \ and self.state.tree_node_dict[node["id"]].get("visited", False): self.canvas.itemconfig(f'box-{node["id"]}', fill=visited_node_bg_color()) self.center_view_on_node(self.selected_node) def draw_tree(self, node, nodex, nodey): self.node_coords[node["id"]] = (nodex, nodey) if self.state.visualization_settings["chapter_mode"]: bbox = self.draw_textbox(node, nodex, nodey) padding = 10 else: if not node.get("open", False): bbox = self.draw_expand_node_button(node, nodex, nodey) return collapsed_offset display_text = self.state.visualization_settings[ 'display_text'] and self.showtext if display_text: bbox = self.draw_textbox(node, nodex, nodey) padding = 10 else: bbox = (nodex, nodey, nodex, nodey) padding = 0 textheight = bbox[3] - bbox[1] text_width = bbox[2] - bbox[0] width_diff = self.state.visualization_settings['text_width'] - text_width \ if (self.state.visualization_settings['display_text'] and fixed_level_width and not self.state.visualization_settings["chapter_mode"]) else 0 offset = textheight # TODO vertical # Draw children with increasing offsets child_offset = 0 # TODO why? level_distance = chapter_leveldist if self.state.visualization_settings['chapter_mode'] \ else self.state.visualization_settings['level_distance'] leaf_distance = chapter_leaf_distance if self.state.visualization_settings['chapter_mode'] \ else self.state.visualization_settings['leaf_distance'] for child in node['children']: childx = nodex + level_distance + text_width + width_diff childy = nodey + child_offset parentx = nodex + text_width parenty = nodey # TODO if vertical child_offset += leaf_distance child_offset += self.draw_tree(child, childx, childy) # Draw line to child if self.node_selected(child): color = selected_line_color() width = 2 else: active = child in self.active color = active_line_color() if active else inactive_line_color( ) width = 2 if active else 1 goto_id = node["chapter"][ "root_id"] if self.state.visualization_settings[ 'chapter_mode'] else child["id"] self.draw_line(parentx - padding, parenty - padding, childx - padding, childy - padding, name=f'lines-{child["id"]}', fill=color, activefill=BLUE, width=width, offset=smooth_line_offset, smooth=True, method=lambda event, node_id=goto_id: self. controller.nav_select(node_id=node_id)) #TODO lightmode # if "ghostchildren" in node: # parentx = nodex + text_width # parenty = nodey # for ghost_id in node["ghostchildren"]: # ghost = self.state.tree_node_dict.get(ghost_id, None) # if ghost is None: # continue # if ghost.get("open", False) and ghost["id"] in self.node_coords: # ghostx, ghosty = self.node_coords[ghost["id"]] # if tree_structure_map[ghost["id"]] == selected_id: # color = active_line_color() # else: # color = inactive_line_color() # self.draw_line(parentx - offset, parenty - offset, ghostx - offset, ghosty - offset, # name=f'ghostlines-{ghost["id"]}', # fill=color, activefill=BLUE, offset=smooth_line_offset, smooth=True, # method=lambda event, node_id=ghost["id"]: self.controller.nav_select(node_id=node_id)) # else: # #print("drew collapsed ghostchild") # #TODO fix position # self.draw_line(parentx - offset, parenty - offset, # parentx + self.state.visualization_settings["level_distance"] - offset, # parenty - offset, # name=f'ghostlines-{ghost["id"]}', # fill=inactive_line_color(), activefill=BLUE, offset=smooth_line_offset, smooth=True, # method=lambda event, node_id=ghost["id"]: self.controller.nav_select(node_id=node_id)) # self.draw_expand_node_button(ghost, parentx + self.state.visualization_settings["level_distance"], parenty, ghost=True) # return return offset if child_offset == 0 else child_offset def draw_line(self, x1, y1, x2, y2, fill, name, width=1, activefill=None, offset=0, smooth=True, method=None): if smooth: line_id = self.canvas.create_line( x1, y1, x1 + offset, y1, x2 - offset, y2, x2, y2, smooth=smooth, fill=fill, activefill=activefill, width=width, tags=[f'{name}', 'data', 'lines']) else: line_id = self.canvas.create_line( x1, y1, x2, y2, fill=fill, activefill=activefill, width=width, tags=[f'{name}', 'data', 'lines']) if method is not None: self.canvas.tag_bind(f'{name}', "<Button-1>", method) self.canvas.tag_lower(line_id) def split_text(self, node): text = node['text'] font = tkinter.font.Font(font=self.font) text_width = font.measure(text) lineheight = font.metrics('linespace') max_lines = math.floor( (self.state.visualization_settings['leaf_distance'] - leaf_padding) / lineheight) lines_estimate = text_width / self.state.visualization_settings[ 'text_width'] try: new_text_len = int( math.floor(len(text) * max_lines / lines_estimate)) except ZeroDivisionError: return text text = node['text'][:new_text_len] return text def draw_textbox(self, node, nodex, nodey): active = node in self.active text_color = active_text_color() if active else inactive_text_color() width = self.root_width if node['id'] == self.root[ 'id'] else self.state.visualization_settings['text_width'] if self.state.visualization_settings["chapter_mode"]: text = node["chapter"]["title"] else: text = self.split_text( node) if self.overflow_display == 'PAGE' else node['text'] text_id = self.canvas.create_text( nodex, nodey, fill=text_color, activefill=BLUE, font=(self.font, self.get_text_size()), width=width, text=text, tags=[f'text-{node["id"]}', 'data', 'text'], anchor=tkinter.NW) padding = (-10, -10, 10, 10) bbox = self.canvas.bbox(text_id) box = tuple(map(lambda i, j: i + j, padding, bbox)) # TODO different for chapter mode fill = visited_node_bg_color() if node.get( "visited", False) else unvisited_node_bg_color() outline_color = selected_line_color() if self.node_selected(node) else \ (active_line_color() if active else inactive_line_color()) width = 2 if active else 1 rect_id = round_rectangle(x1=box[0], x2=box[2], y1=box[1], y2=box[3], canvas=self.canvas, outline=outline_color, width=width, activeoutline=BLUE, fill=fill, tags=[f'box-{node["id"]}', 'data']) self.canvas.tag_raise(text_id, rect_id) if self.state.visualization_settings["chapter_mode"]: self.canvas.tag_bind( f'box-{node["id"]}', "<Button-1>", lambda event, node_id=node["chapter"][ "root_id"]: self.select_node(node_id=node_id)) self.canvas.tag_bind( f'text-{node["id"]}', "<Button-1>", lambda event, node_id=node["chapter"][ "root_id"]: self.select_node(node_id=node_id)) else: self.canvas.tag_bind( f'text-{node["id"]}', "<Button-1>", lambda event, node_id=node["id"]: self.edit_node( node_id=node_id, box=box, text=node['text'])) self.textbox_events[ node["id"]] = lambda node_id=node["id"]: self.edit_node( node_id=node_id, box=box, text=node['text']) self.canvas.tag_bind(f'box-{node["id"]}', "<Button-1>", self.box_click(node["id"], box, node["text"])) # TODO collapsing and buttons for chapters... if not self.state.visualization_settings["chapter_mode"]: if node is not self.root: self.draw_collapse_button(node, box) if self.state.visualization_settings["showbuttons"]: self.draw_buttons(node, box) self.draw_bookmark_star(node, box) return box def canvas_bbox_padding(self, bbox): padding = (-canvas_padding, -canvas_padding, canvas_padding, canvas_padding) box = tuple(map(lambda i, j: i + j, padding, bbox)) return box def draw_expand_node_button(self, node, nodex, nodey, ghost=False): text_id = self.canvas.create_text( nodex - 4, nodey - 6, fill='white', activefill=BLUE, font=(self.font, self.get_text_size()), text='+', tags=[f'expand-{node["id"]}', 'data', 'text'], anchor=tkinter.NW) padding = (-5, -5, 5, 5) bbox = self.canvas.bbox(text_id) box = tuple(map(lambda i, j: i + j, padding, bbox)) outline_color = inactive_line_color() fill = visited_node_bg_color() if ghost else expand_button_color() rect_id = self.canvas.create_rectangle( box, outline=outline_color, activeoutline=BLUE, fill=fill, tags=[f'expand-box-{node["id"]}', 'data']) self.canvas.tag_raise(text_id, rect_id) self.canvas.tag_bind(f'expand-{node["id"]}', "<Button-1>", lambda event, _node=node: self.expand_node(_node)) self.canvas.tag_bind(f'expand-box-{node["id"]}', "<Button-1>", lambda event, _node=node: self.expand_node(_node)) return box def draw_buttons(self, node, box): # TODO dynamic button positions if node is not self.root: # if node has siblings if len(self.state.tree_node_dict[node["parent_id"]] ["children"]) > 1: if box[2] - box[0] > 200: self.draw_shiftup_button(node, box) self.draw_shiftdown_button(node, box) # TODO conditional on generated, etc if box[2] - box[0] > 200: self.draw_read_button(node, box) self.draw_memory_button(node, box) self.draw_info_button(node, box) self.draw_generate_button(node, box) self.draw_collapse_except_subtree_button(node, box) self.draw_changeparent_button(node, box) self.draw_addlink_button(node, box) self.draw_newchild_button(node, box) self.draw_newparent_button(node, box) self.draw_mergeparent_button(node, box) self.draw_delete_button(node, box) self.draw_edit_button(node, box) if len(node["children"]) > 0: if box[2] - box[0] > 200: self.draw_collapse_subtree_button(node, box) self.draw_expand_subtree_button(node, box) self.draw_expand_children_button(node, box) self.draw_collapse_children_button(node, box) self.draw_mergechildren_button(node, box) def draw_icon(self, node, x_pos, y_pos, icon_name, name=None, method=None): if name is None: name = icon_name icon_id = self.canvas.create_image( x_pos, y_pos, image=self.icons.get_icon(icon_name), tags=[f'{name}-{node["id"]}', 'data', 'image']) #self.resize_icon_events.append(lambda: self.canvas.itemconfig(icon_id, image=self.icons.get_icon(icon_name))) self.canvas.tag_bind(f'{name}-{node["id"]}', "<Button-1>", method) return icon_id def draw_read_button(self, node, box): self.draw_icon(node, box[0] + (box[2] - box[0]) / 2 - 31, box[3] + 12, "book-lightgray", method=lambda event, _node=node: self.read_mode(_node)) def draw_info_button(self, node, box): self.draw_icon(node, box[0] + (box[2] - box[0]) / 2 - 11, box[3] + 12, "stats-lightgray", method=lambda event, _node=node: self.show_info(_node)) def draw_edit_button(self, node, box): self.draw_icon(node, box[0] + (box[2] - box[0]) / 2 + 11, box[3] + 12, "edit-blue", method=lambda event, _node_id=node['id']: self. textbox_events[node['id']](_node_id)) def draw_delete_button(self, node, box): self.draw_icon( node, box[0] + (box[2] - box[0]) / 2 + 31, box[3] + 12, "trash-red", method=lambda event, _node=node: self.delete_node(_node)) def draw_newchild_button(self, node, box): self.draw_icon(node, box[2] - 13, box[3] + 12, "plus-blue", method=lambda event, _node=node: self.new_child(_node)) def draw_generate_button(self, node, box): self.draw_icon(node, box[2] - 36, box[3] + 12, "brain-blue", method=lambda event, _node=node: self.generate(_node)) def draw_memory_button(self, node, box): self.draw_icon(node, box[2] - 59, box[3] + 12, "memory-blue", method=lambda event, _node=node: self.memory(_node)) def draw_collapse_button(self, node, box): self.draw_icon( node, box[0] + 7, box[1] - 10, "minus-black", method=lambda event, _node=node: self.collapse_node(_node)) def draw_collapse_subtree_button(self, node, box): self.draw_icon( node, box[0] + 27, box[1] - 10, "collapse-black", method=lambda event, _node=node: self.collapse_node_subtree(_node)) def draw_collapse_except_subtree_button(self, node, box): self.draw_icon(node, box[0] + 50, box[1] - 10, "ancestry-black", method=lambda event, _node=node: self. collapse_except_subtree(_node)) def draw_mergeparent_button(self, node, box): self.draw_icon( node, box[0] + 72, box[1] - 10, "leftarrow-lightgray", method=lambda event, _node=node: self.merge_parent(_node)) def draw_changeparent_button(self, node, box): self.draw_icon( node, box[0] + 94, box[1] - 10, "broken_link-lightgray", method=lambda event, _node=node: self.change_parent(_node)) def draw_addlink_button(self, node, box): self.draw_icon( node, box[0] + 116, box[1] - 10, "add_link-lightgray", method=lambda event, _node=node: self.new_ghostparent(_node)) def draw_newparent_button(self, node, box): self.draw_icon(node, box[0] + 138, box[1] - 10, "plus_left-blue", method=lambda event, _node=node: self.new_parent(_node)) def draw_shiftup_button(self, node, box): self.draw_icon(node, box[0] + 160, box[1] - 10, "up-lightgray", method=lambda event, _node=node: self.shift_up(_node)) def draw_shiftdown_button(self, node, box): self.draw_icon(node, box[0] + 182, box[1] - 10, "down-lightgray", method=lambda event, _node=node: self.shift_down(_node)) def draw_mergechildren_button(self, node, box): self.draw_icon( node, box[2] - 79, box[1] - 10, "rightarrow-lightgray", method=lambda event, _node=node: self.merge_children(_node)) def draw_collapse_children_button(self, node, box): self.draw_icon( node, box[2] - 57, box[1] - 10, "collapse_left-black", method=lambda event, _node=node: self.collapse_children(_node)) def draw_expand_subtree_button(self, node, box): self.draw_icon( node, box[2] - 37, box[1] - 10, "subtree-green", method=lambda event, _node=node: self.expand_node_subtree(_node)) def draw_expand_children_button(self, node, box): self.draw_icon( node, box[2] - 14, box[1] - 10, "children-green", method=lambda event, _node=node: self.expand_children(_node)) def draw_bookmark_star(self, node, box): self.draw_icon( node, box[0] - 15, box[1] + (box[3] - box[1]) / 2, icon_name="star-black" if self.state.has_tag(node, "bookmark") else "empty_star-gray", name="bookmark", method=lambda event, _node=node: self.toggle_bookmark(_node)) ################################# # Expand/Collapse ################################# def select_node(self, node_id): self.selected_node = node_id self.controller.nav_select(node_id=node_id) def expand_node(self, node, change_selection=True, center_selection=True): ancestry = node_ancestry(node, self.state.tree_node_dict) for ancestor in ancestry: ancestor['open'] = True if change_selection or not self.selected_node['open']: #self.controller.nav_select(node) self.select_node(node) self.draw(self.root, self.selected_node, center_on_selection=center_selection) def expand_children(self, node): for child in node["children"]: child['open'] = True self.draw(self.root, self.selected_node, center_on_selection=False) def collapse_node(self, node, select_parent=False): if self.selected_node == node or select_parent: if node == self.root: self.select_node(self.root) else: node["open"] = False self.select_node(self.state.tree_node_dict[node["parent_id"]]) else: node["open"] = False self.draw(self.root, self.selected_node, center_on_selection=False) def expand_all(self): self.expand_subtree(self.root) def collapse_all(self, immune=None): self.collapse_subtree(self.root, immune=immune) def collapse_subtree(self, root, immune=None): if immune is None: immune = [] root["open"] = False for child in root["children"]: if child not in immune: self.collapse_subtree(child, immune) def expand_subtree(self, root): root['open'] = True for child in root["children"]: self.expand_subtree(child) def collapse_node_subtree(self, root): self.collapse_subtree(root) self.collapse_node(root, select_parent=True) def expand_node_subtree(self, root): self.expand_subtree(root) self.expand_node(root, change_selection=False) def collapse_except_subtree(self, root): self.collapse_all(immune=[root]) self.expand_node(root, center_selection=True) def collapse_children(self, node): self.collapse_subtree(node) self.expand_node(node, change_selection=False) ################################# # Topology ################################# # all these should use callbacks def merge_parent(self, node): self.controller.merge_parent(node) def merge_children(self, node): self.controller.merge_children(node) def change_parent(self, node): self.controller.change_parent(node, click_mode=True) def new_ghostparent(self, node): pass def new_parent(self, node): self.controller.create_parent(node) def new_child(self, node): self.controller.create_child(node) def shift_up(self, node): self.controller.move_up(node) def shift_down(self, node): self.controller.move_down(node) ################################# # Interaction ################################# def box_click(self, node_id, box, text): if text == '': return lambda event, node_id=node_id, box=box: self.edit_node( node_id=node_id, box=box, text=text) else: return lambda event, node_id=node_id: self.select_node(node_id= node_id) def edit_node(self, node_id, box, text): # self.select_node_func(node_id=node_id) self.delete_textbox() self.editing_node_id = node_id #fontheight = tkinter.font.Font(font=(self.font, self.get_text_size())).metrics('linespace') self.textbox = TextAware(self.canvas, bg=edit_color(), fg=active_text_color(), padx=10, pady=10, height=10, font=(self.font, self.get_text_size())) self.textbox.insert(tkinter.END, text) textbox_height = box[3] - box[1] if min_edit_box_height < box[3] - box[ 1] else min_edit_box_height textbox_width = self.state.visualization_settings['text_width'] self.textbox_id = self.canvas.create_window( box[0] + (box[2] - box[0]) / 2, box[1] + (box[3] - box[1]) / 2, window=self.textbox, height=textbox_height, width=textbox_width) def delete_textbox(self, save=True): if self.textbox is not None: if save: self.controller.save_edits() self.canvas.delete(self.textbox_id) #self.textbox.destroy() self.textbox = None self.editing_node_id = None self.textbox_id = None def toggle_bookmark(self, node): self.controller.bookmark(node) def delete_node(self, node): self.controller.delete_node(node) def generate(self, node): self.controller.generate(node) def memory(self, node): self.controller.memory(node) def read_mode(self, node): self.select_node(node) self.controller.toggle_visualization_mode() def show_info(self, node): self.controller.node_info_dialogue(node) ################################# # Util ################################# def get_active(self): if self.state.visualization_settings["chapter_mode"]: chapter_tree = self.state.build_chapter_trees()[1] return node_ancestry( chapter_tree[self.state.chapter(self.selected_node)['id']], chapter_tree) else: return node_ancestry(self.selected_node, self.state.tree_node_dict) # in node mode, returns true if node is selected node # in chapter mode, returns true if node corresponds to chapter of selected node def node_selected(self, node): if self.state.visualization_settings["chapter_mode"]: return node["id"] == self.state.chapter(self.selected_node)["id"] else: return node["id"] == self.selected_node["id"] def center_view_on_node(self, node): if not self.state.visualization_settings["chapter_mode"]: self.center_view_on_canvas_coords(*self.node_coords[node["id"]]) else: self.center_view_on_canvas_coords( *self.node_coords[self.state.chapter(node)["id"]]) def center_view(self, x, y): x = x * self.scroll_ratio y = y * self.scroll_ratio self.canvas.xview_moveto(x) self.canvas.yview_moveto(y) def center_view_on_canvas_coords(self, x, y): pass # x1, y1, x2, y2 = self.canvas.bbox("all") # screen_width_in_canvas_coords = self.canvas.canvasx(self.canvas.winfo_width()) - self.canvas.canvasx(0) # screen_height_in_canvas_coords = self.canvas.canvasy(self.canvas.winfo_height()) - self.canvas.canvasy(0) # self.canvas.xview_moveto((x - screen_width_in_canvas_coords / 2) / (x2 - x1)) # self.canvas.yview_moveto((y - screen_height_in_canvas_coords / 2) / (y2 - y1)) def reset_zoom(self): # TODO unknown bug, fix self.canvas.scale("all", 0, 0, 1 / self.scroll_ratio, 1 / self.scroll_ratio) self.canvas.configure( scrollregion=self.canvas_bbox_padding(self.canvas.bbox("all"))) self.scroll_ratio = 1 self.fix_text_zoom() self.icon_visibility_due_to_zoom()
def search_results(self, matches, start=0): # remove previous search results context_padding = 50 limit = 4 counter = 0 if self.num_results_label: self.num_results_label.destroy() if self.next_page_button: self.next_page_button.destroy() if self.prev_page_button: self.prev_page_button.destroy() for result in self.results: result.destroy() for label in self.labels: label.destroy() for button in self.goto_buttons: button.destroy() self.results = [] self.labels = [] self.goto_buttons = [] self.num_results_label = create_side_label(self.master, f'{len(matches)} results') for i, match in enumerate(matches[start:]): if counter >= limit: break node = self.state.tree_node_dict[match['node_id']] self.labels.append( create_side_label( self.master, f"chapter: {self.state.chapter(node)['title']}")) #side_label.config(fg="blue") self.results.append(TextAware(self.master, height=2)) #readable_font = Font(family="Georgia", size=12) self.results[i].configure( #font=readable_font, spacing1=8, foreground=text_color(), background=bg_color(), wrap="word", ) self.results[i].grid(row=self.master.grid_size()[1] - 1, column=1) node_text = node["text"] start_index = max(0, match['span'][0] - context_padding) end_index = min(len(node_text), match['span'][1] + context_padding) text_window = node_text[start_index:end_index] self.results[i].insert(tk.INSERT, text_window) self.results[i].tag_configure("blue", background="blue") self.results[i].highlight_pattern(match['match'], "blue") self.results[i].configure(state='disabled') # makes text copyable # binding causes computer to freeze #self.results[i].bind("<Button>", lambda event, _result=self.results[i]: result.focus_set()) #matched_text.bind("<Alt-Button-1>", lambda event: self.goto_result(match['node_id'])) self.goto_buttons.append( create_button( self.master, "go to match", lambda _match=match: self.goto_result(_match['node_id']))) self.goto_buttons[i].grid(row=self.master.grid_size()[1] - 2, column=2) counter += 1 if start > 0: self.prev_page_button = create_button( self.master, "previous page", lambda _start=start, _limit=limit: self.search_results( matches, start=_start - _limit)) self.prev_page_button.config(width=12) if len(matches) > start + limit: self.next_page_button = create_button( self.master, "next page", lambda _start=start, _limit=limit: self.search_results( matches, start=_start + _limit))
class BlockMultiverse: def __init__(self, parent_frame): self.parent_frame = parent_frame self.frame = None self.multiverse_frame = None self.bottom_input_frame = None self.past_box = None self.canvas = None self.wavefunction = None self.selected_id = None self.window_height = 450 self.node_info = {} self.build_canvas() self.build_past_box() self.window_offset = (0, 0) self.y_scale = default_y_scale self.x_scale = 1 self.bind_mouse_controls() self.prompt = None def clear_multiverse(self): self.wavefunction = None self.selected_id = None self.canvas.delete("all") self.node_info = {} self.set_pastbox_text('', '') self.prompt = None self.reset_view() def build_canvas(self): self.frame = ttk.Frame(self.parent_frame) self.multiverse_frame = ttk.Frame(self.frame) self.multiverse_frame.pack(expand=True, fill=tkinter.BOTH) self.canvas = tkinter.Canvas(self.multiverse_frame, bg="#808080") # hbar = tkinter.Scrollbar(self.multiverse_frame, orient=tkinter.HORIZONTAL) # hbar.pack(side=tkinter.BOTTOM, fill=tkinter.X) # hbar.config(command=self.canvas.xview) # vbar = tkinter.Scrollbar(self.multiverse_frame, orient=tkinter.VERTICAL) # vbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) # vbar.config(command=self.canvas.yview) # self.canvas.config( # xscrollcommand=hbar.set, # yscrollcommand=vbar.set # ) self.canvas.pack(side=tkinter.LEFT, expand=True, fill=tkinter.BOTH) #self.multiverse_frame.update_idletasks() #self.window_height = self.multiverse_frame.winfo_reqheight() * 2 def build_past_box(self): self.bottom_input_frame = ttk.Frame(self.frame) self.bottom_input_frame.pack(side="bottom", fill="x") self.past_box = TextAware(self.bottom_input_frame, bd=3, height=3) self.past_box.pack(expand=True, fill='x') self.past_box.configure( foreground='white', background='black', wrap="word", ) self.past_box.tag_configure("prompt", foreground="gray") self.past_box.configure(state="disabled") def set_pastbox_text(self, prompt_text='', completion_text=''): if self.past_box: self.past_box.configure(state="normal") self.past_box.delete('1.0', "end") self.past_box.insert('1.0', prompt_text, "prompt") self.past_box.insert("end-1c", completion_text) self.past_box.configure(state="disabled") self.past_box.see("end") def bind_mouse_controls(self): # FIXME # def _on_mousewheel(event): # self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") # self.frame.bind_all("<MouseWheel>", _on_mousewheel) # self.canvas.bind_all("<MouseWheel>", _on_mousewheel) # This is what enables scrolling with the mouse: def scroll_start(event): self.canvas.scan_mark(event.x, event.y) def scroll_move(event): self.canvas.scan_dragto(event.x, event.y, gain=1) self.canvas.bind("<ButtonPress-1>", scroll_start) self.canvas.bind("<B1-Motion>", scroll_move) # # windows zoom # def zoomer(event): # if event.delta > 0: # zoom_in(event) # self.scroll_ratio *= 1.1 # self.canvas.scale("all", event.x, event.y, 1.1, 1.1) # elif event.delta < 0: # zoom_out(event) # self.scroll_ratio *= 0.9 # self.canvas.scale("all", event.x, event.y, 0.9, 0.9) # self.canvas.configure(scrollregion=self.canvas.bbox("all")) # #self.fix_text_zoom() # #self.fix_image_zoom() # # linux zoom def zoom_in(event): self.y_scale *= 1.1 self.x_scale *= 1.1 self.canvas.scale("all", event.x, event.y, 1, 1.1) self.canvas.configure(scrollregion=self.canvas.bbox("all")) self.fix_text_zoom() #self.fix_image_zoom() def zoom_out(event): self.y_scale *= 0.9 self.x_scale *= 0.9 self.canvas.scale("all", event.x, event.y, 1, 0.9) self.canvas.configure(scrollregion=self.canvas.bbox("all")) # self.showtext = event.text > 0.8 self.fix_text_zoom() #self.fix_image_zoom() # Mac and then linux scrolls #self.canvas.bind("<MouseWheel>", zoomer) self.canvas.bind("<Button-4>", zoom_in) self.canvas.bind("<Button-5>", zoom_out) def get_text_size(self, original_size=10): text_size = max(1, math.floor(original_size * self.y_scale)) return min(text_size, 12) def fix_text_zoom(self): # size = self.get_text_size() # for item in self.canvas.find_withtag("text"): # self.canvas.itemconfig(item, font=('Arial', size)) for key, info in self.node_info.items(): size = self.get_text_size(info['font_size']) self.canvas.itemconfig(info['text_widget'], font=('Arial', size)) def set_y_window(self, x0, y0, height): old_y_scale = self.y_scale self.reset_view() self.window_offset = (x0, y0) self.canvas.move("all", -x0, -y0) self.y_scale = self.window_height / height magnification = self.y_scale / old_y_scale print('\nmagnification: *', "{:.2f}".format(magnification)) print('total magnification: ', "{:.2f}".format(self.y_scale)) print('+{:.2f} bits'.format(math.log(magnification, 2))) print('total bits: ', "{:.2f}".format(math.log(self.y_scale, 2))) self.canvas.scale("all", 0, 0, 1, self.y_scale) self.fix_text_zoom() def reset_view(self): self.canvas.scale("all", 0, 0, 1, default_y_scale / self.y_scale) self.y_scale = default_y_scale self.canvas.move("all", self.window_offset[0], self.window_offset[1]) self.window_offset = (0, 0) self.fix_text_zoom() if self.prompt: self.set_pastbox_text(prompt_text=self.prompt) def active_wavefunction(self): return self.wavefunction and self.selected_id def active_info(self): return self.node_info[self.selected_id] def node_clicked(self, x0, y0, height, node_id): self.selected_id = node_id #print(self.node_info[node_id]['token']) self.set_y_window(x0, y0, height) prefix_text = self.node_info[node_id]['prefix'] self.set_pastbox_text(prompt_text=self.prompt if self.prompt else '', completion_text=prefix_text) def draw_multiverse(self, multiverse, ground_truth='', block_width=150, start_position=(0, 0), color_index=0, prefix='', show_text=True, show_probabilities=False, prompt=''): if not self.prompt: self.prompt = prompt self.set_pastbox_text(prompt_text=self.prompt) if not self.wavefunction: self.wavefunction = multiverse else: if self.selected_id: #self.node_info[self.selected_id]['node']['children'] = multiverse prefix = self.node_info[self.selected_id]['prefix'] else: return if start_position == (0, 0): self.draw_block(0, 0, self.prompt[-20:], prefix, 1, Decimal(self.window_height), block_width, True, show_text, 0) self.propagate(multiverse, ground_truth, prefix, block_width, start_position, color_index, show_text, y_offset=0, depth=1) # TODO should work purely in absolute coordinates def propagate(self, multiverse, ground_truth, prefix, block_width, start_position, color_index, show_text, y_offset, depth): x = start_position[0] + (depth * block_width) rainbow_index = color_index % len(rainbow_colors) for token, node in multiverse.items(): y = start_position[1] + y_offset height = Decimal(self.window_height) * Decimal( node['unnormalized_prob']) is_ground_truth = (token == ground_truth[0]) if ground_truth else False self.draw_block(x, y, token, prefix, node['unnormalized_prob'], height, block_width, is_ground_truth, show_text, rainbow_index) self.propagate( node['children'], ground_truth=ground_truth[1:] if is_ground_truth else None, prefix=prefix + token, block_width=block_width, start_position=start_position, color_index=rainbow_index, show_text=show_text, y_offset=y_offset, depth=depth + 1, ) y_offset += height rainbow_index = (rainbow_index + 1) % len(rainbow_colors) def draw_block(self, x, y, token, prompt, probability, height, block_width, is_ground_truth, show_text, rainbow_index): color = 'black' if is_ground_truth else rainbow_colors[rainbow_index] identifier = str(uuid.uuid1()) self.draw_rectangle_absolute(x, y, x + block_width, y + height, fill=color, activefill='gray', activeoutline='white', outline=color, tags=[identifier]) self.canvas.tag_bind(f'{identifier}', "<Button-1>", lambda event, _id=identifier, _x=x, _y=y, _height= height: self.node_clicked(_x, _y, _height, _id)) self.node_info[identifier] = { 'id': identifier, 'prefix': prompt + token, 'token': token, 'amplitude': probability, 'x': x, 'y': y, } if show_text: text_color = 'blue' if color == '#FFFF00' else 'white' # if is_ground_truth else 'black' font_size = min(12, int(math.ceil(height * self.y_scale / 2))) text = token self.node_info[identifier]['font_size'] = Decimal( font_size) / Decimal(self.y_scale) self.node_info[identifier][ 'text_widget'] = self.draw_text_absolute( x + block_width / 2, y + height / 2, text=text, font=('Arial', font_size), tags=['text', f'text-{identifier}'], fill=text_color) return identifier # def propagate_realtime(self, prompt, ground_truth='', block_width=150, parent_position=(0,0), max_depth=3, # unnormalized_amplitude=1, threshold=0.01, rainbow_index=0, engine='ada'): # if ground_truth and isinstance(ground_truth, str): # ground_truth = tokenize(ground_truth) # ground_truth = [token_to_word(token).replace('Ġ', ' ') for token in ground_truth] # self.propagate_and_draw(prompt, ground_truth, block_width, parent_position, max_depth, unnormalized_amplitude, # threshold, rainbow_index, engine) # # def propagate_and_draw(self, prompt, ground_truth, block_width, parent_position, max_depth, # unnormalized_amplitude, threshold, rainbow_index, engine): # if max_depth == 0: # return # response = openai.Completion.create(prompt=prompt, # max_tokens=1, # n=1, # temperature=0, # logprobs=100, # engine=engine) # logprobs = response.choices[0]["logprobs"]["top_logprobs"][0] # probs = {k: logprobs_to_probs(v) * unnormalized_amplitude for k, v in sorted(logprobs.items(), # key=lambda item: item[1], # reverse=True)} # # ground_truth_token = ground_truth[0] if ground_truth else 'NO GROUND TRUTH' # x = parent_position[0] + block_width # y_offset = 0 # for token, probability in probs.items(): # y = parent_position[1] + y_offset # height = self.window_height * probability # is_ground_truth = (token == ground_truth_token) if ground_truth else False # self.draw_block(x, y, token, prompt, probability, height, block_width, is_ground_truth, True, rainbow_index) # # if token == ground_truth_token: # self.propagate_and_draw(prompt + token, ground_truth[1:], block_width, (x, y), max_depth-1, probability, # threshold, rainbow_index, engine) # elif probability > threshold: # self.propagate_and_draw(prompt + token, '', block_width, (x, y), max_depth - 1, # probability, threshold, rainbow_index, engine) # else: # break # y_offset += height # rainbow_index = (rainbow_index + 1) % len(rainbow_colors) def map_to_scaled_coordinates(self, x, y): x = x - self.window_offset[0] y = y - self.window_offset[1] y = y * self.y_scale return x, y def map_to_absolute_coordinates(self, x, y): x = x + self.window_offset[0] y = y + self.window_offset[1] y = Decimal(y) / Decimal(self.y_scale) return x, y # draw a rectangle with size and coordinates regardless of current zoom / pan state def draw_rectangle_absolute(self, x0, y0, x1, y1, **kwargs): rel_x0, rel_y0 = self.map_to_scaled_coordinates(x0, y0) rel_x1, rel_y1 = self.map_to_scaled_coordinates(x1, y1) return self.canvas.create_rectangle((rel_x0, rel_y0, rel_x1, rel_y1), **kwargs) def draw_text_absolute(self, x, y, **kwargs): rel_x, rel_y = self.map_to_scaled_coordinates(x, y) #rel_x = int(round(rel_x)) #rel_y = int(round(rel_y)) return self.canvas.create_text(rel_x, rel_y, **kwargs) def save_as_png(self, filename): # grabcanvas=ImageGrab.grab(bbox=self.canvas).save("test.png") # ttk.grabcanvas.save("test.png") self.canvas.postscript(file=filename + '.eps') # use PIL to convert to PNG img = Image.open(filename + '.eps') img.save(filename + '.png', 'png', quality=100)
def _build_textbox(self, frame, frame_attr, textbox_attr, height=1): textbox_frame = ttk.Frame(frame) self.__setattr__(frame_attr, textbox_frame) scrollbar = ttk.Scrollbar(textbox_frame, command=lambda *args: self.__getattribute__( textbox_attr).yview(*args)) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) textbox = TextAware(textbox_frame, bd=3, height=height, yscrollcommand=scrollbar.set, undo=True) self.__setattr__(textbox_attr, textbox) # TODO move this out textbox.bind("<Control-Button-1>", lambda event: self.edit_history(txt=textbox)) textbox.bind("<Control-Shift-Button-1>", lambda event: self.goto_history(txt=textbox)) textbox.bind("<Control-Alt-Button-1>", lambda event: self.split_node(txt=textbox)) textbox.bind("<Alt-Button-1>", lambda event: self.select_token(txt=textbox)) textbox.pack(expand=True, fill='both') readable_font = Font( family="Georgia", size=12) # Other nice options: Helvetica, Arial, Georgia textbox.configure( font=readable_font, spacing1=10, foreground=text_color(), background=bg_color(), padx=2, pady=5, spacing2=8, # Spacing between lines4 spacing3=5, wrap="word", )