Ejemplo n.º 1
0
    def __init__(self, app, *args, **kwargs):
        self.app = app
        ttk.Frame.__init__(self, app, *args, **kwargs)
        self.kwdb = KeywordTable()

        self._create_ui()
        self.reset()
        self.bind("<Visibility>", self._on_visibility)
        self.keyword_tree.bind("<<TreeviewSelect>>", self._on_list_select)
        self.lib_tree.bind("<<TreeviewSelect>>", self._on_lib_select)
        item = self.lib_tree.insert("", "end", text="All", values=[ALL_ID])
        self.lib_tree.selection_set(item)
Ejemplo n.º 2
0
class KwBrowser(ttk.Frame):
    '''A widget designed to view keyword documentation'''
    def __init__(self, app, *args, **kwargs):
        self.app = app
        ttk.Frame.__init__(self, app, *args, **kwargs)
        self.kwdb = KeywordTable()

        self._create_ui()
        self.reset()
        self.bind("<Visibility>", self._on_visibility)
        self.keyword_tree.bind("<<TreeviewSelect>>", self._on_list_select)
        self.lib_tree.bind("<<TreeviewSelect>>", self._on_lib_select)
        item = self.lib_tree.insert("", "end", text="All", values=[ALL_ID])
        self.lib_tree.selection_set(item)

    def reset(self):
        '''Reset the widgets and database to their startup state'''
        self.text.delete("1.0", "end")
        children = self.keyword_tree.get_children("")
        if len(children) > 0:
            self.keyword_tree.delete(*children)

        children = self.lib_tree.get_children("")
        if len(children) > 0:
            self.lib_tree.delete(*children)
        self.kwdb.reset()

    def search(self):
        '''Perform a search based on the criteria in the filter widget

        I should move this into the keywordtable object...
        '''
        type = self.filter.get_type()
        string = self.filter.get_string().lower()
        children = self.keyword_tree.get_children("")
        self.keyword_tree.delete(*children)
        library_selection = self.lib_tree.selection()

        library_id = ALL_ID
        if len(library_selection) > 0:
            item = library_selection[0]
            library = self.lib_tree.item(item, "text")
            library_id = self.lib_tree.item(item, "values")[0]
            
        # UGH. This is klunky. I should refactor this.
        pattern = self._glob_to_sql(string)
        parameters = []
        SQL = ["SELECT kw.name, kw.id, kw.doc, c.name",
               "FROM keyword_table as kw",
               "JOIN collection_table as c",
               ]
        if type == "name":
            SQL.append("WHERE kw.name LIKE ?")
            parameters.append(pattern)
        else:
            SQL.append("WHERE (kw.name like ? OR kw.doc like ?)")
            parameters.append(pattern)
            parameters.append(pattern)

        if library_id != ALL_ID:
            SQL.append("AND c.id = ?")
            parameters.append(str(library_id))
            
        SQL.append("AND kw.collection_id == c.id")
        sql_result = self.kwdb.execute("\n".join(SQL), parameters)

        keywords = sql_result.fetchall()
        first = None
        for (name, kw_id, doc, source) in sorted(keywords, key=lambda t: t[0].lower()):
            description = doc.split("\n")[0]
            item = self.keyword_tree.insert("", "end", text=name,values=(kw_id, source, description))
            if first is None: first = item
        if first is None:
            self.text.delete(1.0, "end")
        else:
            self.keyword_tree.selection_set(first)

    def add_built_in_libraries(self):
        self.kwdb.add_built_in_libraries()
        self._update_lib_tree()

    def add_library(self, name, *args):
        '''Add all the keywords for the given library

        This will load the library using the given args.
        '''
        self.kwdb.add_library(name, *args)
        self._update_lib_tree()

    def add_file(self, filename):
        '''Add documentation for all keywords in the given file'''
        self.kwdb.add_file(filename)
        self._update_lib_tree()

    def _create_list(self, parent):
        listframe = ttk.Frame(parent, width=200, borderwidth=0)
        self.lib_tree = ttk.Treeview(listframe, selectmode="browse", columns=["id"], displaycolumns=[])
        self.lib_tree.heading("#0", text="Libraries", anchor="w")
        self.lib_tree_vsb = AutoScrollbar(listframe, orient="vertical", command=self.lib_tree.yview)
        self.lib_tree.configure(yscrollcommand=self.lib_tree_vsb.set)

        self.keyword_tree = ttk.Treeview(listframe, columns=["kw_id", "source","description"], 
                                 displaycolumns=["source", "description"])
        self.keyword_tree.column("source", width=120, stretch=False)
        self.keyword_tree.heading("#0", text="Keyword Name", anchor="w")
        self.keyword_tree.heading("source", text="Source", anchor="w")
        self.keyword_tree.heading("description", text="Description", anchor="w")
        self.keyword_tree_ysb = AutoScrollbar(listframe, orient="vertical", command=self.keyword_tree.yview)
        self.keyword_tree.configure(yscrollcommand=self.keyword_tree_ysb.set)

        self.lib_tree.grid(row=0, column=0, sticky="nsew")
        self.lib_tree_vsb.grid(row=0, column=1, sticky="ns")
        self.keyword_tree.grid(row=0, column=2, sticky="nsew")
        self.keyword_tree_ysb.grid(row=0, column=3, sticky="ns")
        listframe.grid_rowconfigure(0, weight=1)
        listframe.grid_columnconfigure(2, weight=1)
        return listframe
        

    def _glob_to_sql(self, string):
        '''Convert glob-like wildcards to SQL wildcards

        * becomes %
        ? becomes _
        % becomes \% 
        \\ remains \\

        This also adds a leading and trailing %, unless the pattern begins with
        ^ or ends with $
        '''

        # What's with the chr(1) and chr(2) nonsense? It's a trick to
        # hide \* and \? from the * and ? substitutions. This trick
        # depends on the substitutiones being done in order.  chr(1)
        # and chr(2) were picked because I know those characters
        # almost certainly won't be in the input string
        table = (
            (r'\\', chr(1)),
            (r'\*', chr(2)),
            (r'\?', chr(3)),
            (r'%', r'\%'),
            (r'?', '_'),
            (r'*', '%'),
            (chr(1), r'\\'),
            (chr(2), r'\*'),
            (chr(3), r'\?')
            )
        for (a, b) in table:
            string = string.replace(a,b)

        if string.startswith("^"):
            string = string[1:]
        else:
            string = "%" + string
        if string.endswith("$"):
            string = string[:-1]
        else:
            string = string + "%"
        return string

    def _update_lib_tree(self):
        children = self.lib_tree.get_children("")
        self.lib_tree.delete(*children)
        item = self.lib_tree.insert("", "end", text="All", values=[ALL_ID])
        self.lib_tree.selection_set((item,))
        maxwidth = 0
        for (collection_name, collection_id) in sorted(self.kwdb.get_collections(), key=lambda x: x[0].lower()):
            item = self.lib_tree.insert("", "end", text=collection_name, values=[collection_id])
            maxwidth = max(maxwidth, len(collection_name))

        # the +16 is for the space reserved for the image in the TreeView
        width = self.app.fonts.default.measure("M"*maxwidth) + 16
        self.lib_tree.column("#0", width=width)
        
    def _create_ui(self):
        self.filter = FilterBox(self)
        self.filter.bind("<<Search>>", self._on_search)
        self.filter.entry.bind("<Down>", self._on_down)
        self.filter.entry.bind("<Up>", self._on_up)

        pw = tk.PanedWindow(self, orient="vertical", borderwidth=0, 
                            background=self.app.colors.accent,
                            sashwidth=4, sashpad=0)
        listframe = self._create_list(pw)
        dataframe = self._create_data(pw)
        pw.add(listframe)
        pw.add(dataframe)

        self.filter.pack(side="top", fill="x", pady=2)
        pw.pack(side="top", fill="both", expand="true", padx=4, pady=4)
        
    def _create_data(self, parent):
        dataframe = ttk.Frame(parent, borderwidth=1, relief="sunken")

        self.text = CustomText(dataframe, wrap="word", borderwidth=0, width=120, 
                               font=self.app.fonts.default, highlightthickness=0)
        self.text_ysb = AutoScrollbar(dataframe, orient="vertical", command=self.text.yview)
        self.text.configure(yscrollcommand=self.text_ysb.set)

        self.text.grid(row=0, column=0, sticky="nsew", padx=4)
        self.text_ysb.grid(row=0, column=1, sticky="ns")
        dataframe.grid_rowconfigure(0, weight=1)
        dataframe.grid_columnconfigure(0, weight=1)

        self.text.tag_configure("name", font=self.app.fonts.heading)
        self.text.tag_configure("args", font=self.app.fonts.italic)
        self.text.tag_configure("example", background="lightgray", font=self.app.fonts.fixed)
        self.text.tag_configure("search_string", background="yellow", 
                                borderwidth=1, relief="raised")
        self.text.tag_raise("sel")

        return dataframe

    def _get_keywords(self, library, search_string,type):
        '''Return a list of all keywords that match the criteria'''
        keywords = []
        search_string = search_string.lower()
        
        for kw in self.kwdb.get_keywords():
            keywords.append(kw)
        return keywords

    def _on_lib_select(self, event):
        '''Callback for when user selects a library'''
        if not self.winfo_viewable(): return
        self.search()

    def _on_up(self, event):
        '''Callback for when user presses up arrow in search box'''
        if len(self.keyword_tree.selection()) == 0:
            return
        current = self.keyword_tree.selection()[0]
        prev_item = self.keyword_tree.prev(current)
        if prev_item:
            self.keyword_tree.selection_clear()
            self.keyword_tree.selection_set(prev_item)
            self.keyword_tree.see(prev_item)

    def _on_down(self, event):
        '''Callback for when user presses down arrow in search box'''
        if len(self.keyword_tree.selection()) == 0:
            return
        current = self.keyword_tree.selection()[0]
        next_item = self.keyword_tree.next(current)
        if next_item:
            self.keyword_tree.selection_clear()
            self.keyword_tree.selection_set(next_item)
            self.keyword_tree.see(next_item)

    def _on_visibility(self, event):
        '''Callback for when the UI is first realized'''
        # this causes the list to be updated with all the loaded files
        self.search()
        self.bind("<Visibility>", None)
        self.filter.set_focus()

    def _on_list_select(self, event):
        '''Callback for when user clicks on a keyword'''
        selection = self.keyword_tree.selection()
        item = selection[0]
        (kwid, library, description) = self.keyword_tree.item(item, "values")
        self.text.configure(state="normal")
        self.text.delete(1.0, "end")

        sql_result = self.kwdb.execute('''
            SELECT kw.name, kw.id, kw.doc, kw.args, c.name, c.type
            FROM keyword_table as kw
            JOIN collection_table as c
            WHERE kw.id == ? AND c.id == kw.collection_id
        ''', (kwid,))
        (kw_name, kw_id, kw_doc, kw_args, 
         collection_name, collection_type) = sql_result.fetchone()

        self.text.insert("end", kw_name + "\n", "name", "\n")
        if collection_type == "library" and collection_name != "BuiltIn":
            self.text.insert("end", "| Library | %s \n\n" % collection_name)
        if len(kw_args) > 0:
            self.text.insert("end", "Arguments: %s\n\n" % kw_args, "args")
        self.text.insert("end", self._cleanup_whitespace(kw_doc))
        self.text.highlight_pattern("^\|.*?$", self._on_highlight_example)
        self.text.highlight_pattern("(?iq)"+self.filter.get_string(), 
                                    self._on_highlight_search_string)
        self.text.configure(state="disabled")

    def _cleanup_whitespace(self, s):
        '''Clean up whitespace artifacts due to robot parsing

        This replaces the literal string \n with a newline, and
        removes a single space immediately after a newline
        '''
        s = s.replace(r'\n', "\n")
        s = s.replace("\n ", "\n")
        return s

    def _on_highlight_search_string(self):
        '''Callback to apply highlighting of the search string'''
        self.text.tag_add("search_string", "matchStart", "matchEnd")

    def _on_highlight_example(self):
        '''Callback to apply highlighting on an example'''
        self.text.tag_add("example", "matchStart", "matchEnd+1c")

    def _on_search(self, *args):
        '''Callback for when user changes the search string'''
        self.search()