#!/usr/bin/env python import sys, os, plistlib, base64, binascii, datetime, tempfile, shutil, re, subprocess, math, hashlib, time from collections import OrderedDict from Scripts import config_tex_info try: # Python 2 import Tkinter as tk import ttk import tkFileDialog as fd import tkMessageBox as mb from tkFont import Font from itertools import izip_longest as izip except ImportError: # Python 3 import tkinter as tk import tkinter.ttk as ttk from tkinter import filedialog as fd from tkinter import messagebox as mb from tkinter.font import Font from itertools import zip_longest as izip from . import plist try: long unicode except NameError: # Python 3 long = int unicode = str class EntryPlus(tk.Entry): def __init__(self,parent,master,**kw): tk.Entry.__init__(self, parent, **kw) self.parent = parent self.master = master key = "Command" if str(sys.platform) == "darwin" else "Control" self.bind("<{}-a>".format(key), self.select_all) self.bind("<{}-c>".format(key), self.copy) self.bind("<{}-v>".format(key), self.paste) self.bind("", self.select_prior) self.bind("", self.select_after) self.bind("", self.goto_start) self.bind("", self.goto_end) self.bind("", self.clear_selection) def clear_selection(self, event=None): self.selection_range(0, 0) return 'break' def select_prior(self, *ignore): self.selection_range(0,self.index(tk.INSERT)) self.icursor(0) return 'break' def select_after(self, *ignore): self.selection_range(self.index(tk.INSERT),"end") self.icursor("end") return 'break' def select_all(self, *ignore): self.selection_range(0,"end") self.icursor("end") # returns 'break' to interrupt default key-bindings return 'break' def goto_start(self, event=None): self.selection_range(0, 0) self.icursor(0) return 'break' def goto_end(self, event=None): self.selection_range(0, 0) self.icursor("end") return 'break' def copy(self, event=None): try: get = self.selection_get() except: get = "" if not len(get): return 'break' self.master._clipboard_append(get) self.update() return 'break' def paste(self, event=None): try: contents = self.master.clipboard_get() except: contents = "" if len(contents): try: get = self.selection_get() except: get = "" if len(get): # Have a selection - let's get the first and last start = self.index(tk.SEL_FIRST) end = self.index(tk.SEL_LAST) self.delete(start,end) else: start = self.index(tk.INSERT) self.insert(start,contents) return 'break' class EntryPopup(EntryPlus): def __init__(self, parent, master, text, cell, column, **kw): # tk.Entry.__init__(self, parent, **kw) EntryPlus.__init__(self, parent, master, **kw) self.original_text = text self.insert(0, text) self.select_all() self['state'] = 'normal' self['readonlybackground'] = 'white' self['selectbackground'] = '#1BA1E2' self['exportselection'] = True self.cell = cell self.column = column self['font'] = Font(font=self.master.font) self.focus_force() self.bind("",self.reveal) self.bind("", lambda x:[self.reveal(x),self.cancel(x)]) self.bind("", lambda x:[self.reveal(x),self.confirm(x)]) self.bind("", lambda x:[self.reveal(x),self.confirm(x)]) self.bind("", lambda x:[self.reveal(x),self.goto_start(x)]) self.bind("", lambda x:[self.reveal(x),self.goto_end(x)]) self.bind("", lambda x:[self.reveal(x),self.next_field(x)]) self.bind("", self.focus_out) # Lock to avoid prematurely cancelling on focus_out self.confirming = False def reveal(self, event=None): # Make sure we're visible if editing self.parent.see(self.cell) self.relocate() def focus_out(self, event=None): if self.confirming: return # Don't do anything if we're still confirming if self.master.focus_get(): # Pass True as the event to allow the bell() when our window still # has focus (means we're actively editing) self.confirm(event=True) else: # Pass None as the event to prevent the bell() self.confirm(no_prompt=True) def relocate(self, event=None): # Helper called when the window is scrolled to move the popup bbox = self.parent.bbox(self.cell, column=self.column) if bbox: # Move the entry to accommodate the new cell bbox x,y,width,height = bbox pady = height//2 self.place(x=x,y=y+pady,anchor="w",width=width) elif self.winfo_viewable(): # Entry left the visible area, and our popup is still visible, # hide it self.place_forget() def cancel(self, event=None): # Destroy ourself, then force the parent to focus self.destroy() self.master.entry_popup = None self.parent.focus_force() def next_field(self, event=None): # We need to determine if our other field can be edited # and if so - trigger another double click event there edit_col = None if self.column == "#0": check_type = self.master.get_check_type(self.cell).lower() # We are currently in the key column if check_type in ("dictionary","array"): # Can't edit the other field with these - bail return 'break' edit_col = "#2" elif self.column == "#2": # It's the value column - let's see if we can edit the key parent = self.master._tree.parent(self.cell) check_type = "dictionary" if not len(parent) else self.master.get_check_type(parent).lower() if check_type == "array" or self.cell == self.master.get_root_node(): # Can't edit array keys - as they're just indexes return 'break' edit_col = "#0" if edit_col: # Let's get the bounding box for our other field x,y,width,height = self.master._tree.bbox(self.cell, edit_col) # Create an event e = tk.Event e.x, e.y, e.x_root, e.y_root = x+5, y+5, 0, 0 self.master.on_double_click(e) return 'break' def confirm_clear_and_focus(self): # Helper to clear confirming, then focus the widget self.confirming = False return self.focus_force() def check_edited(self, value): # Make sure we're Edited if the value is different if value != self.original_text and not self.master.edited: self.master.edited = True self.master.title(self.master.title()+" - Edited") def confirm(self, event=None, no_prompt = False): if not self.winfo_exists(): return self.confirming = True # Lock confirming if self.column == "#0": # First we make sure that no other siblings # have the same name - as dict names need to be # unique parent = self.parent.parent(self.cell) text = self.get() for child in self.parent.get_children(parent): if child == self.cell: # Skip ourselves continue # Check if our text is equal to any other # keys if text == self.parent.item(child, "text"): # Have a match, beep and bail if event: self.bell() # Only bell when we have a real event (i.e. return was pressed) if no_prompt or not mb.askyesno("Invalid Key Name","That key name already exists in that dict.\n\nWould you like to keep editing?",parent=self.parent): return self.cancel(event) # no_prompt is false and we wanted to continue editing - set focus again and return return self.confirm_clear_and_focus() # Add to undo stack self.master.add_undo({"type":"edit","cell":self.cell,"text":self.parent.item(self.cell,"text"),"values":self.parent.item(self.cell,"values")}) # No matches, should be safe to set self.parent.item(self.cell, text=self.get()) # Make sure we check if we're edited self.check_edited(text) else: # Need to walk the values and pad values = self.parent.item(self.cell)["values"] or [] # Count up, padding as we need index = int(self.column.replace("#","")) values += [''] * (index - len(values)) original = [x for x in values] # Sanitize our value based on type type_value = self.master.get_check_type(self.cell).lower() value = self.get() # We need to sanitize data and numbers for sure if type_value.lower() == "date" and value.lower() in ("today","now"): # Set it to today first value = datetime.datetime.now().strftime("%b %d, %Y %I:%M:%S %p") output = self.master.qualify_value(value,type_value) if output[0] == False: # Didn't pass the test - show the error and prompt for edit continuing if event: self.bell() # Only bell when we have a real event (i.e. return was pressed) if no_prompt or not mb.askyesno(output[1],output[2]+"\n\nWould you like to keep editing?",parent=self.parent): return self.cancel(event) # no_prompt is false and we wanted to continue editing - set focus again and return return self.confirm_clear_and_focus() # Set the value to the new output value = output[1] # Add to undo stack self.master.add_undo({"type":"edit","cell":self.cell,"text":self.parent.item(self.cell,"text"),"values":original}) # Replace our value (may be slightly modified) values[index-1] = value # Set the values self.parent.item(self.cell, values=values) # Make sure we check if we're edited self.check_edited(value.replace("<","").replace(">","") if type_value.lower() == "data" else value) # Call cancel to close the popup as we're done editing self.cancel(event) class PlistWindow(tk.Toplevel): def __init__(self, controller, root, **kw): tk.Toplevel.__init__(self, root, **kw) self.plist_header = """ """ self.plist_footer = """ """ # Create the window self.root = root self.controller = controller self.undo_stack = [] self.redo_stack = [] self.drag_undo = None self.clicked_drag = False self.saving = False self.last_data = None self.last_int = None self.last_bool = None # self.xcode_data = self.controller.xcode_data # keep xxxx in one line when true # self.sort_dict = self.controller.sort_dict # Preserve key ordering in dictionaries when loading/saving self.menu_code = u"\u21D5" #self.drag_code = u"\u2630" self.drag_code = u"\u2261" self.safe_path_length = 128 # OC_STORAGE_SAFE_PATH_MAX from Include/Acidanthera/Library/OcStorageLib.h in OpenCorePkg # self = tk.Toplevel(self.root) try: w = int(self.controller.settings.get("last_window_width",730)) h = int(self.controller.settings.get("last_window_height",480)) except: # wut - who be breakin dis? w = 730 h = 480 # Save the previous states for comparison self.previous_height = h self.previous_width = w self.minsize(width=730,height=480) self.protocol("WM_DELETE_WINDOW", self.close_window) # Let's also center the window x = self.winfo_screenwidth() // 2 - w // 2 y = self.winfo_screenheight() // 2 - h // 2 self.geometry("{}x{}+{}+{}".format(w,h, x, y)) # Set the title to "Untitled.plist" self.title("Untitled.plist") # Let's track resize events self.bind("",lambda event,obj=self: self.window_resize(event,obj)) # Set up the options self.current_plist = None # None = new self.edited = False self.dragging = False self.drag_start = None self.drag_open = None self.key_history = "" self.last_key = 0 self.last_node_result = None self.last_key_threhsold = 1 # Ignore after 1 second self.mod_bitmask = self.get_mod_bitmask() # Get a bit mask for excluded modifier keys self.show_find_replace = False self.show_type = False self.type_menu = tk.Menu(self, tearoff=0) self.type_menu.add_command(label="Dictionary", command=lambda:self.change_type(self.menu_code + " Dictionary")) self.type_menu.add_command(label="Array", command=lambda:self.change_type(self.menu_code + " Array")) self.type_menu.add_separator() self.type_menu.add_command(label="Boolean", command=lambda:self.change_type(self.menu_code + " Boolean")) self.type_menu.add_command(label="Data", command=lambda:self.change_type(self.menu_code + " Data")) self.type_menu.add_command(label="Date", command=lambda:self.change_type(self.menu_code + " Date")) self.type_menu.add_command(label="Number", command=lambda:self.change_type(self.menu_code + " Number")) self.type_menu.add_command(label="UID", command=lambda:self.change_type(self.menu_code + " UID")) self.type_menu.add_command(label="String", command=lambda:self.change_type(self.menu_code + " String")) # Set up the Root node type menu - only supports Array and Dict self.root_type_menu = tk.Menu(self, tearoff=0) self.root_type_menu.add_command(label="Dictionary", command=lambda:self.change_type(self.menu_code + " Dictionary")) self.root_type_menu.add_command(label="Array", command=lambda:self.change_type(self.menu_code + " Array")) self.style = ttk.Style() # Treeview theming is horribly broken in Windows for whatever reasons... self.style_name = "Corp.TLabel" if os.name=="nt" else "Corp.Treeview" # Attempt to set the color of the headers if not "Corp.Treeheading.border" in self.style.element_names(): self.style.element_create("Corp.Treeheading.border", "from", "default") self.style.layout(self.style_name+".Heading", [ ("Corp.Treeheading.cell", {'sticky': 'nswe'}), ("Corp.Treeheading.border", {'sticky':'nswe', 'children': [ ("Corp.Treeheading.padding", {'sticky':'nswe', 'children': [ ("Corp.Treeheading.image", {'side':'right', 'sticky':''}), ("Corp.Treeheading.text", {'sticky':'we'}) ]}) ]}), ]) self.style.configure(self.style_name+".Heading",borderwidth=1,relief="groove") self.style.map(self.style_name+".Heading",relief=[('active','raised'),('pressed','sunken')]) # Fix font height for High-DPI displays self.font = Font(font='TkTextFont') self.set_font_size() self.set_font_family() # Create the treeview self._tree_frame = tk.Frame(self) # self._tree = ttk.Treeview(self._tree_frame, columns=("Type","Value","Drag"), selectmode="browse", style=self.style_name) self._tree = ttk.Treeview(self._tree_frame, columns=("Type","Value"), selectmode="browse", style=self.style_name) self._tree.heading("#0", text="Key") self._tree.heading("#1", text="Type") self._tree.heading("#2", text="Value") self._tree.column("Type",width=int(self._tree.winfo_reqwidth()/4),stretch=False) # self._tree.column("Drag",minwidth=40,width=40,stretch=False,anchor="center") # Setup the initial colors self.r1 = self.r2 = self.hl = self.r1t = self.r2t = self.hlt = None self.set_colors() # Set the close window and copy/paste bindings key = "Command" if str(sys.platform) == "darwin" else "Control" # Add the window bindings self.bind("<{}-w>".format(key), self.close_window) self.bind("<{}-f>".format(key), self.hide_show_find) self.bind("<{}-p>".format(key), self.hide_show_type) # Add rbits binding self.bind("<{}-i>".format(key), self.show_config_info) # Add the treeview bindings self._tree.bind("<{}-c>".format(key), self.copy_selection) self._tree.bind("<{}-C>".format(key), self.copy_all) self._tree.bind("<{}-v>".format(key), self.paste_selection) # Create the scrollbar self.vsb = ttk.Scrollbar(self._tree_frame,orient='vertical',command=self._tree.yview) self._tree.configure(yscrollcommand=self.scrollbar_set) # Bind right click if str(sys.platform) == "darwin": self._tree.bind("", self.popup) # ButtonRelease-2 on mac self._tree.bind("", self.popup) # Ctrl+Left Click on mac else: self._tree.bind("", self.popup) # Set bindings self._tree.bind("", self.on_double_click) self._tree.bind('<>', self.tree_click_event) self._tree.bind('<>', self.pre_alternate) self._tree.bind('<>', self.alternate_colors) self._tree.bind("", self.move_selection) self._tree.bind("",self.confirm_drag) self._tree.bind("",self.clicked) self._tree.bind("<{}-equal>".format(key), self.new_row) self._tree.bind("<{}-plus>".format(key), self.new_row) self._tree.bind("<{}-KP_Add>".format(key), self.new_row) self._tree.bind("<{}-minus>".format(key), self.remove_row) self._tree.bind("<{}-underscore>".format(key), self.remove_row) self._tree.bind("<{}-KP_Subtract>".format(key), self.remove_row) self._tree.bind("", self.remove_row) self._tree.bind("", self.remove_row) self._tree.bind("", self.start_editing) self._tree.bind("", self.start_editing) self.bind("", self.got_focus) self._tree.bind("", self.quick_search) # Set type and bool bindings self._tree.bind("<{}-Up>".format(key), lambda x:self.cycle_type(increment=False)) self._tree.bind("<{}-Down>".format(key), lambda x:self.cycle_type(increment=True)) self._tree.bind("<{}-Left>".format(key), self.cycle_bool) self._tree.bind("<{}-Right>".format(key), self.cycle_bool) # Set up cmd/ctrl+number key binds to change types as needed menu_max = len(self._get_menu_commands(self.type_menu)) for i in range(menu_max): self._tree.bind("<{}-Key-{}>".format(key,i+1), lambda x:self.set_type_by_index(x)) self._tree.bind("<{}-KP_{}>".format(key,i+1), lambda x:self.set_type_by_index(x)) # Set expansion bindings self._tree.bind("", lambda x:self.expand_children()) self._tree.bind("", lambda x:self.collapse_children()) self.recent_menu = None # Setup menu bar (hopefully per-window) - only happens on non-mac systems if not str(sys.platform) == "darwin": main_menu = tk.Menu(self) file_menu = tk.Menu(self, tearoff=0) self.recent_menu = tk.Menu(self, tearoff=0) main_menu.add_cascade(label="File", menu=file_menu) file_menu.add_command(label="New", command=self.controller.new_plist, accelerator="Ctrl+N") file_menu.add_command(label="Open", command=self.controller.open_plist, accelerator="Ctrl+O") file_menu.add_cascade(label="Open Recent", menu=self.recent_menu) file_menu.add_command(label="Save", command=self.controller.save_plist, accelerator="Ctrl+S") file_menu.add_command(label="Save As...", command=self.controller.save_plist_as, accelerator="Ctrl+Shift+S") file_menu.add_command(label="Duplicate", command=self.controller.duplicate_plist, accelerator="Ctrl+D") file_menu.add_command(label="Reload From Disk", command=self.reload_from_disk, accelerator="Ctrl+L") file_menu.add_separator() file_menu.add_command(label="OC Snapshot", command=self.oc_snapshot, accelerator="Ctrl+R") file_menu.add_command(label="OC Clean Snapshot", command=self.oc_clean_snapshot, accelerator="Ctrl+Shift+R") file_menu.add_separator() file_menu.add_command(label="Convert Window", command=lambda:self.controller.show_window(self.controller.tk), accelerator="Ctrl+T") file_menu.add_command(label="Strip Comments", command=self.strip_comments, accelerator="Ctrl+M") file_menu.add_command(label="Strip Disabled Entries", command=self.strip_disabled, accelerator="Ctrl+E") file_menu.add_separator() file_menu.add_command(label="Settings",command=lambda:self.controller.show_window(self.controller.settings_window), accelerator="Ctrl+,") file_menu.add_separator() file_menu.add_command(label="Toggle Find/Replace Pane",command=self.hide_show_find, accelerator="Ctrl+F") file_menu.add_command(label="Toggle Plist/Data/Int/Bool Type Pane",command=self.hide_show_type, accelerator="Ctrl+P") file_menu.add_separator() file_menu.add_command(label="Quit", command=self.controller.quit, accelerator="Ctrl+Q") self.config(menu=main_menu) # Get the right click menu options cwd = os.getcwd() os.chdir(os.path.abspath(os.path.dirname(os.path.realpath(__file__)))) self.menu_data = {} if os.path.exists("menu.plist"): try: with open("menu.plist","rb") as f: self.menu_data = plist.load(f) except: pass os.chdir(cwd) # Create our type/data view self.display_frame = tk.Frame(self,height=20) for x in (2,4,6,8): self.display_frame.columnconfigure(x,weight=1) pt_label = tk.Label(self.display_frame,text="Plist Type:") dt_label = tk.Label(self.display_frame,text="Display Data as:") in_label = tk.Label(self.display_frame,text="Display Ints as:") bl_label = tk.Label(self.display_frame,text="Display Bools as:") self.plist_type_string = tk.StringVar(self.display_frame) self.plist_type_menu = tk.OptionMenu(self.display_frame, self.plist_type_string, *self.controller.allowed_types, command=self.change_plist_type) self.plist_type_string.set(self.controller.allowed_types[0]) self.data_type_string = tk.StringVar(self.display_frame) self.data_type_menu = tk.OptionMenu(self.display_frame, self.data_type_string, *self.controller.allowed_data, command=self.change_data_type) self.data_type_string.set(self.controller.allowed_data[0]) self.int_type_string = tk.StringVar(self.display_frame) self.int_type_menu = tk.OptionMenu(self.display_frame, self.int_type_string, *self.controller.allowed_int, command=self.change_int_type) self.int_type_string.set(self.controller.allowed_int[0]) self.bool_type_string = tk.StringVar(self.display_frame) self.bool_type_menu = tk.OptionMenu(self.display_frame, self.bool_type_string, *self.controller.allowed_bool, command=self.change_bool_type) self.bool_type_string.set(self.controller.allowed_bool[0]) pt_label.grid(row=1,column=1,pady=10,sticky="w") dt_label.grid(row=1,column=3,pady=10,sticky="w") in_label.grid(row=1,column=5,pady=10,sticky="w") bl_label.grid(row=1,column=7,pady=10,sticky="w") self.plist_type_menu.grid(row=1,column=2,pady=10,padx=5,sticky="we") self.data_type_menu.grid(row=1,column=4,pady=10,padx=5,sticky="we") self.int_type_menu.grid(row=1,column=6,pady=10,padx=5,sticky="we") self.bool_type_menu.grid(row=1,column=8,pady=10,padx=5,sticky="we") # Create our find/replace view self.find_frame = tk.Frame(self,height=20) self.find_frame.columnconfigure(2,weight=1) f_label = tk.Label(self.find_frame, text="Find:") f_label.grid(row=0,column=0,sticky="e") r_label = tk.Label(self.find_frame, text="Replace:") r_label.grid(row=1,column=0,sticky="e") self.f_options = ["Key", "Boolean", "Data", "Date", "Number", "UID", "String"] self.find_type = self.f_options[0] self.f_text = EntryPlus(self.find_frame,self) self.f_text.bind("", self.find_next) self.f_text.bind("", self.find_next) self.f_text.delete(0,tk.END) self.f_text.insert(0,"") self.f_text.grid(row=0,column=2,sticky="we",padx=10,pady=10) self.r_text = EntryPlus(self.find_frame,self) self.r_text.bind("", self.replace) self.r_text.bind("", self.replace) self.r_text.delete(0,tk.END) self.r_text.insert(0,"") self.r_text.grid(row=1,column=2,columnspan=1,sticky="we",padx=10,pady=10) self.f_title = tk.StringVar(self.find_frame) self.f_title.set(self.find_type) f_option = tk.OptionMenu(self.find_frame, self.f_title, *self.f_options, command=self.change_find_type) f_option['menu'].insert_separator(1) f_option.grid(row=0,column=1) self.fp_button = tk.Button(self.find_frame,text="< Prev",width=8,command=self.find_prev) self.fp_button.grid(row=0,column=3,sticky="e",padx=0,pady=10) self.fn_button = tk.Button(self.find_frame,text="Next >",width=8,command=self.find_next) self.fn_button.grid(row=0,column=4,sticky="w",padx=0,pady=10) self.r_button = tk.Button(self.find_frame,text="Replace",command=self.replace) self.r_button.grid(row=1,column=4,sticky="we",padx=10,pady=10) self.r_all_var = tk.IntVar() self.r_all = tk.Checkbutton(self.find_frame,text="Replace All",variable=self.r_all_var) self.r_all.grid(row=1,column=5,sticky="w") self.f_case_var = tk.IntVar() self.f_case = tk.Checkbutton(self.find_frame,text="Case-Sensitive",variable=self.f_case_var) self.f_case.grid(row=0,column=5,sticky="w") # Set find_frame bindings - also bind to child widgets to ensure keybinds are captured def set_frame_binds(widget): for k in ("Up","Down"): widget.bind("<{}-{}>".format(key,k), lambda x:self.cycle_find_type(x)) for i,opt in enumerate(self.f_options,start=1): widget.bind("<{}-Key-{}>".format(key,i), lambda x:self.set_find_type_by_index(x)) widget.bind("<{}-KP_{}>".format(key,i), lambda x:self.set_find_type_by_index(x)) for child in widget.children.values(): set_frame_binds(child) set_frame_binds(self.find_frame) # Add the scroll bars and show the treeview self.vsb.pack(side="right",fill="y") self._tree.pack(side="bottom",fill="both",expand=True) self.draw_frames() self.entry_popup = None self.controller.set_window_opacity(window=self) def get_mod_bitmask(self): # Helper to return a bitmask for modifier keys based on the OS we're running bit_mask = 0xFFFFFFFF # Start with a full mask, then whitelist as we go if os.name == "nt": # Windows bit_mask -= 0x1 # Shift bit_mask -= 0x2 # Caps Lock # bit_mask -= 0x4 # Ctrl, don't whitelist - used for keybinds bit_mask -= 0x8 # Num Lock bit_mask -= 0x20 # Scroll Lock # bit_mask -= 0x20000 # Alt, don't whitelist - used for File keybinds elif sys.platform == "darwin": # macOS bit_mask -= 0x1 # Shift bit_mask -= 0x2 # Caps Lock bit_mask -= 0x4 # Ctrl # bit_mask -= 0x8 # Cmd, don't whitelist - used for keybinds bit_mask -= 0x10 # Alt bit_mask -= 0x20 # Num Lock - any key pressed on the numpad uses this state else: # Assume Linux at this point bit_mask -= 0x1 # Shift bit_mask -= 0x2 # Caps Lock # bit_mask -= 0x4 # Ctrl, don't whitelist - used for keybinds bit_mask -= 0x8 # Alt bit_mask -= 0x10 # Num Lock bit_mask -= 0x40 # Winkey return bit_mask def quick_search(self, event=None): if event.state & self.mod_bitmask: return # Some disallowed modifier was held - bail # Check if we have a char, or a tab char = getattr(event,"char",None) if not char: if event.keysym=="Tab": char = "\t" else: return # No key we care about was pressed event_time = time.time() # Helper to match key starts, case-insensitively def get_match(nodes,text,full=False): for node in nodes: parent_type = self.get_check_type(self._tree.parent(node)).lower() if parent_type == "array": continue # We can check the name name = self._tree.item(node,"text") if (full and name.lower() == text.lower()) or (not full and name.lower().startswith(text.lower())): # Got a match - return the first return node return None # Found nothing # Get all the visible nodes nodes = self.iter_nodes() # Gather our event info if event_time - self.last_key >= self.last_key_threhsold: # We're beyond our time - reset the search self.key_history = char.strip("\t") # Reset the node self.last_node_result = self._tree.focus() else: # Just append - still within our threshold self.key_history += char.strip("\t") # Ensure we have a node result set self.last_node_result = self.last_node_result or self._tree.focus() # Save the new event_time self.last_key = event_time reverse = False # Check if we just pressed tab this time if char == "\t": if event.state & 0x0001: # Shift+tab, reverse the search reverse = True # We're just tab searching - override the last node result self.last_node_result = self._tree.focus() # Set the key history to the last node's key if we don't have a search result if not self.key_history: parent_type = self.get_check_type(self._tree.parent(self.last_node_result)).lower() if parent_type == "array": return # Bail - can't tab through arrays self.key_history = self._tree.item(self.last_node_result,"text") # Build the search list to focus on the next item before = [] after = [] found = False for n in nodes: if n == self.last_node_result: found = True continue # Skip the current node so we don't try to match it if not found: before.append(n) else: after.append(n) # Set our starting point at the current node's index - omitting the current node search = after+before # Search for the next match m = get_match(search[::-1] if reverse else search,self.key_history) if m: # Got something - select it self.select(m,see=True,alternate=False) def _get_menu_commands(self, menu = None, label = False): if not menu: return [] return [menu.entrycget(i,"label") if label else i for i in range(menu.index(tk.END)+1) if menu.type(i) == "command"] def _ensure_edited(self,edited=True,title=None): if title: # Set the title if we're given one self.title(title) if edited and not self.edited: # Make sure we show as edited self.edited = True if not self.title().endswith(" - Edited"): self.title(self.title()+" - Edited") if sys.platform == "darwin": self.attributes("-modified",1) elif not edited and self.edited: # Undo our edited status self.edited = False if sys.platform == "darwin": self.attributes("-modified",0) def scrollbar_set(self, *args): # Intercepted scrollbar set method to set where our # entry_popup is (if any) self.vsb.set(*args) if not self.entry_popup: return self.entry_popup.relocate() def set_font_size(self): self.font["size"] = self.controller.font_string.get() if self.controller.custom_font.get() else self.controller.default_font["size"] self.style.configure(self.style_name, font=self.font, rowheight=int(math.ceil(self.font.metrics()['linespace']*(1.125 if str(sys.platform)=="darwin" else 1.3)))) def set_font_family(self): if self.controller.font_var.get() == 0: self.font = Font(font="TkTextFont") elif len(self.controller.font_family.get()) > 0: self.font = Font(family=self.controller.font_family.get()) self.style.configure(self.style_name, font=self.font) self.set_font_size() # Necessary because turning off 'custom font' option seems to fallback to font_size of '9'. def window_resize(self, event=None, obj=None): if self.entry_popup: self.entry_popup.relocate() if not event or not obj: return if self.winfo_height() == self.previous_height and self.winfo_width() == self.previous_width: return self.previous_height = self.winfo_height() self.previous_width = self.winfo_width() self.controller.settings["last_window_width"] = self.previous_width self.controller.settings["last_window_height"] = self.previous_height def change_plist_type(self, value): self._ensure_edited() def change_data_type(self, value): self.change_data_display(value) self.last_data = value def change_int_type(self, value): self.change_int_display(value) self.last_int = value def change_bool_type(self, value): self.change_bool_display(value) self.last_bool = value def b_true(self,lower=False): return self.bool_type_string.get().split("/")[0].lower() if lower else self.bool_type_string.get().split("/")[0] def all_b_true(self,lower=False): return [x.split("/")[0].lower() if lower else x.split("/")[0] for x in self.controller.allowed_bool] def b_false(self,lower=False): return self.bool_type_string.get().split("/")[-1].lower() if lower else self.bool_type_string.get().split("/")[-1] def all_b_false(self,lower=False): return [x.split("/")[-1].lower() if lower else x.split("/")[-1] for x in self.controller.allowed_bool] def all_b(self,lower=False): b = [] for x in self.controller.allowed_bool: b.extend([a.lower() if lower else a for a in x.split("/")]) return b def set_find_type_by_index(self, index = None, zero_based = False): if not isinstance(index,(int,long)): # Try to get the keysym try: index = int(getattr(index,"keysym",None).replace("KP_","")) except: return # Borked value if not zero_based: index -= 1 # original index started at 1, normalize to 0-based if index < 0 or index >= len(self.f_options): return # Out of range self.f_title.set(self.f_options[index]) self.change_find_type(self.f_options[index]) return "break" # Prevent the keypress from cascading def change_find_type(self, value): self.find_type = value def cycle_find_type(self, event = None): # Use the value if bool, or try to extract the keysym property increment = event if isinstance(event,bool) else {"Up":False,"Down":True}.get(getattr(event,"keysym",None)) if increment is None: return # Not sure why this was fired? # Set our type to the next in the list value = self.f_title.get() try: curr,end = self.f_options.index(value),len(self.f_options) except: return # Menu is janked? mod = 1 if increment else -1 # Return set_find_type_by_index's return to prevent keypress cascading as needed return self.set_find_type_by_index((curr+mod)%end,zero_based=True) def qualify_value(self, value, value_type): value_type = value_type.lower() if value_type == "data": if self.data_type_string.get().lower() == "hex": value = "".join(value.split()).replace("<","").replace(">","") if value.lower().startswith("0x"): value = value[2:] if [x for x in value.lower() if x not in "0123456789abcdef"]: return (False,"Invalid Hex Data","Invalid character in passed hex data.") if len(value) % 2: return (False,"Invalid Hex Data","Hex data must contain an even number of chars.") value = "<{}>".format(" ".join((value[0+i:8+i] for i in range(0, len(value), 8))).upper()) else: value = value.rstrip("=") if [x for x in value if x.lower() not in "0123456789abcdefghijklmnopqrstuvwxyz+/"]: return (False,"Invalid Base64 Data","Invalid base64 data passed.") if len(value) > 0 and len(value) % 4: value += "=" * (4-len(value)%4) try: test = value if sys.version_info >= (3,0): test = test.encode("utf-8") base64.b64decode(test) except Exception as e: return (False,"Invalid Base64 Data","Invalid base64 data passed.") elif value_type == "date": try: value = datetime.datetime.strptime(value,"%b %d, %Y %I:%M:%S %p").strftime("%b %d, %Y %I:%M:%S %p") except: try: value = datetime.datetime.strptime(value,"%Y-%m-%d %H:%M:%S %z").strftime("%b %d, %Y %I:%M:%S %p") except: return (False,"Invalid Date","Couldn't convert the passed string to a date.\n\nValid formats include:\nMar 11, 2019 12:29:00 PM\nYYYY-MM-DD HH:MM:SS Z") elif value_type == "number": if value.lower().startswith("0x"): try: value = int(value,16) except: return (False,"Invalid Hex Data","Couldn't convert the passed hex string to an integer.") else: value = value.replace(",","") try: value = int(value) except: try: value = float(value) except: return (False,"Invalid Number Data","Couldn't convert to an integer or float.") # Check if we're saving an integer that's out of range if isinstance(value,(int,long)) and not (-1 << 63 <= value < 1 << 63): # Convert it to a float which will force it into scientific notation value = float(value) if self.int_type_string.get().lower() == "hex" and not isinstance(value,float) and value >= 0: value = hex(value).upper()[2:] value = "0x"+value value = str(value) elif value_type == "boolean": if not value.lower() in self.all_b(lower=True): return (False,"Invalid Boolean Data","Booleans can only be {}.".format(", ".join(self.all_b()))) value = self.b_true() if value.lower() in self.all_b_true(lower=True) else self.b_false() elif value_type == "uid": # UIDs are not stored as hex, but we'll allow hex input if value.lower().startswith("0x"): try: value = int(value,16) except: return (False,"Invalid Hex Data", "Couldn't convert the passed hex string to an integer.") else: value = value.replace(",","") try: value = int(value) except: return (False,"Invalid Integer Data","Couldn't convert the passed string to an integer.") if not 0 <= value < 1 << 32: return (False,"Invalid Integer Value","UIDs cannot not be negative, and must be less than 2**32 (4294967296)") value = str(value) return (True,value) def draw_frames(self, event=None, changed=None): self.find_frame.pack_forget() self.display_frame.pack_forget() self._tree_frame.pack_forget() if self.show_find_replace: self.find_frame.pack(side="top",fill="x",padx=10) if self.show_type: self.display_frame.pack(side="bottom",fill="x",padx=10) self._tree_frame.pack(fill="both",expand=True) # Check if we've toggled our find/replace pane and set focus if changed == "hideshow": if self.show_find_replace: self.f_text.focus() self.f_text.select_all() else: self._tree.focus_force() def hide_show_find(self, event=None): # Let's find out if we're set to show self.show_find_replace ^= True self.draw_frames(event,"hideshow") def hide_show_type(self, event=None): # Let's find out if we're set to show self.show_type ^= True self.draw_frames(event,"showtype") def do_replace(self, node, find, new_text): # We can assume that we have a legit match for whatever is passed # Let's get some info first case_sensitive = self.f_case_var.get() node_type = self.get_check_type(node) parent_type = self.get_check_type(self._tree.parent(node)) find_type = self.find_type.lower() if find_type == "key": # We're only replacing the text name = self._tree.item(node,"text") new_name = re.sub(("" if case_sensitive else "(?i)")+re.escape(find), lambda m: new_text, name) self._tree.item(node,text=new_name) return # Check the values values = self.get_padded_values(node,3) if find_type == "string": # Just replacing the text value values[1] = re.sub(("" if case_sensitive else "(?i)")+re.escape(find), lambda m: new_text, values[1]) self._tree.item(node,values=values) elif find_type == "data": # if hex, we need to strip spaces and brackets, upper() both, and compare if self.data_type_string.get().lower() == "hex": find = find.replace(" ","").replace("<","").replace(">","").upper() new_text = new_text.replace(" ","").replace("<","").replace(">","").upper() values[1] = values[1].upper().replace(" ","").replace("<","").replace(">","").upper().replace(find,new_text) values[1] = "<{}>".format(" ".join((values[1][0+i:8+i] for i in range(0, len(values[1]), 8)))) else: # Base64 - let's strip = and compare. Must be case-sensitive, since b64 be like that find = find.rstrip("=") new_text = new_text.rstrip("=") values[1] = values[1].rstrip("=").replace(find,new_text) if len(values[1]) % 4: values[1] += "=" * (4-len(values[1])%4) self._tree.item(node,values=values) else: # Do a straight up replace values[1] = new_text self._tree.item(node,values=values) def replace(self, event=None): find = self.f_text.get() if not len(find): self.bell() mb.showerror("Nothing To Find", "The find textbox is empty, nothing to search for.",parent=self) return None repl = self.r_text.get() # Let's convert both values to the targets find = self.qualify_value(find,self.find_type) repl = self.qualify_value(repl,self.find_type) if find[0] == False: # Invalid find type self.bell() mb.showerror("Invalid Find Value",find[2],parent=self) return if repl[0] == False: # Invalid find type self.bell() mb.showerror("Invalid Replace Value",repl[2],parent=self) return find = find[1] repl = repl[1] if find == repl: # Uh... they're the same - no need to replace bois self.bell() mb.showerror("Find and Replace Are Identical", "The find and replace values are the same. Nothing to do.",parent=self) return # Find out if we're replacing everything or not replace_all = self.r_all_var.get() node = "" if not len(self._tree.selection()) else self._tree.selection()[0] is_match = False if node == "" else self.is_match(node,find) if replace_all: matches = self.find_all(find) if not len(matches): # Nothing found - let's throw an error self.bell() mb.showerror("No Replaceable Matches Found", '"{}" did not match any {} fields in the current plist.'.format(find,self.find_type.lower()),parent=self) return elif not node == "" and not is_match == False: # Current is a match - let's add it matches = [(0,node)] else: # Not matching all, and current cell is not a match, let's get the next node = self.find_next(replacing=True) if node is None: # Nothing found - let's throw an error self.bell() mb.showerror("No Replaceable Matches Found", '"{}" did not match any {} fields in the current plist.'.format(find,self.find_type.lower()),parent=self) return return # At this point, we should have something to replace replacements = [] for x in matches: name = self._tree.item(x[1],"text") values = self._tree.item(x[1],"values") self.do_replace(x[1],find,repl) replacements.append({ "type":"edit", "cell":x[1], "text":name, "values":values }) # Select the last matched cell self.select(matches[-1][1]) self.add_undo(replacements) # Ensure we're edited self._ensure_edited() # Let's try to find the next if not replace_all: self.find_next(replacing=True) def is_match(self, node, text): case_sensitive = self.f_case_var.get() node_type = self.get_check_type(node).lower() parent_type = self.get_check_type(self._tree.parent(node)).lower() find_type = self.find_type.lower() if find_type == "key" and not parent_type == "array": # We can check the name name = self._tree.item(node,"text") if (text in name if case_sensitive else text.lower() in name.lower()): return True # Not found - bail return False # We can check values now value = self.get_padded_values(node,2)[1] if node_type != find_type: # Not what we're looking for return False # Break out and compare if node_type == "data": # if hex, we need to strip spaces and brackets, upper() both, and compare if self.data_type_string.get().lower() == "hex": if text.replace(" ","").replace("<","").replace(">","").upper() in value.replace(" ","").replace("<","").replace(">","").upper(): # Got a match! return True else: # Base64 - let's strip = and compare. Must be case-sensitive, since b64 be like that if text.rstrip("=") in value.rstrip("="): # Yee - is match return True elif node_type == "string": if (text in value if case_sensitive else text.lower() in value.lower()): return True elif node_type in ("date","boolean","number","uid"): if text.lower() == value.lower(): # Can only return if we find the same value return True # If we got here, we didn't find it return False def find_all(self, text=""): # Builds a list of tuples that list the node, the index of the found entry, and # where it found it name/value (name == 0, value == 1 respectively) if text is None or not len(text): return [] nodes = self.iter_nodes(False) found = [] for node in nodes: match = self.is_match(node, text) if not match == False: found.append((nodes.index(node),node)) return found def find_prev(self, event=None): find = self.f_text.get() if not len(find): self.bell() mb.showerror("Nothing To Find", "The find textbox is empty, nothing to search for.",parent=self) return None type_check = self.qualify_value(find, self.find_type) if type_check[0] == False: self.bell() mb.showerror("Invalid Find Value",type_check[2],parent=self) return None matches = self.find_all(type_check[1]) if not len(matches): # Nothing found - let's throw an error self.bell() mb.showerror("No Matches Found", '"{}" did not match any {} fields in the current plist.'.format(type_check[1],self.find_type.lower()),parent=self) return None # Let's get the index of our selected item node = "" if not len(self._tree.selection()) else self._tree.selection()[0] nodes = self.iter_nodes(False) index = len(nodes) if node == "" else nodes.index(node) # Find the item at a lower index than our current selection for match in matches[::-1]: if match[0] < index: # Found one - select it self.select(match[1]) return match # If we got here - start over self.select(matches[-1][1]) return match[-1] def find_next(self, event=None, replacing=False): find = self.f_text.get() if not len(find): self.bell() mb.showerror("Nothing To Find", "The find textbox is empty, nothing to search for.",parent=self) return None type_check = self.qualify_value(find, self.find_type) if type_check[0] == False: self.bell() mb.showerror("Invalid Find Value",type_check[2],parent=self) return None matches = self.find_all(type_check[1]) if not len(matches): # Nothing found - let's throw an error if not replacing: self.bell() mb.showerror("No Matches Found", '"{}" did not match any {} fields in the current plist.'.format(type_check[1],self.find_type.lower()),parent=self) return None # Let's get the index of our selected item node = "" if not len(self._tree.selection()) else self._tree.selection()[0] nodes = self.iter_nodes(False) index = len(nodes) if node == "" else nodes.index(node) # Find the item at a higher index than our current selection for match in matches: if match[0] > index: # Found one - select it self.select(match[1]) return match # If we got here - start over self.select(matches[0][1]) return match[0] def start_editing(self, event = None): # Get the currently selected row, if any node = self.get_root_node() if not len(self._tree.selection()) else self._tree.selection()[0] parent = self._tree.parent(node) parent_type = "dictionary" if not len(parent) else self.get_check_type(parent).lower() check_type = self.get_check_type(node).lower() if node == self.get_root_node(): # Let's see if we're trying to edit the Root node if check_type in ("array","dictionary"): # Nothing to do here - can't edit the Root node's name return 'break' else: # Should at least be able to edit the value - *probably* parent_type = "array" edit_col = "#0" if parent_type == "array": if check_type == "boolean": # Can't edit anything - bail return 'break' # Can at least edit the value edit_col = "#2" # Let's get the bounding box for our other field try: x,y,width,height = self._tree.bbox(node, edit_col) except ValueError: # Couldn't unpack - bail return 'break' # Create an event e = tk.Event e.x = x+5 e.y = y+5 e.x_root = 0 e.y_root = 0 self.on_double_click(e) return 'break' def reload_from_disk(self, event = None): # If we have opened a file, let's reload it from disk # We'll dump the current undo stack, and load it fresh if not self.current_plist: # Nothing to do - ding and bail self.bell() return # At this point - we should check if we have edited the file, and if so # prompt the user if self.edited: self.bell() if not mb.askyesno("Unsaved Changes","Any unsaved changes will be lost when reloading from disk. Continue?",parent=self): return # If we got here - we're okay with dumping changes (if any) try: with open(self.current_plist,"rb") as f: plist_data = plist.load(f,dict_type=dict if self.controller.settings.get("sort_dict",False) else OrderedDict) except Exception as e: # Had an issue, throw up a display box self.bell() mb.showerror("An Error Occurred While Opening {}".format(os.path.basename(self.current_plist)), repr(e),parent=self) return # We should have the plist data now self.open_plist(self.current_plist,plist_data,self.plist_type_string.get()) def get_min_max_from_match(self, match_text): # Helper method to take MatchKernel output and break it into the MinKernel and MaxKernel temp_min = "0.0.0" temp_max = "99.99.99" match_text = "" if match_text == "1" else match_text # Strip out "1" in prefix matching to match any if match_text != "": try: min_list = match_text.split(".") max_list = [x for x in min_list] min_list += ["0"] * (3-len(min_list)) # pad it out with 0s for min min_list = [x if len(x) else "0" for x in min_list] # Ensure all blanks are 0s too max_list += ["99"] * (3-len(max_list)) # pad it with 99s for max max_list = [x if len(x) else "99" for x in max_list] # Ensure all blanks are 0s too temp_min = ".".join(min_list) temp_max = ".".join(max_list) except: pass # Broken formatting - it seems return (temp_min,temp_max) def get_min_max_from_kext(self, kext, use_match = False): # Helper to get the min/max kernel versions if use_match: return self.get_min_max_from_match(kext.get("MatchKernel","")) temp_min = kext.get("MinKernel","0.0.0") temp_max = kext.get("MaxKernel","99.99.99") temp_min = "0.0.0" if temp_min == "" else temp_min temp_max = "99.99.99" if temp_max == "" else temp_max return (temp_min,temp_max) def oc_clean_snapshot(self, event = None): self.oc_snapshot(event,True) def check_path_length(self, item): paths_too_long = [] if isinstance(item,dict): # Get the last path component of the Path or BundlePath values for the name name = os.path.basename(item.get("Path",item.get("BundlePath","Unknown Name"))) # Check the keys containing "path" for key in item: if "path" in key.lower() and isinstance(item[key],(str,unicode)) and len(item[key])>self.safe_path_length: paths_too_long.append(key) # Too long - keep a reference of the key elif isinstance(item,(str,unicode)): name = os.path.basename(item) # Retain the last path component as the name # Checking the item itself if len(item)>self.safe_path_length: paths_too_long.append(item) else: return paths_too_long # Empty list if not paths_too_long: return [] # Return an empty array to allow .extend() return [(item,name,paths_too_long)] # Return a list containing a tuple of the original item, and which paths are too long def get_hash(self,path,block_size=65536): if not os.path.exists(path): return "" hasher = hashlib.md5() try: with open(path,"rb") as f: while True: buffer = f.read(block_size) if not buffer: break hasher.update(buffer) return hasher.hexdigest() except: pass return "" # Couldn't determine hash :( def oc_snapshot(self, event = None, clean = False): target_dir = os.path.dirname(self.current_plist) if self.current_plist and os.path.exists(os.path.dirname(self.current_plist)) else None oc_folder = fd.askdirectory(title="Select OC Folder:",initialdir=target_dir) self.controller.lift_window(self) # Lift the window to continue catching events if not len(oc_folder): return # Verify folder structure - should be as follows: # OC # +- ACPI # | +- SSDT.aml # +- Drivers # | +- EfiDriver.efi # +- Kexts # | +- Something.kext # +- config.plist # +- Tools (Optional) # | +- SomeTool.efi # | +- SomeFolder # | | +- SomeOtherTool.efi oc_acpi = os.path.normpath(os.path.join(oc_folder,"ACPI")) oc_drivers = os.path.normpath(os.path.join(oc_folder,"Drivers")) oc_kexts = os.path.normpath(os.path.join(oc_folder,"Kexts")) oc_tools = os.path.normpath(os.path.join(oc_folder,"Tools")) oc_efi = os.path.normpath(os.path.join(oc_folder,"OpenCore.efi")) for x in (oc_acpi,oc_drivers,oc_kexts): if not os.path.exists(x): self.bell() mb.showerror("Incorrect OC Folder Structure", "{} does not exist.\nPlease make sure you're selecting a valid OC folder.".format(x), parent=self) return if x != oc_efi and not os.path.isdir(x): self.bell() mb.showerror("Incorrect OC Folder Structure", "{} exists, but is not a directory.\nPlease make sure you're selecting a valid OC folder.".format(x), parent=self) return # Folders are valid - lets work through each section # Let's get the hash of OpenCore.efi, compare to a known list, and then compare that version to our snapshot_version if found oc_hash = self.get_hash(oc_efi) # Let's get the version of the snapshot that matches our target, and that matches our hash if any latest_snap = {} # Highest min_version target_snap = {} # Matches our hash select_snap = {} # Whatever the user selected user_snap = self.controller.settings.get("snapshot_version","Auto-detect") for snap in self.controller.snapshot_data: hashes = snap.get("release_hashes",[]) hashes.extend(snap.get("debug_hashes",[])) # Retain the highest version we see if snap.get("min_version","0.0.0") > latest_snap.get("min_version","0.0.0"): latest_snap = snap # If we want the latest, retain the select_snap as well if user_snap.lower() == "latest": select_snap = snap # Also retain the last snap that matches our hash if len(oc_hash) and (oc_hash in snap.get("release_hashes",[]) or oc_hash in snap.get("debug_hashes",[])): target_snap = snap # If we're auto-detecting, retain the select_snap as well if user_snap.lower() == "auto-detect": select_snap = snap # Save the snap that matches the user's choice too if not Latest or Auto-detect if user_snap.lower() not in ("auto-detect","latest") and user_snap >= snap.get("min_version","0.0.0") and snap.get("min_version","0.0.0") > select_snap.get("min_version","0.0.0"): select_snap = snap # Make sure we have a value for select_snap - either its own, or the latest select_snap = select_snap or latest_snap if target_snap and target_snap != select_snap: # Version mismatch - warn tar_min,tar_max = target_snap.get("min_version","0.0.0"),target_snap.get("max_version","Current") sel_min,sel_max = select_snap.get("min_version","0.0.0"),select_snap.get("max_version","Current") found_ver = tar_min if tar_min==tar_max else "{} -> {}".format(tar_min,tar_max) select_ver = sel_min if sel_min==sel_max else "{} -> {}".format(sel_min,sel_max) if mb.askyesno("Snapshot Version Mismatch","Detected OpenCore.efi version: {}\n\nOC Snapshot target version: {}{}\n\nWould you like to snapshot for the detected OpenCore.efi version instead?".format( found_ver, select_ver, " (Latest)" if user_snap.lower() == "latest" else "" ),parent=self): # We want to change for this snapshot select_snap = target_snap # Apply our snapshot values acpi_add = select_snap.get("acpi_add",{}) kext_add = select_snap.get("kext_add",{}) tool_add = select_snap.get("tool_add",{}) driver_add = select_snap.get("driver_add",{}) long_paths = [] # We'll add any paths that exceed the OC_STORAGE_SAFE_PATH_MAX of 128 chars # ACPI is first, we'll iterate the .aml files we have and add what is missing # while also removing what exists in the plist and not in the folder. # If something exists in the table already, we won't touch it. This leaves the # enabled and comment properties untouched. # # Let's make sure we have the ACPI -> Add sections in our config tree_dict = self.nodes_to_values(binary=False) # We're going to replace the whole list if not "ACPI" in tree_dict or not isinstance(tree_dict["ACPI"],dict): tree_dict["ACPI"] = {"Add":[]} if not "Add" in tree_dict["ACPI"] or not isinstance(tree_dict["ACPI"]["Add"],list): tree_dict["ACPI"]["Add"] = [] # Now we walk the existing add values new_acpi = [] for path, subdirs, files in os.walk(oc_acpi): for name in files: if not name.startswith(".") and name.lower().endswith(".aml"): new_acpi.append(os.path.join(path,name)[len(oc_acpi):].replace("\\", "/").lstrip("/")) add = [] if clean else tree_dict["ACPI"]["Add"] for aml in sorted(new_acpi,key=lambda x:x.lower()): if aml.lower() in [x.get("Path","").lower() for x in add if isinstance(x,dict)]: # Found it - skip continue # Doesn't exist, add it new_aml_entry = { "Comment":os.path.basename(aml), "Enabled":True, "Path":aml } # Add our snapshot custom entries, if any for x in acpi_add: new_aml_entry[x] = acpi_add[x] add.append(OrderedDict(sorted(new_aml_entry.items(),key=lambda x: str(x[0]).lower()))) new_add = [] for aml in add: if not isinstance(aml,dict): # Not the right type - skip it continue if not aml.get("Path","").lower() in [x.lower() for x in new_acpi]: # Not there, skip continue new_add.append(aml) # Check path length long_paths.extend(self.check_path_length(aml)) tree_dict["ACPI"]["Add"] = new_add # Now we need to walk the kexts if not "Kernel" in tree_dict or not isinstance(tree_dict["Kernel"],dict): tree_dict["Kernel"] = {"Add":[]} if not "Add" in tree_dict["Kernel"] or not isinstance(tree_dict["Kernel"]["Add"],list): tree_dict["Kernel"]["Add"] = [] kext_list = [] # We need to gather a list of all the files inside that and with .efi for path, subdirs, files in os.walk(oc_kexts): for name in sorted(subdirs, key=lambda x:x.lower()): if name.startswith(".") or not name.lower().endswith(".kext"): continue kdict = { # "Arch":"Any", "BundlePath":os.path.join(path,name)[len(oc_kexts):].replace("\\", "/").lstrip("/"), "Comment":name, "Enabled":True, # "MaxKernel":"", # "MinKernel":"", "ExecutablePath":"" } # Add our entries from kext_add as needed for y in kext_add: kdict[y] = kext_add[y] # Get the Info.plist plist_full_path = plist_rel_path = None for kpath, ksubdirs, kfiles in os.walk(os.path.join(path,name)): for kname in kfiles: if kname.lower() == "info.plist": plist_full_path = os.path.join(kpath,kname) plist_rel_path = plist_full_path[len(os.path.join(path,name)):].replace("\\", "/").lstrip("/") break if plist_full_path: break # Found it - break else: # Didn't find it - skip continue kdict["PlistPath"] = plist_rel_path # Let's load the plist and check for other info try: with open(plist_full_path,"rb") as f: info_plist = plist.load(f) if not "CFBundleIdentifier" in info_plist or not isinstance(info_plist["CFBundleIdentifier"],(str,unicode)): continue # Requires a valid CFBundleIdentifier string kinfo = { "CFBundleIdentifier": info_plist["CFBundleIdentifier"], "OSBundleLibraries": info_plist.get("OSBundleLibraries",[]), "cfbi": info_plist["CFBundleIdentifier"].lower(), # Case insensitive "osbl": [x.lower() for x in info_plist.get("OSBundleLibraries",[]) if isinstance(x,(str,unicode))] # Case insensitive } if info_plist.get("CFBundleExecutable",None): if not os.path.exists(os.path.join(path,name,"Contents","MacOS",info_plist["CFBundleExecutable"])): continue # Requires an executable that doesn't exist - bail kdict["ExecutablePath"] = "Contents/MacOS/"+info_plist["CFBundleExecutable"] except Exception as e: continue # Something else broke here - bail # Should have something valid here kext_list.append((OrderedDict(sorted(kdict.items(),key=lambda x: str(x[0]).lower())),kinfo)) bundle_list = [x[0].get("BundlePath","") for x in kext_list] kexts = [] if clean else tree_dict["Kernel"]["Add"] original_kexts = [x for x in kexts if isinstance(x,dict) and x.get("BundlePath","") in bundle_list] # get the original load order for comparison purposes - but omit any that no longer exist for kext,info in kext_list: if kext["BundlePath"].lower() in [x.get("BundlePath","").lower() for x in kexts if isinstance(x,dict)]: # Already have it, skip continue # We need it, it seems kexts.append(kext) new_kexts = [] for kext in kexts: if not isinstance(kext,dict) or not kext.get("BundlePath"): # Not a dict, or missing BundlePath - skip it continue # Get our first match based on BundlePath which should be unique kext_match = next((k for k,i in kext_list if k["BundlePath"].lower() == kext["BundlePath"].lower()),None) if kext_match is None: # Not there, skip it continue # Make sure the ExecutablePath and PlistPath are updated if different for check in ("ExecutablePath","PlistPath"): if kext.get(check,"") != kext_match.get(check,""): kext[check] = kext_match.get(check,"") new_kexts.append(kext) # Let's check inheritance via the info # We need to ensure that no 2 kexts consider each other as parents unordered_kexts = [] for x in new_kexts: x = next((y for y in kext_list if y[0].get("BundlePath","") == x.get("BundlePath","")),None) if not x: continue parents = [next(((z,y[1]) for z in new_kexts if z.get("BundlePath","") == y[0].get("BundlePath","")),[]) for y in kext_list if y[1].get("cfbi",None) in x[1].get("osbl",[])] children = [next((z for z in new_kexts if z.get("BundlePath","") == y[0].get("BundlePath","")),[]) for y in kext_list if x[1].get("cfbi",None) in y[1].get("osbl",[])] parents = [y for y in parents if not y[0] in children and not y[0].get("BundlePath","") == x[0].get("BundlePath","")] unordered_kexts.append({ "kext":x[0], "parents":parents }) ordered_kexts = [] disabled_parents = [] while len(unordered_kexts): # This could be dangerous if things aren't properly prepared above kext = unordered_kexts.pop(0) if len(kext["parents"]): # Gather a list of enabled/disabled parents - and ensure we properly populate # our disabled_parents list enabled_parents = [x[1].get("cfbi") for x in kext["parents"] if x[0].get("Enabled")] disabled_add = [x for x in kext["parents"] if x[0].get("Enabled") == False and not x[1].get("cfbi") in enabled_parents and not any((x[1].get("cfbi")==y[1].get("cfbi") for y in disabled_parents))] # Get any existing kext we're referencing k = next((x for x in original_kexts if x.get("BundlePath")==kext["kext"].get("BundlePath")),None) if not k or k.get("Enabled"): for p in kext["parents"]: p_cf = p[1].get("cfbi") if not p_cf: continue # Broken - can't check if p_cf in enabled_parents: continue # Already have an enabled copy if any((p_cf == x[1].get("cfbi") for x in disabled_parents)): continue # Already have a warning copy disabled_parents.append(p) if not all(x[0] in ordered_kexts for x in kext["parents"]): unordered_kexts.append(kext) continue ordered_kexts.append(next(x for x in new_kexts if x.get("BundlePath","") == kext["kext"].get("BundlePath",""))) # Let's compare against the original load order - to prevent mis-prompting missing_kexts = [x for x in ordered_kexts if not x in original_kexts] original_kexts.extend(missing_kexts) # Let's walk both lists and gather all kexts that are in different spots rearranged = [] while True: check1 = [x.get("BundlePath","") for x in ordered_kexts if not x.get("BundlePath","") in rearranged] check2 = [x.get("BundlePath","") for x in original_kexts if not x.get("BundlePath","") in rearranged] out_of_place = next((x for x in range(len(check1)) if check1[x] != check2[x]),None) if out_of_place is None: break rearranged.append(check2[out_of_place]) # Verify if the load order changed - and prompt the user if need be if len(rearranged): if not mb.askyesno("Incorrect Kext Load Order","Correct the following kext load inheritance issues?\n\n{}".format("\n".join(rearranged)),parent=self): ordered_kexts = original_kexts # We didn't want to update it if len(disabled_parents): if mb.askyesno("Disabled Parent Kexts","Enable the following disabled parent kexts?\n\n{}".format("\n".join([x[0].get("BundlePath","") for x in disabled_parents])),parent=self): for x in ordered_kexts: # Walk our kexts and enable the parents if any((x.get("BundlePath","") == y[0].get("BundlePath","") for y in disabled_parents)): x["Enabled"] = True # Finally - we walk the kexts and ensure that we're not loading the same CFBundleIdentifier more than once enabled_kexts = [] duplicate_bundles = [] duplicates_disabled = [] for kext in ordered_kexts: # Check path length long_paths.extend(self.check_path_length(kext)) temp_kext = OrderedDict() if isinstance(kext,OrderedDict) else {} # Shallow copy the kext entry to avoid changing it in ordered_kexts for x in kext: temp_kext[x] = kext[x] duplicates_disabled.append(temp_kext) # Ignore if alreday disabled if not temp_kext.get("Enabled",False): continue # Get the original info info = next((x for x in kext_list if x[0].get("BundlePath","") == temp_kext.get("BundlePath","")),None) if not info or not info[1].get("cfbi",None): continue # Broken info # Let's see if it's already in enabled_kexts - and compare the Min/Max/Match Kernel options temp_min,temp_max = self.get_min_max_from_kext(temp_kext,"MatchKernel" in kext_add) # Gather a list of like IDs comp_kexts = [x for x in enabled_kexts if x[1]["cfbi"] == info[1]["cfbi"]] # Walk the comp_kexts, and disable if we find an overlap for comp_info in comp_kexts: comp_kext = comp_info[0] # Gather our min/max comp_min,comp_max = self.get_min_max_from_kext(comp_kext,"MatchKernel" in kext_add) # Let's see if we don't overlap if temp_min > comp_max or temp_max < comp_min: # We're good, continue continue # We overlapped - let's disable it temp_kext["Enabled"] = False # Add it to the list - then break out of this loop duplicate_bundles.append(temp_kext.get("BundlePath","")) break # Check if we ended up disabling temp_kext, and if not - add it to the enabled_kexts list if temp_kext.get("Enabled",False): enabled_kexts.append((temp_kext,info[1])) # Check if we have duplicates - and offer to disable them if len(duplicate_bundles): if mb.askyesno("Duplicate CFBundleIdentifiers","Disable the following kexts with duplicate CFBundleIdentifiers?\n\n{}".format("\n".join(duplicate_bundles)),parent=self): ordered_kexts = duplicates_disabled tree_dict["Kernel"]["Add"] = ordered_kexts # Let's walk the Tools folder if it exists if not "Misc" in tree_dict or not isinstance(tree_dict["Misc"],dict): tree_dict["Misc"] = {"Tools":[]} if not "Tools" in tree_dict["Misc"] or not isinstance(tree_dict["Misc"]["Tools"],list): tree_dict["Misc"]["Tools"] = [] if os.path.exists(oc_tools) and os.path.isdir(oc_tools): tools_list = [] # We need to gather a list of all the files inside that and with .efi for path, subdirs, files in os.walk(oc_tools): for name in files: if not name.startswith(".") and name.lower().endswith(".efi"): # Save it new_tool_entry = { # "Arguments":"", # "Auxiliary":True, "Name":name, "Comment":name, "Enabled":True, "Path":os.path.join(path,name)[len(oc_tools):].replace("\\", "/").lstrip("/") # Strip the /Volumes/EFI/ } # Add our snapshot custom entries, if any for x in tool_add: new_tool_entry[x] = tool_add[x] tools_list.append(OrderedDict(sorted(new_tool_entry.items(),key=lambda x:str(x[0]).lower()))) tools = [] if clean else tree_dict["Misc"]["Tools"] for tool in sorted(tools_list, key=lambda x: x.get("Path","").lower()): if tool["Path"].lower() in [x.get("Path","").lower() for x in tools if isinstance(x,dict)]: # Already have it, skip continue # We need it, it seems tools.append(tool) new_tools = [] for tool in tools: if not isinstance(tool,dict): # Not a dict - skip it continue if not tool.get("Path","").lower() in [x["Path"].lower() for x in tools_list]: # Not there, skip it continue new_tools.append(tool) # Check path length long_paths.extend(self.check_path_length(tool)) tree_dict["Misc"]["Tools"] = new_tools else: # Make sure our Tools list is empty tree_dict["Misc"]["Tools"] = [] # Last we need to walk the .efi drivers if not "UEFI" in tree_dict or not isinstance(tree_dict["UEFI"],dict): tree_dict["UEFI"] = {"Drivers":[]} if not "Drivers" in tree_dict["UEFI"] or not isinstance(tree_dict["UEFI"]["Drivers"],list): tree_dict["UEFI"]["Drivers"] = [] if os.path.exists(oc_drivers) and os.path.isdir(oc_drivers): drivers_list = [] # We need to gather a list of all the files inside that and with .efi for path, subdirs, files in os.walk(oc_drivers): for name in files: if not name.startswith(".") and name.lower().endswith(".efi"): # Check if we're using the new approach - or just listing the paths if not driver_add: drivers_list.append(os.path.join(path,name)[len(oc_drivers):].replace("\\", "/").lstrip("/")) # Strip the /Volumes/EFI/ else: new_driver_entry = { # "Arguments": "", "Enabled":True, "Path":os.path.join(path,name)[len(oc_drivers):].replace("\\", "/").lstrip("/") # Strip the /Volumes/EFI/ } # Add our snapshot custom entries, if any - include the name of the .efi driver if the Comment for x in driver_add: new_driver_entry[x] = name if x.lower() == "comment" else driver_add[x] drivers_list.append(OrderedDict(sorted(new_driver_entry.items(),key=lambda x:str(x[0]).lower()))) drivers = [] if clean else tree_dict["UEFI"]["Drivers"] for driver in sorted(drivers_list, key=lambda x: x.get("Path","").lower() if driver_add else x): if not driver_add: # Old way if not isinstance(driver,(str,unicode)) or driver.lower() in [x.lower() for x in drivers if isinstance(x,(str,unicode))]: continue else: if driver["Path"].lower() in [x.get("Path","").lower() for x in drivers if isinstance(x,dict)]: # Already have it, skip continue # We need it, it seems drivers.append(driver) new_drivers = [] for driver in drivers: if not driver_add: # Old way if not isinstance(driver,(str,unicode)) or not driver.lower() in [x.lower() for x in drivers_list if isinstance(x,(str,unicode))]: continue else: if not isinstance(driver,dict): # Not a dict - skip it continue if not driver.get("Path","").lower() in [x["Path"].lower() for x in drivers_list]: # Not there, skip it continue new_drivers.append(driver) # Check path length long_paths.extend(self.check_path_length(driver)) tree_dict["UEFI"]["Drivers"] = new_drivers else: # Make sure our Drivers list is empty tree_dict["UEFI"]["Drivers"] = [] # Check if we're forcing schema - and ensure values line up if self.controller.settings.get("force_snapshot_schema",False): ignored = ["Comment","Enabled","Path","BundlePath","ExecutablePath","PlistPath","Name"] for entries,values in ((tree_dict["ACPI"]["Add"],acpi_add),(tree_dict["Kernel"]["Add"],kext_add),(tree_dict["Misc"]["Tools"],tool_add),(tree_dict["UEFI"]["Drivers"],driver_add)): if not values: continue # Skip if nothing to check for entry in entries: to_remove = [x for x in entry if not x in values and not x in ignored] to_add = [x for x in values if not x in entry] for add in to_add: entry[add] = os.path.basename(entry.get("Path",values[add])) if add.lower() == "comment" else values[add] for rem in to_remove: entry.pop(rem,None) # Now we remove the original tree - then replace it undo_list = [] for x in self._tree.get_children(): undo_list.append({ "type":"remove", "cell":x, "from":self._tree.parent(x), "index":self._tree.index(x) }) self._tree.detach(x) # Finally, we add the nodes back self.add_node(tree_dict) # Add all the root items to our undo list for child in self._tree.get_children(): undo_list.append({ "type":"add", "cell":child }) self.add_undo(undo_list) # Select the root element self.select(self.get_root_node()) # Ensure we're edited self._ensure_edited() self.update_all_children() self.alternate_colors() # Check if we have any paths that are too long if long_paths: formatted = [] for entry in long_paths: item,name,keys = entry if isinstance(item,(str,unicode)): # It's an older string path formatted.append(name) elif isinstance(item,dict): formatted.append("{} -> {}".format(name,", ".join(keys))) # Show the dialog warning of lengthy paths mb.showwarning( "Potentially Unsafe Paths".format(self.safe_path_length), "The following exceed the {:,} character safe path max declared by OpenCore and may not work as intended:\n\n{}".format( self.safe_path_length, "\n".join(formatted) ), parent=self ) def get_check_type(self, cell=None, string=None): if not cell is None: t = self.get_padded_values(cell,1)[0] elif not string is None: t = string else: return None if t.startswith(self.menu_code): t = t.replace(self.menu_code+" ","") t = t.replace(self.menu_code,"") return t def clicked(self, event = None): # Reset every click self.clicked_drag = False if not event: return rowid = self._tree.identify_row(event.y) if rowid and self._tree.bbox(rowid): # Mouse down in a valid node self.clicked_drag = True def _change_display(self,new_display,target_funct): # Walks the nodes, undo, and redo stacks and runs # the target funct accordingly target_funct(self.iter_nodes(False),new_display) for u in self.undo_stack: target_funct(u,new_display) for r in self.redo_stack: target_funct(r,new_display) def _qualify_node(self,node,type_check): # Helper function to qualify a passed node - or check if it's # an undo/redo entry. Returns (None,None,None) if it doesn't qualify, # or a tuple of (node,task,values) task = None if isinstance(node,dict): # Should be an undo/redo action if node.get("type") in ("move","add"): return (None,None,None) # Handled by the nodes loop task = node node = task.get("cell") if not node: return (None,None,None) # Broken formatting? if task and task.get("type") == "edit": values = list(task.get("values",[])) if len(values) < 2: return (None,None,None) # Broken :( else: values = self.get_padded_values(node,3) t = self.get_check_type(cell=None,string=values[0]).lower() if t != type_check: return (None,None,None) return (node,task,values) def change_int_display(self,new_display="Decimal"): if new_display == self.last_int: return self.int_type_string.set(new_display[0].upper()+new_display[1:]) self._change_display(new_display,self._change_int_display) def _change_int_display(self,node_list,new_display="Decimal"): if not isinstance(node_list,(list,tuple)): node_list = [node_list] for node in node_list: node,task,values = self._qualify_node(node,"number") if task == False or values is None: continue # Didn't qualify value = values[1] if new_display.lower() == "hex": try: value = int(value) if value >= 0: value = "0x"+hex(value).upper()[2:] except: pass else: if value.lower().startswith("0x"): value = str(int(value,16)) values[1] = value if task and task.get("type") == "edit": task["values"] = values else: self._tree.item(node,values=values) def change_bool_display(self,new_display="True/False"): if new_display == self.last_bool: return self.bool_type_string.set(new_display) # Walk all nodes, then walk the undo/redo stack to ensure # all bool values are updated. self._change_display(new_display,self._change_bool_display) def _change_bool_display(self,node_list,new_display="True/False"): if not isinstance(node_list,(list,tuple)): node_list = [node_list] on,off = new_display.split("/") on_list = [x.split("/")[0] for x in self.controller.allowed_bool] for node in node_list: node,task,values = self._qualify_node(node,"boolean") if task == False or values is None: continue # Didn't qualify values[1] = on if values[1] in on_list else off if task and task.get("type") == "edit": task["values"] = values else: self._tree.item(node,values=values) def change_data_display(self,new_display="Hex"): if new_display == self.last_data: return self.data_type_string.set(new_display[0].upper()+new_display[1:]) # This will change how data is displayed - we do this by converting all our existing # data values to bytes, then reconverting and displaying appropriately self._change_display(new_display,self._change_data_display) def _change_data_display(self,node_list,new_display="Hex"): if not isinstance(node_list,(list,tuple)): node_list = [node_list] for node in node_list: node,task,values = self._qualify_node(node,"data") if values is None: continue # Didn't qualify value = values[1] # We need to adjust how it is displayed, load the bytes first if new_display.lower() == "hex": # Convert to hex if sys.version_info < (3,0): value = binascii.hexlify(base64.b64decode(value)) else: value = binascii.hexlify(base64.b64decode(value.encode("utf-8"))).decode("utf-8") # format the hex value = "<{}>".format(" ".join((value[0+i:8+i] for i in range(0, len(value), 8))).upper()) else: # Assume base64 if sys.version_info < (3, 0): value = base64.b64encode(binascii.unhexlify(value.replace("<","").replace(">","").replace(" ",""))) else: value = base64.b64encode(binascii.unhexlify(value.replace("<","").replace(">","").replace(" ","").encode("utf-8"))).decode("utf-8") values[1] = value if task and task.get("type") == "edit": task["values"] = values else: self._tree.item(node,values=values) def add_undo(self, action): if not isinstance(action,list): action = [action] try: # Get the max undo items max_undo = int(self.controller.undo_max_text.get()) assert max_undo >= 0 except: max_undo = self.controller.max_undo self.undo_stack.append(action) # See if we need to limit the undo_stack if max_undo > 0: # Pop the first item until we're at/under max while len(self.undo_stack) > max_undo: self.undo_stack.pop(0) self.redo_stack = [] # clear the redo stack def reundo(self, event=None, undo = True, single_undo = None): # Let's come up with a more centralized way to do this # We'll break down the potential actions into a few types: # # add, remove, edit # # edited: {"type":"edit","cell":cell_edited,"text":text_value,"values":values_list} # added: {"type":"add","cell":cell_added} # removed: {"type":"remove","cell":cell_removed,"from":cell_removed_from,"index":index_in_parent_children} # moved: {"type":"move","cell":cell_moved,"from":old_parent,"to":new_parent,"index":index_in_old_parent} # # All actions are lists of individual changes, in the order they happened. # If a cell was changed from a Dict to a String, we would have a removed entry # for each child under that cell, then an edit entry for the cell itself. # if undo: u = self.undo_stack r = self.redo_stack else: r = self.undo_stack u = self.redo_stack # Allow a single undo without any trace if single_undo != None: u = [single_undo] r = [] if not len(u): self.bell() # Nothing to undo/redo return # Check if we have any open EntryPopups and cancel them if self.entry_popup: self.entry_popup.cancel() # Retain the original selection selected,nodes = self.preselect() task_list = u.pop(-1) r_task_list = [] # Iterate in reverse to undo the last thing first for task in task_list[::-1]: cell = task["cell"] ttype = task["type"].lower() if ttype == "edit": # We changed something in the cell, build a snapshot of the current r_task_list.append({ "type":"edit", "cell":cell, "text":self._tree.item(cell,"text"), "values":self._tree.item(cell,"values") }) # Now we undo our edit self._tree.item(cell,text=task["text"],values=task["values"]) elif ttype == "add": # We added new things - let's create a removal list r_task_list.append({ "type":"remove", "cell":cell, "from":self._tree.parent(cell), "index":self._tree.index(cell) }) # Now we actually remove it self._tree.detach(cell) elif ttype == "remove": # We removed this cell, let's attach it to its old parent r_task_list.append({ "type":"add", "cell":cell, }) # Now we actually add it self._tree.move(cell,task["from"],task.get("index","end")) selected = cell elif ttype == "move": # We moved a cell - let's save the old info r_task_list.append({ "type":"move", "cell":cell, "from":self._tree.parent(cell), "to":task["from"], "index":self._tree.index(cell) }) # Let's actually move it now self._tree.move(cell,task["from"],task.get("index","end")) # Let's check if we have an r_task_list - and add it if it wasn't a one-off if len(r_task_list) and single_undo is None: r.append(r_task_list) # Ensure we're edited self._ensure_edited() # Change selection if needed self.update_all_children() self.reselect((selected,nodes)) def preselect(self): # Returns a tuple of the selected item and current visible nodes return (self._tree.focus(),self.iter_nodes()) def reselect(self, selection_tuple = None): # Select the node or index that was last selected if not selection_tuple or not isinstance(selection_tuple,tuple): # Just select the root return self.select(self.get_root_node()) selected,original_nodes = selection_tuple nodes = self.iter_nodes() if selected in nodes: return self.select(selected) # Our item no longer exists, let's adjust our selection index = original_nodes.index(selected) self.select(nodes[index] if index < len(nodes) else nodes[-1]) def got_focus(self, event=None): # Lift us to the top of the stack order # only when the window specifically gained focus if event and event.widget == self: self.lift() def move_selection(self, event): # Check if we have a entry_popup - and relocate it if self.entry_popup: self.entry_popup.relocate() # Verify we had clicked in the drag column if not self.clicked_drag: # Nope, ignore return target = self.get_root_node() if not len(self._tree.selection()) else self._tree.selection()[0] if target == self.get_root_node(): return # Nothing to do here as we can't drag it if self.drag_start is None: # Let's set the drag start globals self.drag_start = (event.x, event.y) self.drag_undo = None return # Find how far we've drug so far if not self.dragging: x, y = self.drag_start drag_distance = math.sqrt((event.x - x)**2 + (event.y - y)**2) if drag_distance < self.controller.drag_scale.get(): # Not drug enough return # We've passed the threshold, start dragging self.dragging = True # Save a reference to the item if not self.drag_undo: self.drag_undo = {"from":self._tree.parent(target),"index":self._tree.index(target),"name":self._tree.item(target,"text")} # Make sure if we drag to the bottom, it stays at the bottom if self._tree.identify_region(event.x, event.y) == "nothing" and not event.y < 5: move_to = len(self.iter_nodes()) else: move_to = self._tree.index(self._tree.identify_row(event.y)) tv_item = self._tree.identify('item', event.x, event.y) tv_item = tv_item or self.get_root_node() # Force Root node as needed # Check if it's not a collection - or if it is, if it has children and is closed, # then keep it a sibling. if not self.get_check_type(tv_item).lower() in ("dictionary","array") \ or (len(self._tree.get_children(tv_item)) and not self._tree.item(tv_item,"open")): # Keep it a sibling if not tv_item == self.get_root_node(): tv_item = self._tree.parent(tv_item) # Let's get the bounding box for the target, and if we're in the lower half, # we'll add as a child, uper half will add as a sibling else: rowid = self._tree.identify_row(event.y) column = self._tree.identify_column(event.x) try: x,y,width,height = self._tree.bbox(rowid, column) except: # We drug outside the possible bounds if move_to == len(self.iter_nodes()): # Drug down self._tree.move(target,self.get_root_node(),move_to) return if y+(height/2)<=event.y tags inline if not isinstance(plist_text,(list,tuple)): # Split it if it's not already a list plist_text = plist_text.split("\n") new_plist = [] data_tag = "" types = ("","") for i,x in enumerate(plist_text): x_stripped = x.strip() try: type_check = types[types.index(x_stripped)].replace("","/>") data_tag = "" continue except (ValueError, IndexError) as e: pass if x_stripped == "": data_tag = x continue if not len(data_tag): # Not primed, and wasn't new_plist.append(x) continue data_tag += x_stripped # Check for the end if x_stripped == "": # Found the end, append it and reset new_plist.append(data_tag) data_tag = "" return "\n".join(new_plist) def save_plist_as(self, event=None, path=None): if path is None: # Get the file dialog path = fd.asksaveasfilename( title="Please select a file name for saving:", defaultextension=".plist", initialfile=self.get_title(), initialdir=self.get_dir() ) if not len(path): # User cancelled - no changes return None # Check if it should be binary binary = self.plist_type_string.get().lower() == "binary" # Should have the save path plist_data = self.nodes_to_values(binary=binary) # Create a temp folder and save there first temp = tempfile.mkdtemp() temp_file = os.path.join(temp, os.path.basename(path)) try: if binary: with open(temp_file,"wb") as f: plist.dump(plist_data,f,sort_keys=self.controller.settings.get("sort_dict",False),fmt=plist.FMT_BINARY) elif not self.controller.settings.get("xcode_data",True): with open(temp_file,"wb") as f: plist.dump(plist_data,f,sort_keys=self.controller.settings.get("sort_dict",False)) else: # Dump to a string first plist_text = self._format_data_string(plist.dumps(plist_data,sort_keys=self.controller.settings.get("sort_dict",False))) # At this point, we have a list of lines - with all tags on the same line # let's write to file with open(temp_file,"wb") as f: if sys.version_info >= (3,0): plist_text = plist_text.encode("utf-8") f.write(plist_text) if os.path.isfile(path): # We are overwriting an existing file - try to clone the perms and ownership try: shutil.copystat(path,temp_file) except: pass # Update the accessed and modified times update_timestamp = time.time() try: os.utime(temp_file,(update_timestamp,update_timestamp)) except: pass # Copy the temp over shutil.copy(temp_file,path) # Let's ensure the md5 of the temp file and saved file are the same # There have been some reports of file issues when saving directly to an ESP temp_hash = self.get_hash(temp_file) save_hash = self.get_hash(path) if not temp_hash == save_hash: # Some issue occurred - let's throw an exception self.bell() mb.showerror("Saved MD5 Hash Mismatch","The saved and temp file hashes do not match - which suggests that copying from the temp directory to the destination was unsuccessful.\n\nIf the destination volume is an ESP, try first saving to your Desktop, then copying the file over manually.",parent=self) except Exception as e: # Had an issue, throw up a display box self.bell() mb.showerror("An Error Occurred While Saving", repr(e), parent=self) return None finally: try: shutil.rmtree(temp,ignore_errors=True) except: pass # Normalize the path as needed path = os.path.normpath(path) if path else path # Retain the new path if the save worked correctly self.current_plist = path # Set the window title to the path self.title(path) # No changes - so we'll reset that self._ensure_edited(edited=False) return True def open_plist(self, path, plist_data, plist_type = "XML", auto_expand = True): # Opened it correctly - let's load it, and set our values self.plist_type_string.set(plist_type) self._tree.delete(*self._tree.get_children()) self.add_node(plist_data,check_binary=plist_type.lower() == "binary") self.current_plist = os.path.normpath(path) if path else path if path is None: self._ensure_edited(title=path or "Untitled.plist") else: self._ensure_edited(edited=False,title=path) self.undo_stack = [] self.redo_stack = [] # Close if need be if not auto_expand: self.collapse_all() # Ensure the root is expanded at least root = self.get_root_node() self._tree.item(root,open=True) self.select(root) def close_window(self, event = None, check_saving = True, check_close = True): # Check if we need to save first, then quit if we didn't cancel if check_saving and (self.saving or self.check_save() is None): # User cancelled or we failed to save, lift the window and bail self.controller.lift_window(self) return None # Destroy our current window - and initiate a check in the controller self.destroy() if check_close: self.controller.check_close() return True def _clipboard_append(self, clipboard_string = None): # Tkinter has issues copying to the system clipboard as evident in this bug report: # https://bugs.python.org/issue40452 # # There are some workarounds that require compiling a new tk binary - but we can # ensure the system clipboard is updated by calling either clip or pbcopy depending # on the current OS. # # First we clear the tkinter clipboard self.clipboard_clear() # Only write to the tkinter clipboard if we have a value if clipboard_string: self.clipboard_append(clipboard_string) else: clipboard_string = "" # Ensure we have at least an empty string # Gather our args for the potential clipboard commands Windows -> macOS -> Linux for args in (["clip"],) if os.name=="nt" else (["pbcopy"],) if sys.platform=="darwin" else (["xclip","-sel","c"],["xsel","-ib"],): # Try to start a subprocess to mirror the tkinter clipboard contents try: clipboard = subprocess.Popen(args,stdin=subprocess.PIPE) except: continue # Dirty py2 check to see if we need to encode the data or not clipboard.stdin.write(clipboard_string if 2/3==0 else clipboard_string.encode("utf-8")) break # Break out of the loop as needed def copy_selection(self, event = None): node = self._tree.focus() if node == "": # Nothing to copy return try: clipboard_string = plist.dumps(self.nodes_to_values(node),sort_keys=self.controller.settings.get("sort_dict",False)) if self.controller.settings.get("xcode_data",True): clipboard_string = self._format_data_string(clipboard_string) # Get just the values self._clipboard_append(clipboard_string) except: pass def copy_children(self, event = None): node = self._tree.focus() if node == "": # Nothing to copy return try: plist_data = self.nodes_to_values(node) if isinstance(plist_data,dict) and len(plist_data): # Set it to the first key's value plist_data = plist_data[list(plist_data)[0]] elif isinstance(plist_data,list) and len(plist_data): # Set it to the first item of the array plist_data = plist_data[0] clipboard_string = plist.dumps(plist_data,sort_keys=self.controller.settings.get("sort_dict",False)) self._clipboard_append(clipboard_string) except: pass def copy_all(self, event = None): try: clipboard_string = plist.dumps(self.nodes_to_values(self.get_root_node()),sort_keys=self.controller.settings.get("sort_dict",False)) if self.controller.settings.get("xcode_data",True): clipboard_string = self._format_data_string(clipboard_string) # Get just the values self._clipboard_append(clipboard_string) except: pass def paste_selection(self, event = None): # Try to format the clipboard contents as a plist try: clip = self.clipboard_get() except: clip = "" plist_data = None try: plist_data = plist.loads(clip,dict_type=dict if self.controller.settings.get("sort_dict",False) else OrderedDict) except: # May need the header # First check the type of the first element clip_check = clip.strip().lower() cb_list = [self.plist_header,clip,self.plist_footer] # If we start with a key, assume it's a dict. If we don't start with an array but have multiple newline-delimited elements, assume an array # - for all else, let the type remain element_type = "dict" if clip_check.startswith("") else "array" if not clip_check.startswith("") and len(clip_check.split("\n")) > 1 else None if element_type: cb_list.insert(1,"<{}>".format(element_type)) cb_list.insert(3,"".format(element_type)) cb = "\n".join(cb_list) try: plist_data = plist.loads(cb,dict_type=dict if self.controller.settings.get("sort_dict",False) else OrderedDict) except Exception as e: # Let's throw an error self.bell() mb.showerror("An Error Occurred While Pasting", repr(e),parent=self) return 'break' if plist_data is None: if len(clip): # Check if we actually pasted something self.bell() mb.showerror("An Error Occurred While Pasting", "The pasted value is not a valid plist string.",parent=self) # Nothing to paste return 'break' node = "" if not len(self._tree.selection()) else self._tree.selection()[0] # Verify the type - or get the parent t = self.get_check_type(node).lower() index = 0 if not node == "" and (not t in ("dictionary","array") or (self._tree.get_children(node) and not self._tree.item(node,"open"))): index = self._tree.index(node)+1 node = self._tree.parent(node) node = self.get_root_node() if node == "" else node # Force Root node if need be t = self.get_check_type(node).lower() # Convert data to dict first if isinstance(plist_data,list): # Convert to a dict to add new_plist = {} if self.controller.settings.get("sort_dict",False) else OrderedDict() for i,x in enumerate(plist_data): new_plist[str(i)] = x plist_data = new_plist add_list = [] if not isinstance(plist_data,dict): # Check if we're replacing the Root node if node == self.get_root_node() and self.get_root_type() is None: # Update the cell to reflect what's going on add_list.append({"type":"edit","cell":node,"text":self._tree.item(node,"text"),"values":self._tree.item(node,"values")}) if self.is_data(plist_data): self._tree.item(node, values=(self.get_type(plist_data),self.get_data(plist_data),"",)) elif isinstance(plist_data, datetime.datetime): self._tree.item(node, values=(self.get_type(plist_data),plist_data.strftime("%b %d, %Y %I:%M:%S %p"),"",)) else: self._tree.item(node, values=(self.get_type(plist_data),plist_data,"",)) else: # I guess we're not - let's force it into a dict to be savage plist_data = {"New item":plist_data} if isinstance(plist_data,dict): dict_list = list(plist_data.items()) if not self.controller.settings.get("sort_dict",False) else sorted(list(plist_data.items())) names = [self._tree.item(x,"text") for x in self._tree.get_children(node)] if t == "dictionary" else [] for (key,val) in dict_list[::-1]: if t == "dictionary": # create a unique name key = self.get_unique_name(str(key),names) names.append(key) last = self.add_node(val, node, key) # Move it into place self._tree.move(last,node,index) add_list.append({"type":"add","cell":last}) self._tree.item(last,open=True) first = self.get_root_node() if not len(add_list) else add_list[0].get("cell") self.add_undo(add_list) self._ensure_edited() self.update_all_children() self.select(first) ### ### # Converstion to/from Dict and Treeview Functions # ### ### def add_node(self, value, parentNode="", key=None, check_binary=False): if key is None: key = "Root" # Show the Root if isinstance(value,(list,tuple)): children = "1 child" if len(value) == 1 else "{} children".format(len(value)) values = (self.get_type(value),children,"" if parentNode == "" else self.drag_code) elif isinstance(value,dict): children = "1 key/value pair" if len(value) == 1 else "{} key/value pairs".format(len(value)) values = (self.get_type(value),children,"" if parentNode == "" else self.drag_code) else: values = (self.get_type(value),value,"" if parentNode == "" else self.drag_code) i = self._tree.insert(parentNode, "end", text=key, values=values) if isinstance(value, dict): if (not check_binary or (check_binary and self.plist_type_string.get().lower() != "binary")) \ and len(value) == 1 and "CF$UID" in value and isinstance(value["CF$UID"],(int,long)) \ and 0 <= value["CF$UID"] < 1 << 32: # Got a UID - ensure it's within the unsigned 32-bit int bounds value = value["CF$UID"] self._tree.item(i, values=(self.get_type(value,override="UID"),value,"" if parentNode == "" else self.drag_code,)) else: self._tree.item(i, open=True) dict_list = list(value.items()) if not self.controller.settings.get("sort_dict",False) else sorted(list(value.items())) for (key,val) in dict_list: self.add_node(val, i, key, check_binary=check_binary) elif isinstance(value, (list,tuple)): self._tree.item(i, open=True) for (key,val) in enumerate(value): self.add_node(val, i, key, check_binary=check_binary) elif self.is_data(value): self._tree.item(i, values=(self.get_type(value),self.get_data(value),"" if parentNode == "" else self.drag_code,)) elif isinstance(value, datetime.datetime): self._tree.item(i, values=(self.get_type(value),value.strftime("%b %d, %Y %I:%M:%S %p"),"" if parentNode == "" else self.drag_code,)) elif isinstance(value, (int,long)) and not isinstance(value, bool) and self.int_type_string.get().lower() == "hex": value = int(value) # Get the type before converting to hex string value_type = self.get_type(value) if value >= 0: value = "0x"+hex(value).upper()[2:] self._tree.item(i, values=(value_type,value,"" if parentNode == "" else self.drag_code,)) elif isinstance(value, bool): self._tree.item(i, values=(self.get_type(value),self.b_true() if value else self.b_false(),"" if parentNode == "" else self.drag_code,)) elif isinstance(value,plist.UID) or (hasattr(plistlib,"UID") and isinstance(value,plistlib.UID)): self._tree.item(i, values=(self.get_type(value),value.data,"" if parentNode == "" else self.drag_code,)) else: self._tree.item(i, values=(self.get_type(value),value,"" if parentNode == "" else self.drag_code,)) return i def get_value_from_node(self,node="",binary=False): values = self.get_padded_values(node, 3) value = values[1] check_type = self.get_check_type(node).lower() # Iterate value types if check_type == "dictionary": value = {} if self.controller.settings.get("sort_dict",False) else OrderedDict() elif check_type == "array": value = [] elif check_type == "boolean": value = True if values[1].lower() in self.all_b_true(lower=True) else False elif check_type == "number": if self.int_type_string.get().lower() == "hex" and value.lower().startswith("0x"): try: value = int(value,16) except: value = 0 else: try: value = int(value) except: try: value = float(value) except: value = 0 # default to 0 if we have to have something elif check_type == "uid": try: value = int(value) except: value = 0 # Same principle as the integer # UIDs behave differently depending on XML vs binary context value = plist.UID(value) if binary else {"CF$UID":value} elif check_type == "data": if self.data_type_string.get().lower() == "hex": # Convert the hex if sys.version_info < (3, 0): value = plistlib.Data(binascii.unhexlify(value.replace("<","").replace(">","").replace(" ",""))) else: value = binascii.unhexlify(value.replace("<","").replace(">","").replace(" ","").encode("utf-8")) else: # Assume base64 if sys.version_info < (3,0): value = plistlib.Data(base64.b64decode(value)) else: value = base64.b64decode(value.encode("utf-8")) elif check_type == "date": value = datetime.datetime.strptime(value,"%b %d, %Y %I:%M:%S %p") return value def nodes_to_values(self,node="",parent=None,binary=False): if node in ("",None,self.get_root_node()): # Top level - set the parent to the type of our Root node = self.get_root_node() parent = self.get_root_type() if parent is None: return self.get_value_from_node(node,binary=binary) # Return the raw value - we don't have a collection for child in self._tree.get_children(node): parent = self.nodes_to_values(child,parent=parent,binary=binary) return parent # Not top - process if parent is None: # We need to setup the parent p = self._tree.parent(node) if p in ("",self.get_root_node()): # The parent is the Root node parent = self.get_root_type() else: # Get the type based on our prefs parent = [] if self.get_check_type(p).lower() == "array" else {} if self.controller.settings.get("sort_dict",False) else OrderedDict() name = self._tree.item(node,"text") value = self.get_value_from_node(node,binary=binary) # At this point, we should have the name and value for child in self._tree.get_children(node): value = self.nodes_to_values(child,parent=value,binary=binary) if isinstance(parent,list): parent.append(value) elif isinstance(parent,dict): if isinstance(name,unicode): parent[name] = value else: parent[str(name)] = value return parent def get_type(self, value, override=None): if override and isinstance(override,str): return self.menu_code + " " + override elif isinstance(value, dict): return self.menu_code + " Dictionary" elif isinstance(value, list): return self.menu_code + " Array" elif isinstance(value, datetime.datetime): return self.menu_code + " Date" elif self.is_data(value): return self.menu_code + " Data" elif isinstance(value, bool): return self.menu_code + " Boolean" elif isinstance(value, (int,float,long)): return self.menu_code + " Number" elif isinstance(value, (str,unicode)): return self.menu_code + " String" elif isinstance(value,plist.UID) or (hasattr(plistlib,"UID") and isinstance(value,plistlib.UID)): return self.menu_code + " UID" else: return self.menu_code + str(type(value)) def is_data(self, value): return (sys.version_info >= (3, 0) and isinstance(value, bytes)) or (sys.version_info < (3,0) and isinstance(value, plistlib.Data)) def get_data(self, value): if sys.version_info < (3,0) and isinstance(value, plistlib.Data): value = value.data if not len(value): return "<>" if self.data_type_string.get().lower() == "hex" else "" if self.data_type_string.get().lower() == "hex": h = binascii.hexlify(value) if sys.version_info >= (3,0): h = h.decode("utf-8") return "<{}>".format(" ".join((h[0+i:8+i] for i in range(0, len(h), 8))).upper()) else: h = base64.b64encode(value) if sys.version_info >= (3,0): h = h.decode("utf-8") return h ### ### # Node Update Functions # ### ### def get_unique_name(self,name,names,int_check=False): start = 1 sep = " - " # create a unique name num = start # Initialize our counter while True: temp_name = str(name+num) if isinstance(name,(int,long)) else name if num == start else name+str(sep)+str(num) if not temp_name in names: break num += 1 return temp_name def new_row(self,target=None,force_sibling=False): if target is None or isinstance(target, tk.Event): target = "" if not len(self._tree.selection()) else self._tree.selection()[0] if target == self.get_root_node() and not self.get_check_type(self.get_root_node()).lower() in ("array","dictionary"): return # Can't add to a non-collection! new_cell = None index = 0 if not self.get_check_type(target).lower() in ("dictionary","array") or force_sibling or (not self._tree.item(target,"open") and len(self._tree.get_children(target))): index = self._tree.index(target)+1 target = self._tree.parent(target) target = self.get_root_node() if target == "" else target # Force the Root node if need be # create a unique name name = "" if self.get_check_type(target).lower() == "dictionary": names = [self._tree.item(x,"text")for x in self._tree.get_children(target)] name = self.get_unique_name("New String",names) new_cell = self._tree.insert(target, index, text=name, values=(self.menu_code + " String","",self.drag_code,)) # Verify that array names are updated to show the proper indexes if self.get_check_type(target).lower() == "array": self.update_array_counts(target) # Select and scroll to the target self.select(new_cell,alternate=False) self._ensure_edited() self.add_undo({"type":"add","cell":new_cell}) if target == "": # Top level, nothing to do here but edit the new row self.alternate_colors() return # Update the child counts self.update_children(target) # Ensure the target is opened self._tree.item(target,open=True) # Flush our alternating lines self.alternate_colors() def remove_row(self,target=None): if target is None or isinstance(target, tk.Event): target = "" if not len(self._tree.selection()) else self._tree.selection()[0] if target in ("",self.get_root_node()): # Can't remove top level return parent = self._tree.parent(target) self.add_undo({ "type":"remove", "cell":target, "from":parent, "index":self._tree.index(target) }) # Retain the index of our selected node to select the new target target_index = self._tree.get_children(parent).index(target) self._tree.detach(target) # Figure out what's left - select the node at the same index if possible, # the last index if not, and the parent if no other items remaining = self._tree.get_children(parent) new_target = parent if not len(remaining) else remaining[target_index] if target_index < len(remaining) else remaining[-1] self.select(new_target,alternate=False) self._ensure_edited() # Check if the parent was an array/dict, and update counts if parent == "": return if self.get_check_type(parent).lower() == "array": self.update_array_counts(parent) self.update_children(parent) self.alternate_colors() ### ### # Treeview Data Helper Methods # ### ### def get_padded_values(self, item, pad_to = 2): values = list(self._tree.item(item,"values")) values = [] if values == "" else values values += [''] * (pad_to - len(values)) return values def update_all_children(self): # Iterate the whole list, and ensure all arrays and dicts have their children updated # properly nodes = self.iter_nodes(False) for node in nodes: check_type = self.get_check_type(node).lower() if check_type == "dictionary": self.update_children(node) elif check_type == "array": self.update_children(node) self.update_array_counts(node) def update_children(self, target): # Update the key/value pairs or children count child_count = self._tree.get_children(target) values = self.get_padded_values(target, 3) if self.get_check_type(target).lower() == "dictionary": children = "1 key/value pair" if len(child_count) == 1 else "{} key/value pairs".format(len(child_count)) elif self.get_check_type(target).lower() == "array": children = "1 child" if len(child_count) == 1 else "{} children".format(len(child_count)) # Set the resulting values values[1] = children self._tree.item(target,values=values) def update_array_counts(self, target): for x,child in enumerate(self._tree.get_children(target)): # Only updating the "text" field self._tree.item(child,text=x) def cycle_bool(self, event=None): cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] value = self.get_check_type(cell) if not value.lower() == "boolean": return "break" bool_val = self.get_value_from_node(cell) self.set_bool(self.b_false() if bool_val else self.b_true()) return "break" def set_type_by_index(self, index = None, menu = None, zero_based = False): # Set our type based on index value if not isinstance(index,(int,long)): # Try to get the keysym try: index = int(getattr(index,"keysym",None).replace("KP_","")) except: return # Borked value if not menu: # We need to retrieve the menu manually cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] menu = self.root_type_menu if cell in ("",self.get_root_node()) else self.type_menu valid_vals = self._get_menu_commands(menu,label=True) if not valid_vals: return # Menu has no commands if not zero_based: index -= 1 # original index started at 1, normalize to 0-based if index < 0 or index >= len(valid_vals): return # Out of range target_index = menu.index(valid_vals[index]) if target_index is None: return # Nothing found menu.invoke(target_index) return "break" # Prevent the keypress from cascading def cycle_type(self, increment = True): # Set our type to the next in the list cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] value = self.get_check_type(cell) menu = self.root_type_menu if cell in ("",self.get_root_node()) else self.type_menu valid_vals = self._get_menu_commands(menu,label=True) if not valid_vals: return # Menu has no commands try: curr = valid_vals.index(value) except: return # Our current value isn't in the menu? mod = 1 if increment else -1 # Return set_type_by_index's return to prevent keypress cascading as needed return self.set_type_by_index((curr+mod)%len(valid_vals),menu=menu,zero_based=True) def change_type(self, value, cell = None): # Need to walk the values and pad if cell is None: cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] values = self.get_padded_values(cell, 3) # Verify we actually changed type if values[0] == value: # No change, bail return original = [x for x in values] # Replace our value values[0] = value # Remove children if needed changes = [] for i in self._tree.get_children(cell): changes.append({ "type":"remove", "cell":i, "from":self._tree.parent(i), "index":self._tree.index(i) }) self._tree.detach(i) changes.append({ "type":"edit", "cell":cell, "text":self._tree.item(cell,"text"), "values":self._tree.item(cell,"values") }) # Add to the undo stack self.add_undo(changes) # Set the value if need be value = self.get_check_type(None,value).lower() if value.lower() == "number": values[1] = "0" if self.int_type_string.get().lower() == "decimal" else "0x0" elif value.lower() == "boolean": values[1] = self.b_true() elif value.lower() == "array": self._tree.item(cell,open=True) values[1] = "0 children" elif value.lower() == "dictionary": self._tree.item(cell,open=True) values[1] = "0 key/value pairs" elif value.lower() == "date": values[1] = datetime.datetime.now().strftime("%b %d, %Y %I:%M:%S %p") elif value.lower() == "data": values[1] = "<>" if self.data_type_string.get().lower() == "hex" else "" elif value.lower() == "uid": values[1] = "0" else: values[1] = "" # Set the values self._tree.item(cell, values=values) self._ensure_edited() ### ### # Click Functions # ### ### def set_bool(self, value): # Need to walk the values and pad values = self.get_padded_values("" if not len(self._tree.selection()) else self._tree.selection()[0], 3) if values[1] == value: return # Nothing to do, setting it to itself. cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] self.add_undo({ "type":"edit", "cell":cell, "text":self._tree.item(cell,"text"), "values":[x for x in values] }) values[1] = value # Set the values self._tree.item("" if not len(self._tree.selection()) else self._tree.selection()[0], values=values) self._ensure_edited() def split(self, a, escape = '\\', separator = '/'): result = [] token = '' state = 0 for t in a: if state == 0: if t == escape: state = 1 elif t == separator: result.append(token) token = '' else: token += t elif state == 1: token += t state = 0 result.append(token) return result def get_cell_path(self, cell = None): # Returns the path to the given cell # Will clear out array indexes - as those should be ignored if cell is None: return None current_cell = cell path = [] while True: if current_cell == "": # Reached the top break cell = self._tree.parent(current_cell) if not self.get_check_type(cell).lower() == "array": # Our name isn't just a number add the key path.append(self._tree.item(current_cell,"text").replace("/","\\/")) else: path.append("*") current_cell = cell return "/".join(path[::-1]) def merge_menu_preset(self, val = None): if val is None: return # We need to walk the path of the item to ensure all # items exist - each should be a dict, unless specified # by a "*", which denotes a list. cell,path,itypes,value = val paths = self.split(str(path)) types = itypes.split("/") if not len(paths) == len(types): self.bell() mb.showerror("Incorrect Patch Format", "Patch is incomplete.", parent=self) return if not len(paths) or paths[0] == "*": # No path, or it's an array self.bell() mb.showerror("Incorrect Patch Format", "Patch starts with an array - must be a dictionary.", parent=self) return # Iterate both the paths and types lists - checking for each value, # and ensuring the type created = None current_cell = "" undo_list = [] for p,t in izip(paths,types): found = False needed_type = {"d":"Dictionary","a":"Array"}.get(t.lower(),"Dictionary") for x in self._tree.get_children(current_cell): cell_name = self._tree.item(x,"text") if cell_name == p: current_type = self.get_check_type(x) if not current_type.lower() == needed_type.lower(): # Raise an error - type mismatch self.bell() if mb.askyesno("Incorrect Type","{} is {}, should be {}.\n\nWould you like to replace it?".format(cell_name,current_type,needed_type),parent=self): # We need to remove any children, and change the type for y in self._tree.get_children(x): undo_list.append({ "type":"remove", "cell":y, "from":x, "index":self._tree.index(y) }) self._tree.detach(y) # Change the type undo_list.append({ "type":"edit", "cell":x, "text":self._tree.item(x,"text"), "values":self._tree.item(x,"values") }) values = self.get_padded_values(x,3) values[0] = self.menu_code + " " + needed_type if needed_type.lower() == "dictionary": values[1] = "0 key/value pairs" else: values[1] = "0 children" self._tree.item(x, values=values) else: # Let's undo anything we've already done and bail self.reundo(None,True,undo_list) return found = True current_cell = x break if not found: # Need to add it current_cell = self._tree.insert(current_cell,"end",text=p,values=(self.menu_code+" "+needed_type,"",self.drag_code,),open=True) undo_list.append({ "type":"add", "cell":current_cell }) # At this point - we should be able to add the final piece # let's first make sure it doesn't already exist - if it does, we # will overwrite it current_type = self.get_check_type(current_cell).lower() just_add = True replace_asked = False if current_type == "dictionary": # Scan through and make sure we have all the keys needed for x in self._tree.get_children(current_cell): name = self._tree.item(x,"text") if name in value: if not replace_asked: # Ask first self.bell() if mb.askyesno("Key(s) Already Exist","One or more keys already exist at the destination.\n\nWould you like to replace them?",parent=self): replace_asked = True else: # User said no, let's undo self.reundo(None,True,undo_list) return # Remove the top level item undo_list.append({ "type":"remove", "cell":x, "from":current_cell, "index":self._tree.index(x) }) self._tree.detach(x) # Add the entries if isinstance(value,dict): just_add = False for x in value: created = self.add_node(value[x],current_cell,x) undo_list.append({ "type":"add", "cell":created }) if just_add: last_cell = self.add_node(value,current_cell,"") undo_list.append({ "type":"add", "cell":last_cell }) self.add_undo(undo_list) self._ensure_edited() self.update_all_children() self.alternate_colors() def do_sort(self, cell, recursive = False, reverse = False): undo_tasks = [] children = self._tree.get_children(cell) if not len(children): return undo_tasks # bail early, nothing to check sorted_children = [x for x in children] if self.get_check_type(cell).lower() == "dictionary": sorted_children = sorted(sorted_children,key=lambda x:self._tree.item(x,"text").lower(), reverse=reverse) skip_sort = all((children[x]==sorted_children[x] for x in range(len(children)))) for index,child in enumerate(sorted_children): if self.get_check_type(child).lower() in ("dictionary","array") and recursive: undo_tasks.extend(self.do_sort(child,recursive=recursive,reverse=reverse)) if skip_sort: continue # No change - skip sorting # Add the move command undo_tasks.append({ "type":"move", "cell":child, "from":cell, "to":cell, "index":self._tree.index(child) }) self._tree.move(child, cell, index) return undo_tasks def sort_keys(self, cell, recursive = False, reverse = False): # Let's build a sorted list of keys, then generate move edits for each undo_tasks = self.do_sort(cell,recursive=recursive,reverse=reverse) if not len(undo_tasks): return # Nothing changed - bail self._ensure_edited() self.add_undo(undo_tasks) self.alternate_colors() def popup(self, event): # Select the item there if possible cell = self._tree.identify('item', event.x, event.y) if cell: self.select(cell,alternate=False) # Build right click menu popup_menu = tk.Menu(self, tearoff=0) if self.get_check_type(cell).lower() in ("array","dictionary"): popup_menu.add_command(label="Expand Node", command=self.expand_node) popup_menu.add_command(label="Collapse Node", command=self.collapse_node) popup_menu.add_separator() popup_menu.add_command(label="Expand Children", command=self.expand_children) popup_menu.add_command(label="Collapse Children", command=self.collapse_children) popup_menu.add_separator() popup_menu.add_command(label="Expand All", command=self.expand_all) popup_menu.add_command(label="Collapse All", command=self.collapse_all) popup_menu.add_separator() is_mac = sys.platform == "darwin" # Determine if we are adding a child or a sibling if cell in ("",self.get_root_node()): # Top level - get the Root if self.get_check_type(self.get_root_node()).lower() in ("array","dictionary"): popup_menu.add_command(label="New top level entry{}".format(" (Cmd +)" if is_mac else ""), command=lambda:self.new_row(self.get_root_node()),accelerator=None if is_mac else "(Ctrl +)") else: if self.get_check_type(cell).lower() in ("dictionary","array") and (self._tree.item(cell,"open") or not len(self._tree.get_children(cell))): popup_menu.add_command(label="New child under '{}'{}".format(self._tree.item(cell,"text")," (Cmd +)" if is_mac else ""), command=lambda:self.new_row(cell),accelerator=None if is_mac else "(Ctrl +)") popup_menu.add_command(label="New sibling of '{}'".format(self._tree.item(cell,"text")), command=lambda:self.new_row(cell,True)) popup_menu.add_command(label="Remove '{}' and any children{}".format(self._tree.item(cell,"text")," (Cmd -)" if is_mac else ""), command=lambda:self.remove_row(cell),accelerator=None if is_mac else "(Ctrl -)") else: popup_menu.add_command(label="New sibling of '{}'{}".format(self._tree.item(cell,"text")," (Cmd +)" if is_mac else ""), command=lambda:self.new_row(cell),accelerator=None if is_mac else "(Ctrl +)") popup_menu.add_command(label="Remove '{}'{}".format(self._tree.item(cell,"text")," (Cmd -)" if is_mac else ""), command=lambda:self.remove_row(cell),accelerator=None if is_mac else "(Ctrl -)") # Let's get our sorting menus parent = cell if cell in ("",self.get_root_node()) else self._tree.parent(cell) if self.get_check_type(parent).lower() in ("dictionary","array"): # Add a separator, and the Sort Keys options popup_menu.add_separator() # Find out if we can recursively start with the cell, or if we need to start with the parent recurs_target = cell if self.get_check_type(cell).lower() in ("dictionary","array") and len(self._tree.get_children(cell)) else parent popup_menu.add_command(label="Recursively sort keys starting at '{}'".format(self._tree.item(recurs_target,"text")), command=lambda:self.sort_keys(recurs_target,recursive=True)) popup_menu.add_command(label="Recursively reverse sort keys starting at '{}'".format(self._tree.item(recurs_target,"text")), command=lambda:self.sort_keys(recurs_target,recursive=True,reverse=True)) # Check the actual cell sort_target = cell if cell in ("",self.get_root_node()) or (self.get_check_type(cell).lower() == "dictionary" and len(self._tree.get_children(cell))>1) else self._tree.parent(cell) popup_menu.add_command(label="Sort keys in '{}'".format(self._tree.item(sort_target,"text")), command=lambda:self.sort_keys(sort_target)) popup_menu.add_command(label="Reverse sort keys in '{}'".format(self._tree.item(sort_target,"text")), command=lambda:self.sort_keys(sort_target,reverse=True)) # Add the copy and paste options popup_menu.add_separator() c_state = "normal" if len(self._tree.selection()) else "disabled" try: p_state = "normal" if len(self.root.clipboard_get()) else "disabled" except: p_state = "disabled" # Invalid clipboard content popup_menu.add_command(label="Copy{}".format(" (Cmd+C)" if is_mac else ""),command=self.copy_selection,state=c_state,accelerator=None if is_mac else "(Ctrl+C)") if not cell in ("",self.get_root_node()) and self.get_check_type(cell).lower() in ("array","dictionary"): popup_menu.add_command(label="Copy Children", command=self.copy_children,state=c_state) popup_menu.add_command(label="Paste{}".format(" (Cmd+V)" if is_mac else ""),command=self.paste_selection,state=p_state,accelerator=None if is_mac else "(Ctrl+V)") cell_path = self.get_cell_path(cell) # Add rbits option cell_search = [x for x in self.split(cell_path) if not x=="*"] if cell_search and cell_search[0] == "Root": cell_search = cell_search[1:] if cell_search and self.get_configuration_path(): popup_menu.add_separator() popup_menu.add_command(label="Show info for \"{}\"{}".format( " -> ".join(cell_search), " (Cmd+I)" if is_mac else ""), command=self.show_config_info, accelerator=None if is_mac else "(Ctrl+I)") # Walk through the menu data if it exists first_key = True for key in sorted(list(self.menu_data)): options = self.menu_data[key] valid = [x for x in list(options) if x.startswith(cell_path)] if not len(valid): # No hits - bail continue # Add a separator if first_key: popup_menu.add_separator() first_key = False # Iterate and add option_menu = tk.Menu(popup_menu,tearoff=0) for item in sorted(valid): item_menu = tk.Menu(option_menu,tearoff=0) for x in options[item]: if x.get("separator",False) != False: item_menu.add_separator() elif x.get("title",False) != False: item_menu.add("command",label=x.get("title",""),state="disabled") else: name = x["name"] value = x["value"] types = x["types"] passed = (cell,item,types,value) item_menu.add_command(label=name,command=lambda item=passed: self.merge_menu_preset(item)) parts = self.split(item) option_menu.add_cascade(label=" -> ".join(parts[1 if parts[0].lower() == "root" and len(parts) > 1 else 0:]),menu=item_menu) popup_menu.add_cascade(label=key,menu=option_menu) try: popup_menu.tk_popup(event.x_root, event.y_root, 0) except: pass finally: popup_menu.grab_release() def expand_node(self): # Get selected node cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] self._tree.item(cell,open=True) self.alternate_colors() def collapse_node(self): # Get selected node cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] self._tree.item(cell,open=False) def expand_all(self): # Get all nodes nodes = self.iter_nodes(False) for node in nodes: self._tree.item(node,open=True) self.alternate_colors() def collapse_all(self): # Get all nodes nodes = self.iter_nodes(False) for node in nodes: self._tree.item(node,open=False) self.alternate_colors() def expand_children(self): # Get all children of the selected node cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] nodes = self.iter_nodes(False, cell) nodes.append(cell) for node in nodes: self._tree.item(node,open=True) self.alternate_colors() return "break" # Prevent keybinds from propagating further def collapse_children(self): # Get all children of the selected node cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] nodes = self.iter_nodes(False, cell) # nodes.append(cell) for node in nodes: self._tree.item(node,open=False) self.alternate_colors() return "break" # Prevent keybinds from propagating further def tree_click_event(self, event): # close previous popups self.destroy_popups() self.alternate_colors() def on_double_click(self, event): # close previous popups self.destroy_popups() # what row and column was clicked on rowid = self._tree.identify_row(event.y) column = self._tree.identify_column(event.x) if not rowid or not self._tree.bbox(rowid): # Nothing double clicked, bail return "break" # clicked row parent id parent = self._tree.parent(rowid) # get the actual item name we're editing tv_item = self._tree.identify('item', event.x, event.y) # Get the actual text index = int(column.replace("#","")) try: t = self.get_check_type(rowid) except: t = "" try: pt = self.get_check_type(self._tree.parent(tv_item)) except: pt = "" if index == 1: # Type change - let's show our menu type_menu = self.root_type_menu if parent == "" else self.type_menu try: type_menu.tk_popup(event.x_root, event.y_root, 0) finally: type_menu.grab_release() return 'break' if parent == "" and (index == 0 or self.get_check_type(self.get_root_node()).lower() in ("array","dictionary")): return 'break' # Not changing the type - can't change the name of Root if index == 2: if t.lower() in ("dictionary","array"): # Can't edit the "value" directly - should only show the number of children return 'break' elif t.lower() == "boolean": # Set up the boolean selection menu bool_menu = tk.Menu(self, tearoff=0) bool_menu.add_command(label=self.b_true(), command=lambda:self.set_bool(self.b_true())) bool_menu.add_command(label=self.b_false(), command=lambda:self.set_bool(self.b_false())) # Bool change try: bool_menu.tk_popup(event.x_root, event.y_root, 0) finally: bool_menu.grab_release() return 'break' if index == 0: if pt.lower() == "array": # No names here, bail return 'break' # The name of the item, can be changed at any time text = self._tree.item(rowid, 'text') else: try: text = self._tree.item(rowid, 'values')[index-1] except: text = "" if index == 2 and t.lower() == "data": # Special formatting of hex values text = text.replace("<","").replace(">","") # place Entry popup properly self.entry_popup = EntryPopup(self._tree, self, text, tv_item, column) self.entry_popup.relocate() return 'break' ### ### # Maintenance Functions # ### ### def destroy_popups(self): # auto-confirm changes if not self.entry_popup: return try: self.entry_popup.confirm(None) except: pass try: self.entry_popup.destroy() except: pass self.entry_popup = None def iter_nodes(self, visible = True, current_item = None): items = [] if current_item is None or isinstance(current_item, tk.Event): current_item = "" for child in self._tree.get_children(current_item): items.append(child) if not visible or self._tree.item(child,"open"): items.extend(self.iter_nodes(visible, child)) return items def get_root_node(self): children = self._tree.get_children("") if not len(children) == 1: raise Exception("Root is malformed!") return children[0] def get_root_type(self): check_type = self.get_check_type(self.get_root_node()).lower() # Iterate value types if check_type == "dictionary": return {} if self.controller.settings.get("sort_dict",False) else OrderedDict() elif check_type == "array": return [] return None def select(self, node, see = True, alternate = True): self._tree.selection_set(node) self._tree.focus(node) self._tree.update() if see: self._tree.see(node) if alternate: self.alternate_colors() def pre_alternate(self, event): # Only called before an item opens - we need to open it manually to ensure # colors alternate correctly cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] if not self._tree.item(cell,"open"): self._tree.item(cell,open=True) # Call the actual alternate_colors function self.alternate_colors(event) def set_colors(self, event = None): # Setup the colors and styles self.r1 = self.controller.r1_canvas["background"] self.r2 = self.controller.r2_canvas["background"] self.hl = self.controller.hl_canvas["background"] self.bg = self.controller.bg_canvas["background"] if self.controller.ig_bg_check.get() or not self.controller.should_set_header_text(): # Ignoring background color when updating header text self.bgt = "white" if self.controller.bg_inv_check.get() else "black" else: self.bgt = self.controller.text_color(self.bg,invert=self.controller.bg_inv_check.get()) self.r1t = self.controller.text_color(self.r1,invert=self.controller.r1_inv_check.get()) self.r2t = self.controller.text_color(self.r2,invert=self.controller.r2_inv_check.get()) self.hlt = self.controller.text_color(self.hl,invert=self.controller.hl_inv_check.get()) self.style.configure(self.style_name, background=self.bg, fieldbackground=self.bg) self.style.configure(self.style_name+".Heading", background=self.bg, foreground=self.bgt) self.style.map(self.style_name, background=[("selected", self.hl)], foreground=[("selected", self.hlt)]) self._tree.tag_configure('even', foreground=self.r1t, background=self.r1) self._tree.tag_configure('odd', foreground=self.r2t, background=self.r2) self._tree.tag_configure("selected", foreground="black", background=self.hl) self.alternate_colors() def alternate_colors(self, event = None): # Let's walk the children of our treeview visible = self.iter_nodes(True,event) for x,item in enumerate(visible): tags = self._tree.item(item,"tags") if not isinstance(tags,list): tags = [] # Strip out odd/even/selected tags = [x for x in tags if not x in ("odd","even","selected")] if item == self._tree.focus(): tags.append("selected") else: tags.append("odd" if x % 2 else "even") self._tree.item(item, tags=tags) def get_configuration_path(self): # will need path to a Configuration.tex based for the version of OpenCore being used # for this to provide the correct info # for now this is a cheap hack to a Configuration.tex in the same location as ProperTree pt_path = os.path.normpath(sys.path[0]) # Add a check for next to the script config_tex_paths = [os.path.join(pt_path,"Configuration.tex")] pt_path_parts = pt_path.split(os.sep) if len(pt_path_parts) >= 3 and pt_path_parts[-2:] == ["Contents","MacOS"] \ and pt_path_parts[-3].lower().endswith(".app"): for x in range(3): # Remove the last 3 path components as we're in a .app bundle pt_path = os.path.dirname(pt_path) # Add a check for next to the .app bundle config_tex_paths.append(os.path.join(pt_path,"Configuration.tex")) # Iterate any paths we need to check and return the first match for path in config_tex_paths: if os.path.isfile(path): return path def show_config_info(self, event = None): # find the path of selected cell cell = "" if not len(self._tree.selection()) else self._tree.selection()[0] # don't strip * - needed to differentiate sub section from subsub section search_list = self.split(self.get_cell_path(cell)) # but remove it if it's the last item if search_list and search_list[-1] == "*": search_list.pop() if search_list and search_list[0] == "Root": search_list = search_list[1:] # Remove "Root" if not search_list: # nothing to search for return # Check if a window with this entry is already open check_title = '"{}" Info'.format(" -> ".join([x for x in search_list if not x=="*"])) window = next((x for x in self.controller.stackorder(self.controller.tk,include_defaults=True) if x.title() == check_title),None) if not window: config_tex_path = self.get_configuration_path() if config_tex_path: # pass mouse pointer location as location to open info window mx = self.root.winfo_pointerx() my = self.root.winfo_pointery() window = config_tex_info.display_info_window( config_tex_path, search_list, 120, False, False, mx, my, font=self.font, fg=self.r1t, bg=self.r1 ) if window: # Override the window closing protocol to allow stack order checks window.protocol("WM_DELETE_WINDOW", lambda x=window:self.controller.close_window(window=x)) window.bind("<{}-w>".format("Command" if sys.platform == "darwin" else "Control"), self.controller.close_window) self.controller.set_window_opacity(window=window) else: window = self # Ensure we're lifted again # Ensure window is lifted self.controller.lift_window(window)