import os import sys import re if sys.version_info.major >= 3: import tkinter as tk import tkinter.ttk as ttk import tkinter.font as tk_font from tkinter import messagebox as mb else: import Tkinter as tk import ttk import tkFont as tk_font #pylint: disable=E0401 import tkMessageBox as mb #pylint: disable=E0401 class ConfigurationLoadError(Exception): """Raised when the Configuration.tex could not be found or opened.""" pass def display_info_window(config_tex, search_list, width, valid_only, show_urls, calling_window, font=None, fg="white", bg="black"): # probably a simpler way to set up the formatted text # but found this format online and it works for now class FormattedText(tk.Text): def __init__(self, *args, **kwargs): # super(FormattedText,self).__init__(*args, **kwargs) tk.Text.__init__(self, *args, **kwargs) self.font = kwargs.get("font") self.update_font() def update_font(self, font=None): self.font = font or self.font if not self.font: # Fall back to defaults self.font = tk_font.nametofont(self.cget("font")) #TODO find a way to turn attributes on and off individually # instead of defining a font for each combination self.bold_font = tk_font.Font(**self.font.configure()) self.bold_mono_font = tk_font.Font(**self.font.configure()) self.italic_font = tk_font.Font(**self.font.configure()) self.mono_font = tk_font.Font(**self.font.configure()) self.normal_font = tk_font.Font(**self.font.configure()) self.underline_font = tk_font.Font(**self.font.configure()) self.url_font = tk_font.Font(**self.font.configure()) self.bold_font.configure(weight="bold") self.bold_mono_font.configure(family="Courier New", weight="bold") self.italic_font.configure(slant="italic") self.mono_font.configure(family="Courier New") self.underline_font.configure(underline=1) self.url_font.configure(family="Courier New") self.tag_configure("bold", font=self.bold_font) self.tag_configure("bold_mono", font=self.bold_mono_font) self.tag_configure("italic", font=self.italic_font) self.tag_configure("underline", font=self.underline_font) self.tag_configure("mono", font=self.mono_font) self.tag_configure("normal", font=self.normal_font) self.tag_configure("url", font=self.url_font, foreground="blue") self.tag_configure( "reverse", font=self.normal_font, background="white", foreground="black") # Another online discovery for auto-hiding scrollbars in a text widget class AutoHideScrollbar(ttk.Scrollbar): def set(self, low, high): if float(low) <= 0 and float(high) >= 1.0: try: self.tk.call("grid", "remove", self) except: pass else: try: self.grid() except: pass ttk.Scrollbar.set(self, low, high) try: result = parse_configuration_tex( config_tex, search_list, width, valid_only, show_urls) except ConfigurationLoadError: mb.showerror( "Configuration.tex Error", "Could not find/open Configuration.tex at: {}".format( config_tex or "No Path Specified" )) return pad_pixels = 30 info_window = tk.Toplevel() info_window.withdraw() # Hide it until we're ready to draw info_window.update_idletasks() title = " -> ".join([x for x in search_list if not x == "*"]) info_window.title('"{}" Info'.format(title)) # set title to search path # Create and place the scrollbar vsb = AutoHideScrollbar(info_window) vsb.grid(row=0, column=1, sticky="ns") # Create, place, and color the text widget text = FormattedText( info_window, yscrollcommand=vsb.set, highlightthickness=0, borderwidth=5, wrap=tk.WORD, font=font ) text.grid(row=0, column=0, sticky="nsew") text.configure(bg=bg, fg=fg) # Set up the scroll command vsb.config(command=text.yview) # Allow the text widget to expand info_window.rowconfigure(0, weight=1) info_window.columnconfigure(0, weight=1) # Madness to get the titlebar height offset_y = 0 if os.name == "nt": import ctypes try: # >= win 8.1 ctypes.windll.shcore.SetProcessDpiAwareness(2) except: # win 8.0 or less ctypes.windll.user32.SetProcessDPIAware() offset_y = int(info_window.geometry().rsplit('+', 1)[-1]) bar_height = info_window.winfo_rooty() - offset_y # very basic error checking and display # length of zero means no result was found # length of one is an error message if result: style = "normal" in_escape = False esc_code = "" # uncomment print line to get output in console while debugging # print(result) out = "" # build output string between esc seq one char at a time max_w = line_w = 0 # keep track of the longest line we have total_h = line_h = 0 def dump_out(out, style): text.insert("end", out, style) line_w = getattr(text, style+"_font").measure(out) line_h = getattr(text, style+"_font").metrics("linespace") return (line_w, line_h) for c in result: # quick hack to decode the escape seqs ret from the parse # only including encodings needed for Configuration.tex and a # few others for now if in_escape: esc_code += c if c == "m": # end of esc code # should be using these to turn font attributes on and off # but for now just have a style defined for current needs if esc_code == '[0m': style = "normal" if esc_code == "[10m": # switch to default family style = "normal" if esc_code == '[1m': # bold on if style == "mono": style = "bold_mono" # until a better method is found else: style = "bold" if esc_code == "[22m": # bold off if style == "bold_mono": style = "mono" else: style = "normal" if esc_code == '[3m': # italic on style = "italic" # [23m italic off if esc_code == "[4m": # underline on style = "underline" # [24m underline off if esc_code == '[11m': # switch to mono family style = "mono" if esc_code == '[7m': # reverse on style = "reverse" # [27m not reverse if esc_code == '[34m': # foreground blue if show_urls: style = "url" else: style = "mono" out = "" # found valid esc - clear out esc_code = "" in_escape = False continue if c == '\x1b': # found end of one esc and start of another # dump formatted output to window # and start over w, h = dump_out(out, style) line_w += w line_h = max(line_h, h) out = "" in_escape = True continue # Check for a newline and dump the output if c == "\n": w, h = dump_out(out, style) line_w += w line_h = max(line_h, h) max_w = max(line_w, max_w) total_h += line_h line_w = 0 # reset the line width counter out = "" out += c if out: w, h = dump_out(out, style) line_w += w line_h = max(line_h, h) max_w = max(line_w, max_w) total_h += line_h else: # Don't show the window - instead, show an error # that nothing was found info_window.destroy() mb.showerror( "Info Not Found", "No info found for: \"{}\"".format(title)) return # Fit the line width to our max line length topping out at our width value # Adjust the width to a fancy mess max_mono_w = text.mono_font.measure("-"*width) width = min((max_w, max_mono_w))+pad_pixels height = total_h+bar_height+5 # Calculate our x,y based on the calling window - attempting to center the info window # horizontally, but only lowering it by 30 pixels info_window.geometry("{}x{}+{}+{}".format( width, height, int(calling_window.winfo_rootx()+(calling_window.winfo_width()/2)-(width/2)), int(calling_window.winfo_rooty()+30) )) info_window.deiconify() # text.configure(width=final_w) # Adjust the height to the line count # text.configure(height=int(text.index("end-1c").split(".")[0])+1) # Add an extra line for looks # Set the text to read-only text.configure(state="disabled") # Ensure we return a reference to the window for updating colors and stack order checks return info_window def parse_configuration_tex(config_file, search_list, width, valid_only, show_urls): # valid_only: True - return only the valid config.plist options for the search term & # return an empty list if no valid options found # False: return whole text of section # # show_urls: True - return full url of links in the text # False - return only link text with no url try: config = open(config_file, "r") except: raise ConfigurationLoadError result = [] search_len = len(search_list) if search_len == 0: # we shouldn't get here, but just in case return result search_terms = ["\\section{"] search_terms[0] += search_list[0] text_search = search_list[search_len - 1] # ultimately looking for last item # set the search terms based on selected position if search_len == 1: # we're done pass elif search_len == 2: search_terms.append("\\subsection{Properties") search_terms.append("texttt{" + text_search + "}\\") elif search_len == 3: if search_list[0] == "NVRAM": # look for value in Introduction search_terms.append("\\subsection{Introduction") search_terms.append("texttt{" + text_search + "}") else: search_terms.append( "\\subsection{" + search_list[1] + " Properties") search_terms.append("texttt{" + text_search + "}\\") elif search_len == 4: item_zero = search_list[0] sub_search = "\\subsection{" if item_zero == "NVRAM": # look for UUID:term in Introduction sub_search = "\\subsection{Introduction" text_search = search_list[2] text_search += ":" text_search += search_list[3] text_search += "}" elif item_zero == "DeviceProperties": # look in Common sub_search += "Common" text_search += "}" elif item_zero == "Misc": # Entry Properties or subsub if len(search_list[2]) < 3: sub_search += "Entry Properties" else: sub_search = "\\subsubsection{" sub_search += search_list[1] text_search += "}" else: sub_search += search_list[1] sub_search += " Properties" text_search += "}\\" search_terms.append(sub_search) search_terms.append("texttt{" + text_search) elif search_len == 5: sub_search = "\\subsubsection{" sub_search += search_list[1] search_terms.append(sub_search) search_terms.append("texttt{" + text_search) # keep a set of prefixes that would break us out of our search disallowed = set() # move down the Configuration.tex to the section we want for i in range(0, len(search_terms)): while True: line = config.readline() if not line: return result line = line.strip() # Check for disallowed if line.startswith(tuple(disallowed)) and (search_terms[0] != "\\section{NVRAM" or not "\\label{nvram" in line): # We've broken out of our current scope - bail return result if search_terms[i] in line: # Make sure parent search prefixes get added # to the disallowed set if not search_terms[i].startswith("texttt{"): # Retain the prefix as needed disallowed.add(search_terms[i].split("{")[0]+"{") break align = False itemize = 0 not_first_item = False in_listing = False enum = 0 columns = 0 lines_between_valid = 0 last_line_ended_in_colon = False last_line_had_forced_return = False last_line_ended_in_return = False last_line_was_blank = False while True: # track document state & preprocess line before parsing line = config.readline() if not line: break line = line.strip() if line.startswith("%"): # skip comments continue if "\\subsection{Introduction}" in line: continue if "\\begin{tabular}" in line: result.append("\x1b[11m") for c in line: if c == "c": columns += 1 continue if "\\begin(align*}" in line: align = True continue if "\\end{align*}}" in line: align = False continue if "\\begin{itemize}" in line: itemize += 1 continue if "\\begin{enumerate}" in line: enum += 1 continue if "\\begin{lstlisting}" in line: in_listing = True result.append("\n\x1b[11m") result.append("-"*width) result.append("\n") continue if "\\begin{" in line: # ignore other begins continue if "\\mbox" in line: continue if "\\end{tabular}" in line: result.append("\x1b[10m") columns = 0 continue if "\\end{itemize}" in line: itemize -= 1 if itemize == 0 and enum == 0: not_first_item = False continue if "\\end{enumerate}" in line: enum = 0 if itemize == 0: not_first_item = False continue if "\\end{lstlisting}" in line: in_listing = False result.append("-"*width) result.append("\x1b[10m\n") continue if "\\end{" in line: # ignore other ends continue if "\\item" in line: if itemize == 0 and enum == 0: break # skip line, not itemizing, shouldn't get here else: if not_first_item or not last_line_ended_in_return: # newline before this item result.append("\n") not_first_item = True if itemize == 0: # in enum if search_len == 1: # first level enumerate, use numeric replace_str = str(enum) + "." else: # use alpha replace_str = "(" + chr(96 + enum) + ")" line = line.replace("\\item", replace_str) enum += 1 elif itemize == 1: # first level item line = line.replace("\\item", u"\u2022") else: line = line.replace("\\item", "-") # fix indenting line = " "*itemize + line if enum != 0: line = " " + line else: if itemize > 0 or enum > 0: # inside multi line item if last_line_had_forced_return: line = " "*itemize + line line = " " + line # indent if "section{" in line: # stop when next section is found # let's try only checking for "section{" instead of 3 checks # if "\\section{" in line or "\\subsection{" in line or "\\subsubsection{" in line: # reached end of current section break if line.strip() == "": # blank line, need linefeed, maybe two, maybe none if last_line_ended_in_colon: parsed_line = "\n" else: if last_line_was_blank: # skip this blank line continue else: parsed_line = "\n\n" last_line_was_blank = True else: last_line_was_blank = False parsed_line = parse_line(line, columns, width, align, valid_only, show_urls) if len(parsed_line) == 0: continue # post process line last_line_had_forced_return = False last_line_ended_in_colon = False if parsed_line.endswith("\n"): last_line_had_forced_return = True elif parsed_line.endswith(":"): parsed_line += "\n" if not_first_item: # treat as forced return instead last_line_had_forced_return = True else: last_line_ended_in_colon = True else: parsed_line += " " # add space for next word if parsed_line.endswith("\n"): # slightly different use than last_line_had_forced_return last_line_ended_in_return = True else: last_line_ended_in_return = False if valid_only: # we only want to return valid plist options for the field if itemize > 0: if "---" in line: if lines_between_valid < 10: result.append(parsed_line) else: if len(result) > 0: lines_between_valid += 1 else: result.append(parsed_line) if in_listing: result.append("\n") # Join the result into a single string and remove # leading, trailing, and excessive newlines # result = re.sub(r"\n{2,}",r"\n\n","".join(result)) # return result.strip("\n") # leave all excess internal newlines for now for easier debugging return "".join(result).strip("\n") # return re.sub("\n{2,}", "\n\n", "".join(result)).strip("\n") def parse_line(line, columns, width, align, valid_only, show_urls): ret = "" build_key = False key = "" col_width = 0 if columns > 0: col_width = int(width / (columns + 1)) ignore = False col_contents_len = 0 line = line.rstrip() for c in line: if build_key: if c in "{[": build_key = False if not valid_only: if key == "text": ret += "\x1b[0m" elif key == "textit": ret += "\x1b[3m" elif key == "textbf": ret += "\x1b[1m" elif key == "emph": ret += "\x1b[3m" elif key == "texttt": ret += "\x1b[11m" elif key == "href": if show_urls: ret += "\x1b[34m" else: ignore = True else: ignore = True if key != "href": key = "" elif c in " ,()\\0123456789$&": build_key = False ret += special_char(key) col_contents_len += 1 if c in ",()0123456789$": ret += c if c == "\\": if len(key) > 0: build_key = True key = "" elif c in "_^#": build_key = False ret += c col_contents_len += 1 key = "" else: key += c else: if c == "\\": build_key = True elif c in "}]": if not ignore: if not valid_only: if columns > 0: ret += "\x1b[22m" else: ret += "\x1b[0m" if key == "href": # ret += " " key = "" elif c == "]": ret += "]" ignore = False elif c == "{": if not valid_only: ret += "\x1b[11m" elif c == "&": if columns > 0: pad = col_width - col_contents_len - 1 if pad > 0: ret += " "*pad col_contents_len = 0 ret += "|" else: if not align: ret += "&" else: if not ignore: ret += c col_contents_len += 1 if len(key) > 0: ret += special_char(key) if not valid_only: if key == "tightlist": ret = "" else: if key == "hline": ret = "-"*(width-4) ret += "\n" if line.endswith("\\\\"): ret += "\n" return ret def special_char(key): if key == "kappa": return u"\u03f0" elif key == "lambda": return u"\u03bb" elif key == "mu": return u"\u03bc" elif key == "alpha": return u"\u03b1" elif key == "beta": return u"\u03b2" elif key == "gamma": return u"\u03b3" elif key == "leq": return u"\u2264" elif key == "cdot": return u"\u00b7" elif key == "in": return u"\u220a" elif key == "infty": return u"\u221e" elif key == "textbackslash": return "\\" elif key == "hline": return u"\u200b" else: return " "