1import re 2import tkinter as tk 3from tkinter import ttk 4 5from thonny import get_workbench 6from thonny.languages import tr 7from thonny.ui_utils import SafeScrollbar 8 9 10class OutlineView(ttk.Frame): 11 def __init__(self, master): 12 ttk.Frame.__init__(self, master) 13 self._init_widgets() 14 15 self._tab_changed_binding = ( 16 get_workbench() 17 .get_editor_notebook() 18 .bind("<<NotebookTabChanged>>", self._update_frame_contents, True) 19 ) 20 get_workbench().bind("Save", self._update_frame_contents, True) 21 get_workbench().bind("SaveAs", self._update_frame_contents, True) 22 get_workbench().bind_class("Text", "<<NewLine>>", self._update_frame_contents, True) 23 24 self._update_frame_contents() 25 26 def destroy(self): 27 try: 28 # Not sure if editor notebook is still living 29 get_workbench().get_editor_notebook().unbind( 30 "<<NotebookTabChanged>>", self._tab_changed_binding 31 ) 32 except Exception: 33 pass 34 self.vert_scrollbar["command"] = None 35 ttk.Frame.destroy(self) 36 37 def _init_widgets(self): 38 # init and place scrollbar 39 self.vert_scrollbar = SafeScrollbar(self, orient=tk.VERTICAL) 40 self.vert_scrollbar.grid(row=0, column=1, sticky=tk.NSEW) 41 42 # init and place tree 43 self.tree = ttk.Treeview(self, yscrollcommand=self.vert_scrollbar.set) 44 self.tree.grid(row=0, column=0, sticky=tk.NSEW) 45 self.vert_scrollbar["command"] = self.tree.yview 46 47 # set single-cell frame 48 self.columnconfigure(0, weight=1) 49 self.rowconfigure(0, weight=1) 50 51 # init tree events 52 self.tree.bind("<<TreeviewSelect>>", self._on_select, True) 53 self.tree.bind("<Map>", self._update_frame_contents, True) 54 55 # configure the only tree column 56 self.tree.column("#0", anchor=tk.W, stretch=True) 57 # self.tree.heading('#0', text='Item (type @ line)', anchor=tk.W) 58 self.tree["show"] = ("tree",) 59 60 self._class_img = get_workbench().get_image("outline-class") 61 self._method_img = get_workbench().get_image("outline-method") 62 63 def _update_frame_contents(self, event=None): 64 if not self.winfo_ismapped(): 65 return 66 67 self._clear_tree() 68 69 editor = get_workbench().get_editor_notebook().get_current_editor() 70 if editor is None: 71 return 72 73 root = self._parse_source(editor.get_code_view().get_content()) 74 for child in root[2]: 75 self._add_item_to_tree("", child) 76 77 def _parse_source(self, source): 78 # all nodes in format (parent, node_indent, node_children, name, type, linenumber) 79 root_node = (None, 0, [], None, None, None) # name, type and linenumber not needed for root 80 active_node = root_node 81 82 lineno = 0 83 for line in source.split("\n"): 84 lineno += 1 85 m = re.match(r"[ ]*[\w]{1}", line) 86 if m: 87 indent = len(m.group(0)) 88 while indent <= active_node[1]: 89 active_node = active_node[0] 90 91 t = re.match( 92 r"[ \t]*(async[ \t]+)?(?P<type>(def|class){1})[ ]+(?P<name>[\w]+)", line 93 ) 94 if t: 95 current = (active_node, indent, [], t.group("name"), t.group("type"), lineno) 96 active_node[2].append(current) 97 active_node = current 98 99 return root_node 100 101 # adds a single item to the tree, recursively calls itself to add any child nodes 102 def _add_item_to_tree(self, parent, item): 103 # create the text to be played for this item 104 item_type = item[4] 105 item_text = " " + item[3] 106 107 if item_type == "class": 108 image = self._class_img 109 elif item_type == "def": 110 image = self._method_img 111 else: 112 image = None 113 114 # insert the item, set lineno as a 'hidden' value 115 current = self.tree.insert(parent, "end", text=item_text, values=item[5], image=image) 116 117 for child in item[2]: 118 self._add_item_to_tree(current, child) 119 120 # clears the tree by deleting all items 121 def _clear_tree(self): 122 for child_id in self.tree.get_children(): 123 self.tree.delete(child_id) 124 125 def _on_select(self, event): 126 editor = get_workbench().get_editor_notebook().get_current_editor() 127 if editor: 128 code_view = editor.get_code_view() 129 focus = self.tree.focus() 130 if not focus: 131 return 132 133 values = self.tree.item(focus)["values"] 134 if not values: 135 return 136 137 lineno = values[0] 138 index = code_view.text.index(str(lineno) + ".0") 139 code_view.text.see(index) # make sure that the double-clicked item is visible 140 code_view.text.select_lines(lineno, lineno) 141 142 get_workbench().event_generate( 143 "OutlineDoubleClick", item_text=self.tree.item(self.tree.focus(), option="text") 144 ) 145 146 147def load_plugin() -> None: 148 get_workbench().add_view(OutlineView, tr("Outline"), "ne") 149