Ejemplo n.º 1
0
 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")
Ejemplo n.º 2
0
    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",
            )
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
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()
Ejemplo n.º 7
0
    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))
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
    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",
        )