class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) self.bdm = None self.parent = parent self.entry_list = TreeListCtrl(self, style=TL_MULTIPLE) self.entry_list.AppendColumn("Entry") self.entry_list.Bind(EVT_TREELIST_ITEM_CONTEXT_MENU, self.on_right_click) self.entry_list.Bind(EVT_TREELIST_SELECTION_CHANGED, self.on_select) self.cdo = wx.CustomDataObject("BDMEntry") self.Bind(wx.EVT_MENU, self.on_delete, id=wx.ID_DELETE) self.Bind(wx.EVT_MENU, self.on_copy, id=wx.ID_COPY) self.Bind(wx.EVT_MENU, self.on_paste, id=wx.ID_PASTE) self.Bind(wx.EVT_MENU, self.on_new, id=wx.ID_NEW) accelerator_table = wx.AcceleratorTable([ (wx.ACCEL_CTRL, ord('c'), wx.ID_COPY), (wx.ACCEL_CTRL, ord('v'), wx.ID_PASTE), (wx.ACCEL_NORMAL, wx.WXK_DELETE, wx.ID_DELETE), ]) self.entry_list.SetAcceleratorTable(accelerator_table) pub.subscribe(self.on_select, 'on_select') pub.subscribe(self.convert_for_skill_creator, 'convert_for_skill_creator') # Use some sizers to see layout options sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.entry_list, 1, wx.ALL | wx.EXPAND, 10) # Layout sizers self.SetSizer(sizer) self.SetAutoLayout(1) def build_tree(self): self.entry_list.DeleteAllItems() root = self.entry_list.GetRootItem() for entry in sorted(self.bdm.entries, key=lambda e: e.id): self.entry_list.AppendItem(root, f'{entry.id}: Entry', data=entry) def get_current_entry_ids(self): entry_ids = [] item = self.entry_list.GetFirstItem() while item.IsOk(): data = self.entry_list.GetItemData(item) entry_ids.append(data.id) item = self.entry_list.GetNextItem(item) return entry_ids def get_previous_entry(self, entry_id): item = self.entry_list.GetFirstItem() prev = TLI_FIRST while item.IsOk(): data = self.entry_list.GetItemData(item) if data.id > entry_id: break prev, item = item, self.entry_list.GetNextItem(item) return prev def on_right_click(self, _): selected = self.entry_list.GetSelections() if not selected: return menu = wx.Menu() menu.Append(wx.ID_NEW) menu.Append(wx.ID_DELETE) menu.Append(wx.ID_COPY) paste = menu.Append(wx.ID_PASTE) add = menu.Append(wx.ID_ADD, '&Add Copied Entry') success = False # Check Clipboard if wx.TheClipboard.Open(): success = wx.TheClipboard.IsSupported(wx.DataFormat("BDMEntry")) wx.TheClipboard.Close() add.Enable(success) paste.Enable(success) self.PopupMenu(menu) menu.Destroy() def on_select(self, _): selected = self.entry_list.GetSelections() if len(selected) != 1: pub.sendMessage('disable') return pub.sendMessage('load_entry', entry=self.entry_list.GetItemData(selected[0])) def add_entry(self, entry): root = self.entry_list.GetRootItem() prev = self.get_previous_entry(entry.id) item = self.entry_list.InsertItem(root, prev, f'{entry.id}: Entry', data=entry) self.entry_list.Select(item) self.on_select(None) def on_new(self, _): # Ask for ID if self.bdm is None: return with NewEntryDialog(self, self.get_current_entry_ids()) as dlg: if dlg.ShowModal() != wx.ID_OK: return entry_id = dlg.GetValue() self.entry_list.UnselectAll() # Add it entry = Entry(entry_id=entry_id) self.add_entry(entry) pub.sendMessage('set_status_bar', text='Added new entry') def on_add(self, _): if self.bdm is None: return cdo = wx.CustomDataObject("BDMEntry") success = False if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(cdo) wx.TheClipboard.Close() if not success: with wx.MessageDialog(self, 'Unable to get copied data') as dlg: dlg.ShowModal() return paste_data = pickle.loads(cdo.GetData()) # Get new Id's with NewEntryDialog(self, self.get_current_entry_ids()) as dlg: if dlg.ShowModal() != wx.ID_OK: return entry_id = dlg.GetValue() current_entry_ids = self.get_current_entry_ids() self.entry_list.UnselectAll() # Paste for paste in paste_data: while entry_id in current_entry_ids: entry_id += 1 entry = Entry(entry_id=entry_id) entry.paste(paste) self.add_entry(entry) entry_id += 1 self.on_select(None) pub.sendMessage(f'Pasted {len(paste_data)} new entry(s)') def on_delete(self, _): selected = self.entry_list.GetSelections() if not selected: return for item in selected: self.entry_list.DeleteItem(item) pub.sendMessage('disable') pub.sendMessage('set_status_bar', text=f'Deleted {len(selected)} entries') def on_copy(self, _): selected = self.entry_list.GetSelections() self.cdo = wx.CustomDataObject("BDMEntry") self.cdo.SetData( pickle.dumps( [self.entry_list.GetItemData(item) for item in selected])) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(self.cdo) wx.TheClipboard.Flush() wx.TheClipboard.Close() pub.sendMessage('set_status_bar', text=f'Copied {len(selected)} entries') def on_paste(self, _): selected = self.entry_list.GetSelections() if not selected: return success = False cdo = wx.CustomDataObject("BDMEntry") if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(cdo) wx.TheClipboard.Close() if success: paste_data = pickle.loads(cdo.GetData()) paste_length = len(paste_data) selected_length = len(selected) if selected_length > paste_length: for item in selected[paste_length:]: self.entry_list.Unselect(item) selected = selected[:paste_length] item = selected[-1] self.entry_list.Select(item) for n in range(paste_length - selected_length): item = self.entry_list.GetNextItem(item) if not item.IsOk(): with wx.MessageDialog( self, f'Not enough entries to paste over. Expected {paste_length}' ) as dlg: dlg.ShowModal() return self.entry_list.Select(item) selected.append(item) if len(selected) > 1: msg = '\n'.join([ f' * {self.entry_list.GetItemData(item).id}: Entry' for item in selected ]) with MultiMessageDialog( self, 'Are you sure you want to replace the following entries?', 'Warning', msg, wx.YES | wx.NO) as dlg: if dlg.ShowModal() != wx.ID_YES: return for n, paste in enumerate(paste_data): data = self.entry_list.GetItemData(selected[n]) data.paste(paste) self.on_select(None) pub.sendMessage('set_status_bar', text=f'Pasted {len(paste_data)} entry(s)') def convert_for_skill_creator(self): if not self.bdm: with wx.MessageDialog(self, "No BDM loaded!", "Error") as dlg: dlg.ShowModal() return # Get choices choices = set() item = self.entry_list.GetFirstItem() effect_ids = [ 'effect_1_skill_id', 'effect_2_skill_id', 'effect_3_skill_id' ] while item.IsOk(): data = self.entry_list.GetItemData(item) item = self.entry_list.GetNextItem(item) for sub_entry in data.sub_entries: for effect_id in effect_ids: try: if sub_entry[effect_id] != 0 and sub_entry[ effect_id] != 0xFFFF and sub_entry[ effect_id] != 0xBACA: choices.update([str(sub_entry[effect_id])]) except Exception as e: pass if not choices: with wx.MessageDialog(self, "Cannot find any Skill IDs to convert", "Error") as dlg: dlg.ShowModal() return # Show Dialog with ConvertDialog(self, list(choices)) as dlg: if dlg.ShowModal() != wx.ID_OK: return skill_id = dlg.GetValue() # Do Conversion item = self.entry_list.GetFirstItem() changed = 0 while item.IsOk(): data = self.entry_list.GetItemData(item) item = self.entry_list.GetNextItem(item) for sub_entry in data.sub_entries: for effect_id in effect_ids: if sub_entry[effect_id] == skill_id: try: sub_entry[effect_id] = 0xBACA changed += 1 except Exception: pass self.on_select(None) pub.sendMessage('set_status_bar', text=f'Changed {changed} skill ids to 0xBACA') def reindex(self): selected = self.entry_list.GetSelections() if len(selected) != 1: return item = selected[0] entry = self.entry_list.GetItemData(item) self.entry_list.DeleteItem(item) self.add_entry(entry)
class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) self.code = '' self.bac = None self.bdm = None self.ean = None self.cam_ean = None self.dirname = '' self.parent = parent self.links = defaultdict( lambda: defaultdict(lambda: defaultdict(dict))) # Name self.name = wx.StaticText(self, -1, '(No file loaded)') self.font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) self.name.SetFont(self.font) # Buttons self.open = wx.Button(self, wx.ID_OPEN, "Load") self.save = wx.Button(self, wx.ID_SAVE, "Save") self.save.Disable() self.paste = wx.Button(self, wx.ID_PASTE, "Paste") self.paste.Disable() self.add = wx.Button(self, wx.ID_ADD, "Add Copy") self.add.Disable() # Entry List self.entry_list = TreeListCtrl(self, style=TL_MULTIPLE) self.entry_list.AppendColumn("BAC Entry") self.entry_list.Bind(EVT_TREELIST_ITEM_CONTEXT_MENU, self.on_right_click) # Bind self.Bind(wx.EVT_BUTTON, self.on_open, id=wx.ID_OPEN) self.Bind(wx.EVT_BUTTON, self.on_save, id=wx.ID_SAVE) self.Bind(wx.EVT_BUTTON, self.on_paste, id=wx.ID_PASTE) self.Bind(wx.EVT_BUTTON, self.on_add, id=wx.ID_ADD) self.Bind(wx.EVT_MENU, self.on_paste, id=wx.ID_PASTE) accelerator_table = wx.AcceleratorTable([ (wx.ACCEL_CTRL, ord('v'), wx.ID_PASTE), ]) self.entry_list.SetAcceleratorTable(accelerator_table) self.SetDropTarget(FileDropTarget(self, "load_main_moveset")) # Button Sizer button_sizer = wx.BoxSizer(wx.HORIZONTAL) button_sizer.Add(self.open) button_sizer.AddSpacer(5) button_sizer.Add(self.save) button_sizer.AddSpacer(5) button_sizer.Add(self.paste) button_sizer.AddSpacer(5) button_sizer.Add(self.add) # Use some sizers to see layout options sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.name, 0, wx.CENTER) sizer.Add(button_sizer) sizer.Add(self.entry_list, 1, wx.ALL | wx.EXPAND, 10) # Layout sizers self.SetSizer(sizer) self.SetAutoLayout(1) def on_open(self, _): pub.sendMessage('open_main_moveset') def on_save(self, _): pub.sendMessage('save_moveset') def build_tree(self): self.entry_list.DeleteAllItems() root = self.entry_list.GetRootItem() for i, entry in enumerate(self.bac.entries): if not entry.sub_entries: continue self.entry_list.AppendItem( root, f'{entry.index}: {KNOWN_ENTRIES.get(entry.index, "Unknown")}', data=entry) self.save.Enable() self.paste.Enable() self.add.Enable() def on_right_click(self, _): selected = self.entry_list.GetSelections() if not selected: return menu = wx.Menu() paste = menu.Append(wx.ID_PASTE) paste.Enable(self.parent.copied is not None) self.PopupMenu(menu) menu.Destroy() def find_next_available_index(self, item_type): if item_type == Animation: return len(self.ean.animations) elif item_type == Hitbox: return max([entry.id for entry in self.bdm.entries]) + 1 elif item_type == Camera: return len(self.cam_ean.animations) else: raise (TypeError(f'Unsupported type: {item_type.__name__}')) def find_conflict(self, item_type, entry_pair, depend_value, selected_data, value): if value in list( self.links[item_type][entry_pair][depend_value].values()): return True for entry in self.bac.entries: # Skip this entry if its already in the selected data if entry in selected_data: continue for sub_entry in entry.sub_entries: # Skip if this is not the type we want if item_type != ITEM_TYPES[sub_entry.type]: continue for item in sub_entry.items: if item[entry_pair[1]] == depend_value and item[ entry_pair[0]] == value: return True return False def create_new_index(self, item_type): new_value = self.find_next_available_index(item_type) if item_type == Animation: animation = EanAnimation(self.ean) self.ean.animations.append(animation) elif item_type == Hitbox: self.bdm.entries.append(BdmEntry(entry_id=new_value)) elif item_type == Camera: self.cam_ean.animations.append(EanAnimation(self.cam_ean)) else: raise (TypeError(f'Unsupported type: {item_type.__name__}')) return new_value def file_not_found_dialog(self, filename): with wx.MessageDialog( self, f'{filename} was not opened. Please add it and reload the moveset', 'Error') as dlg: dlg.ShowModal() def invalid_index_dialog(self, filename, index): with wx.MessageDialog(self, f'{filename} does not contain index {index}', 'Error') as dlg: dlg.ShowModal() def copy_index(self, item_type, old_value, new_value): new_code = self.code old_code = self.parent.side_panel.code if item_type == Animation: try: animation = self.ean.animations[new_value] except IndexError: self.invalid_index_dialog(f'{new_code}.ean', new_value) return False try: animation.paste( self.parent.side_panel.ean.animations[old_value], keep_name=True) except IndexError: self.invalid_index_dialog(f'{old_code}.ean', old_value) return False elif item_type == Hitbox: if not self.bdm: self.file_not_found_dialog(f'{new_code}_PLAYER.bdm') return False if not self.parent.side_panel.bdm: self.file_not_found_dialog(f'{old_code}_PLAYER.bdm') return False try: entry = [ entry for entry in self.bdm.entries if entry.id == new_value ][0] except IndexError: self.invalid_index_dialog(f'{new_code}_PLAYER.bdm', new_value) return False try: old_entry = [ entry for entry in self.parent.side_panel.bdm.entries if entry.id == old_value ][0] except IndexError: self.invalid_index_dialog(f'{old_code}_PLAYER.bdm', old_value) return False entry.paste(old_entry) elif item_type == Camera: if not self.cam_ean: self.file_not_found_dialog(f'{new_code}.cam.ean') return False if not self.parent.side_panel.cam_ean: self.file_not_found_dialog(f'{old_code}.cam.ean') return False try: camera = self.cam_ean.animations[new_value] except IndexError: self.invalid_index_dialog(f'{new_code}.cam.ean', new_value) return False try: camera.paste( self.parent.side_panel.cam_ean.animations[old_value], keep_name=True) except IndexError: self.invalid_index_dialog(f'{old_code}.cam.ean', old_value) return False else: raise (TypeError(f'Unsupported type: {item_type.__name__}')) return True def changed_value_message(self, entry_index, changed_values, item_type, old_value, new_value, new=True): old_string = str(old_value) new_string = str(new_value) if item_type == Animation: old_string += f' ({self.parent.side_panel.ean.animations[old_value].name})' new_string += f' ({"*new*" if new else self.ean.animations[new_value].name})' changed_values[item_type].append( (f'[{entry_index}]', new_string, old_string)) def get_changed_values(self, changed_values, item_type, entry_pair, depend_value, entry_values, selected_val, selected_data): entry_index = selected_val[0] selected_val = selected_val[1] entry_name = KNOWN_ENTRIES.get(entry_index, 'Unknown') n = 0 for old_value in entry_values: # Continue if we already have a link old_animation_name = self.parent.side_panel.ean.animations[old_value].name \ if item_type == Animation and old_value < len(self.parent.side_panel.ean.animations) else '' if item_type == Animation and any( word in old_animation_name for word in BLACKLISTED_WORDS) \ and not old_animation_name.endswith(entry_name): continue if old_value in self.links[item_type][entry_pair][depend_value]: new_value = self.links[item_type][entry_pair][depend_value][ old_value] # if (old_value, new_value) in list(zip(*changed_values[item_type])): self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value, False) continue # If the current n is bigger then the selected values or selected values doesn't exist if item_type in selected_val and entry_pair in selected_val[ item_type]: if n >= len(selected_val[item_type][entry_pair][depend_value]): new_value = self.create_new_index(item_type) self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value) else: while n < len( selected_val[item_type][entry_pair][depend_value]): new_value = list(selected_val[item_type][entry_pair] [depend_value])[n] new_animation_name = self.ean.animations[new_value].name \ if item_type == Animation and new_value < len(self.ean.animations) else '' if item_type != Animation or not any( word in new_animation_name for word in BLACKLISTED_WORDS) \ or new_animation_name.endswith(entry_name): if not new_animation_name.endswith( entry_name) and self.find_conflict( item_type, entry_pair, depend_value, selected_data, new_value): new_value = self.create_new_index(item_type) self.changed_value_message( entry_index, changed_values, item_type, old_value, new_value) else: self.changed_value_message( entry_index, changed_values, item_type, old_value, new_value, False) break n += 1 else: new_value = self.create_new_index(item_type) self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value) else: new_value = self.create_new_index(item_type) self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value) # Copy EAN/BDM entries if not self.copy_index(item_type, old_value, new_value): return False self.links[item_type][entry_pair][depend_value][ old_value] = new_value n += 1 return True def on_paste(self, _): if not self.parent.copied: with wx.MessageDialog( self, f'No entries are copied from the right panel to Paste' ) as dlg: dlg.ShowModal() return selected = self.entry_list.GetSelections() if not selected: with wx.MessageDialog( self, f'No entries are selected to Paste onto') as dlg: dlg.ShowModal() return copied = pickle.loads(self.parent.copied) # Cut length of selected to match copied copy_length = len(copied) selected_length = len(selected) if selected_length > copy_length: for item in selected[copy_length:]: self.entry_list.Unselect(item) selected = selected[:copy_length] item = selected[-1] self.entry_list.Select(item) # Increase length to match selected for n in range(copy_length - selected_length): item = self.entry_list.GetNextItem(item) if not item.IsOk(): with wx.MessageDialog( self, f'Not enough entries to paste over. Expected {copy_length}' ) as dlg: dlg.ShowModal() return self.entry_list.Select(item) selected.append(item) selected_data = [ self.entry_list.GetItemData(item) for item in selected ] # Warn about changing multiple entries if len(copied) > 1: msg = '' for n, copied_data in enumerate(copied): msg += f' * {selected_data[n].index} -> {copied_data.index}\n' with MultiMessageDialog( self, 'Are you sure you want to replace the following entries?', 'Warning', msg, wx.YES | wx.NO) as dlg: if dlg.ShowModal() != wx.ID_YES: return # Paste entries selected_values = [] copied_values = [] changed_values = defaultdict(list) for n, copied_data in enumerate(copied): selected_values.append( (selected_data[n].index, selected_data[n].get_static_values())) copied_values.append(copied_data.get_static_values()) for selected_val, copied_val in zip(selected_values, copied_values): # Example: # Item type: Animation # entry: Index # dependency: Type # depend_value: 5 # entry_values: {1, 2, 3} for item_type, v1 in copied_val.items(): if item_type not in [Animation, Hitbox, Camera]: continue for entry_pair, v2 in v1.items(): for depend_value, entry_values in v2.items(): # Skip if dependency isn't a character if item_type.dependencies[entry_pair][ depend_value] != 'Character': continue if not self.get_changed_values( changed_values, item_type, entry_pair, depend_value, entry_values, selected_val, selected_data): return # Finally copy BAC Entries for n, copied_data in enumerate(copied): entry = self.entry_list.GetItemData(selected[n]) entry.paste(copied_data, self.links) # Display message msg = f'Pasted {len(copied)} entry(s)' pub.sendMessage('set_status_bar', text=msg) with ChangedDialog(self, changed_values) as dlg: dlg.ShowModal() # changed_msg = '' # for item_type, values in changed_values.items(): # if item_type == Animation: # changed_msg += f'{self.code}.ean -> {self.parent.side_panel.code}.ean\n' # elif item_type == Hitbox: # changed_msg += f'{self.code}_PLAYER.bdm -> {self.parent.side_panel.code}_PLAYER.bdm\n' # elif item_type == Camera: # changed_msg += f'{self.code}.cam.ean -> {self.parent.side_panel.code}.cam.ean\n' # else: # raise(TypeError(f'Unsupported type: {item_type.__name__}')) # changed_msg += ''.join(sorted(list(zip(*values))[1])) # with MultiMessageDialog( # self, 'The following entries in the listed files have been changed:', msg, changed_msg, wx.OK) as dlg: # dlg.ShowModal() def on_add(self, _): if not self.parent.copied: with wx.MessageDialog( self, f'No entries are copied from the right panel to Add' ) as dlg: dlg.ShowModal() return copied = pickle.loads(self.parent.copied) selected_data = [item for item in copied] # Paste entries selected_values = [] copied_values = [] changed_values = defaultdict(list) for n, copied_data in enumerate(copied): selected_values.append( (selected_data[n].index, selected_data[n].get_static_values())) copied_values.append(copied_data.get_static_values()) # same code as in on_paste(), but it compares to the added entry (i really have no idea why this works..) for selected_val, copied_val in zip(selected_values, copied_values): for item_type, v1 in copied_val.items(): if item_type not in [Animation, Hitbox, Camera]: continue for entry_pair, v2 in v1.items(): for depend_value, entry_values in v2.items(): # Skip if dependency isn't a character if item_type.dependencies[entry_pair][ depend_value] != 'Character': continue if not self.get_changed_values( changed_values, item_type, entry_pair, depend_value, entry_values, selected_val, selected_data): return # Add BAC Entry for n, copied_data in enumerate(copied): # add new entries to the end so we don't override important CMN entries index = len(self.bac.entries) new_entry = Entry(self.bac, index) root = self.entry_list.GetRootItem() new_entry.paste(copied_data, self.links) self.bac.entries.append(new_entry) self.entry_list.AppendItem( root, f'{new_entry.index}: {KNOWN_ENTRIES.get(new_entry.index, "Unknown")}', data=new_entry) # Display message msg = f'Added {len(copied)} entry(s)' pub.sendMessage('set_status_bar', text=msg) with ChangedDialog(self, changed_values) as dlg: dlg.ShowModal()
class MainFrame(wx.Frame): """ This is the main frame of the program, into which the tree is put Functions that start with "On" are event handlers, which you can see because event is the second argument to the function """ def __init__(self, parent, root_path = os.path.expanduser("~"), *args, **kwargs): size = (800, 800) #Default size # If preferences.json exists, load it and use it to specify the window size pref_path = os.path.join(app_root_path, 'preferences.json') if os.path.exists(pref_path): with open(pref_path,'r') as fp: options = json.load(fp) if 'size' in options: size = options['size'] wx.Frame.__init__(self, parent, title='Winder', *args, size = size, **kwargs) self.make_menu_bar() vsizer = wx.BoxSizer(wx.VERTICAL) self.tree = TreeListCtrl(self, -1, style = wx.dataview.TL_MULTIPLE | wx.dataview.TL_CHECKBOX ) self.isz = (16,16) il = wx.ImageList(*self.isz) self.fldridx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, self.isz)) self.fldropenidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_OTHER, self.isz)) self.fileidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, self.isz)) self.tree.SetImageList(il) self.il = il self.root_path = root_path self.build_tree() # Set the acceleratators to manually intercept some keystrokes self.set_accelerators() vsizer.Add(self.tree, 1, wx.EXPAND) self.SetSizer(vsizer) self.Bind(wx.EVT_CLOSE, self.OnClose) def build_tree(self): self.tree.ClearColumns() self.tree.DeleteAllItems() # create some columns self.tree.AppendColumn("Main column", align = wx.ALIGN_RIGHT, flags = wx.COL_SORTABLE|wx.COL_RESIZABLE) self.tree.AppendColumn("Size (B)", width = wx.COL_WIDTH_AUTOSIZE, align = wx.ALIGN_RIGHT, flags = wx.COL_SORTABLE|wx.COL_RESIZABLE) self.tree.AppendColumn("Day", width = wx.COL_WIDTH_AUTOSIZE, align = wx.ALIGN_RIGHT, flags = wx.COL_SORTABLE|wx.COL_RESIZABLE) self.tree.AppendColumn("Time", width = wx.COL_WIDTH_AUTOSIZE, align = wx.ALIGN_RIGHT, flags = wx.COL_SORTABLE|wx.COL_RESIZABLE) self.tree.AppendColumn("", width = 20, align = wx.ALIGN_RIGHT, flags = wx.COL_SORTABLE|wx.COL_RESIZABLE) self.root = self.tree.AppendItem(self.tree.GetRootItem(), self.root_path, self.fldridx, self.fldropenidx) self.tree.GetView().Bind(wx.EVT_KEY_DOWN, self.OnTreeKeyPress) self.tree.GetView().Bind(wx.EVT_CHAR, self.OnTreeChar) self.Bind(wx.dataview.EVT_TREELIST_ITEM_ACTIVATED, self.OnTreeDoubleClick, self.tree) self.tree.GetDataView().Bind(wx.dataview.EVT_DATAVIEW_COLUMN_HEADER_CLICK, self.OnHeaderClick) self.Bind(wx.dataview.EVT_TREELIST_ITEM_EXPANDING, self.OnExpandTreeListLeaf, self.tree) self.populate_tree(self.root, self.root_path) self.tree.SetItemComparator(comparator) self.tree.Expand(self.root) for col in [col_size, col_day, col_time]: self.tree.SetColumnWidth(col, self.tree.GetColumnWidth(col) + 5) def populate_tree(self, parent, root_path, recurse = True): """ Lazily populate the TreeListCtrl for given item parent(TreeListItem) : The parent node in the tree root_path(str) : The absolute path in the OS to the recurse(bool) : Keep going deeper into the tree """ day_fmt = " %m-%d-%y " time_fmt = " %I:%M:%S %p " # Don't add again children if parent item is already expanded # But DO recurse into directories contained by the parent directory data = self.tree.GetItemData(parent) if data is None or data.expanded == False: # We are going to populate! # Get the directories and files contained in this folder dirs, files = dirs_and_files(root_path) for dirname in dirs: child = self.tree.AppendItem(parent, dirname, self.fldridx, self.fldropenidx) self.tree.SetItemText(child, col_size, "") try: complete_path = os.path.join(root_path, dirname) mtime = time.localtime(os.path.getmtime(complete_path)) except PermissionError: continue self.tree.SetItemText(child, col_day, str(time.strftime(day_fmt, mtime))) self.tree.SetItemText(child, col_time, str(time.strftime(time_fmt, mtime))) for file in files: child = self.tree.AppendItem(parent, file, self.fileidx, self.fileidx) complete_path = os.path.join(root_path, file) filesize_kb = os.path.getsize(complete_path) self.tree.SetItemText(child, col_size, '{:,}'.format(filesize_kb)) mtime = time.localtime(os.path.getmtime(complete_path)) self.tree.SetItemText(child, col_day, str(time.strftime(day_fmt, mtime))) self.tree.SetItemText(child, col_time, str(time.strftime(time_fmt, mtime))) # The top level has been populated if needed, now recurse into # directories and populate them one more level if recurse: # Visit all the children of the parent item = self.tree.GetFirstChild(parent) while item.IsOk(): if self.ItemIsDirectory(item): # Go down one more level self.populate_tree(item, self.ItemToAbsPath(item), recurse = False) # Go to next sibling in this directory item = self.tree.GetNextSibling(item) # Set the flag telling you that the parent has been populated data = NodeData() data.expanded = True self.tree.SetItemData(parent, data) def refresh_tree(self, parent, root_path, recurse = True): """ Lazily refresh the TreeListCtrl for given item parent(TreeListItem) : The parent node in the tree root_path(str) : The absolute path in the OS to the recurse(bool) : Keep going deeper into the tree """ day_fmt = " %m-%d-%y " time_fmt = " %I:%M:%S %p " # We are going to populate! # Get the directories and files contained in this folder dirs, files = dirs_and_files(root_path) abspaths = [os.path.join(root_path, f) for f in files] + [os.path.join(root_path, d) for d in dirs] # Current children of this node item = self.tree.GetFirstChild(parent) children = [] while item.IsOk(): # Add this child children.append(item) # Go to next sibling in this directory item = self.tree.GetNextSibling(item) # First remove things that no longer exist for child in children: if self.ItemToAbsPath(child) not in abspaths: self.tree.DeleteItem(child) # Current children of this node item = self.tree.GetFirstChild(parent) children = [] while item.IsOk(): # Add this child children.append(item) # Go to next sibling in this directory item = self.tree.GetNextSibling(item) children_paths = [self.ItemToAbsPath(child) for child in children] # Then add things that are new for dirname in dirs: complete_path = os.path.join(root_path, dirname) try: mtime = time.localtime(os.path.getmtime(complete_path)) except PermissionError: continue if complete_path not in children_paths: child = self.tree.AppendItem(parent, dirname, self.fldridx, self.fldropenidx) else: child = children[children_paths.index(complete_path)] self.tree.SetItemText(child, col_size, "") self.tree.SetItemText(child, col_day, str(time.strftime(day_fmt, mtime))) self.tree.SetItemText(child, col_time, str(time.strftime(time_fmt, mtime))) for file in files: complete_path = os.path.join(root_path, file) if complete_path not in children_paths: child = self.tree.AppendItem(parent, file, self.fileidx, self.fileidx) else: child = children[children_paths.index(complete_path)] filesize_kb = os.path.getsize(complete_path) self.tree.SetItemText(child, col_size, '{:,}'.format(filesize_kb)) mtime = time.localtime(os.path.getmtime(complete_path)) self.tree.SetItemText(child, col_day, str(time.strftime(day_fmt, mtime))) self.tree.SetItemText(child, col_time, str(time.strftime(time_fmt, mtime))) # The top level has been populated if needed, now recurse into # directories and populate them one more level if recurse: # Visit all the children of the parent item = self.tree.GetFirstChild(parent) while item.IsOk(): if self.ItemIsDirectory(item): # Go down one more level self.populate_tree(item, self.ItemToAbsPath(item), recurse = False) # Go to next sibling in this directory item = self.tree.GetNextSibling(item) # Set the flag telling you that the parent has been populated data = NodeData() data.expanded = True self.tree.SetItemData(parent, data) def OnHeaderClick(self, event): sorted, col, ascendingOrder = self.tree.GetSortColumn() if event.GetColumn() == col and sorted and ascendingOrder == True: event.Veto() else: event.Skip() def ItemToAbsPath(self, item): """ Build the absolute path to the item by walking back up the tree""" parts = [] while item.IsOk(): # Get the next part part = self.tree.GetItemText(item, col_tree) if part: # Prepend this part parts.insert(0, part) # Walk up the tree one level item = self.tree.GetItemParent(item) path = os.path.sep.join(parts) return path def ItemIsDirectory(self, item): return os.path.isdir(self.ItemToAbsPath(item)) def OnExpandTreeListLeaf(self, event): items = self.tree.GetSelections() if items: fname = self.tree.GetItemText(items[0], col_tree) self.populate_tree(items[0], self.ItemToAbsPath(items[0])) def OnTreeDoubleClick(self, event): if 'win' in sys.platform: items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("Must select one thing in tree") return path = self.ItemToAbsPath(items[0]) if path.upper().endswith('.EXE'): self.OnRunExecutable(event) else: logging.log('Cannot double-click launch on non-windows platform') def OnTreeChar(self, event = None): keycode = event.GetKeyCode() # Toggle the value in the first column def toggle(items): for s in items: if self.tree.GetCheckedState(s) == wx.CHK_CHECKED: self.tree.CheckItem(s, wx.CHK_UNCHECKED) else: self.tree.CheckItem(s, wx.CHK_CHECKED) if keycode == ord('*'): if len(self.tree.GetSelections()) != 1: wx.LogMessage("Can only select one item for glob select with *") item = self.tree.GetSelections()[0] # Get the file extension of the selected entity root_ext = os.path.splitext(self.tree.GetItemText(item, col_tree))[1] if self.ItemIsDirectory(item): wx.LogMessage("Can only apply glob select to files") return # Rewind to the first sibling parent = self.tree.GetItemParent(item) item = self.tree.GetFirstChild(parent) while item.IsOk(): fname = self.tree.GetItemText(item, col_tree) ext = os.path.splitext(fname)[1] if ext and ext.upper() == root_ext.upper() and not self.ItemIsDirectory(item): toggle([item]) item = self.tree.GetNextSibling(item) else: event.Skip() def OnTreeKeyPress(self, event = None): # Toggle the value in the first column def toggle(items): for s in items: if self.tree.GetCheckedState(s) == wx.CHK_CHECKED: self.tree.CheckItem(s, wx.CHK_UNCHECKED) else: self.tree.CheckItem(s, wx.CHK_CHECKED) keycode = event.GetUnicodeKey() if keycode == wx.WXK_SPACE or keycode == ' ': if len(self.tree.GetSelections()) > 1: for s in self.tree.GetSelections(): if self.ItemIsDirectory(s): wx.LogMessage("Cannot use directory as part of multi-select") return toggle(self.tree.GetSelections()) else: item = self.tree.GetSelections()[0] # Only one selection for sure if not self.ItemIsDirectory(item): toggle(self.tree.GetSelections()) else: # If a directory, select all files (but not subdirectories) in the directory item = self.tree.GetFirstChild(item) while item.IsOk(): if self.ItemIsDirectory(item): item = self.tree.GetNextSibling(item) continue else: toggle([item]) item = self.tree.GetNextSibling(item) elif event.GetKeyCode() == wx.WXK_ESCAPE: self.UncheckAllItems() event.Skip() else: #print keycode, type(keycode), event.GetKeyCode(), wx.WXK_ESCAPE event.Skip() def set_accelerators(self): # See http://www.blog.pythonlibrary.org/2010/12/02/wxpython-keyboard-shortcuts-accelerators/ accelEntries = [] for key in ['F', 'D', 'A', 'X', 'R', 'O']: eventId = wx.NewId() accelEntries.append( (wx.ACCEL_NORMAL, ord(key), eventId) ) self.Bind(wx.EVT_MENU, lambda evt, _id=eventId, _key = key: self.OnManualAccelerator(evt, _id, _key), id=eventId) accelTable = wx.AcceleratorTable(accelEntries) self.SetAcceleratorTable(accelTable ) def OnManualAccelerator(self, event, id, key): #for menu, label in self.menuBar.GetMenus(): # print menu, label, menu.GetWindow().GetScreenPosition(), menu.GetWindow().GetScreenPosition() #print self.menuBar.GetScreenPosition() treepos = self.tree.GetScreenPosition() #print treepos coords = self.ScreenToClient(treepos) if key == 'F': mnu = self.make_file_menu(self) elif key == 'D': mnu = self.make_directory_menu(self) elif key == 'A': mnu = self.make_applications_menu(self) elif key == 'O': mnu = self.make_options_menu(self) elif key == 'R': self.OnRunExecutable(self) return elif key == 'X': self.OnClose() return else: ErrorMessage("Not able to process keystroke " + key) self.PopupMenu(mnu, wx.Point(coords.x, coords.y)) def OnWriteDirectory(self, event): item = self.tree.GetFirstChild(self.root) text = [] while item.IsOk(): if self.tree.IsVisible(item): name = self.tree.GetItemText(item, col_tree) size = self.tree.GetItemText(item, col_size) day = self.tree.GetItemText(item, col_day) time = self.tree.GetItemText(item, col_time) line = "{name:s}\t{size:s}\t{day:s}\t{time:s}".format(**locals()) text.append(line) item = self.tree.GetNext(item) dlg = wx.FileDialog( self, message="Select output file", defaultDir=os.getcwd(), defaultFile="", style=wx.SAVE ) if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() with open(path, 'w') as fp: fp.write('\n'.join(text)) dlg.Destroy() def UncheckAllItems(self): item = self.root items = [] while item.IsOk(): self.tree.CheckItem(item, wx.CHK_UNCHECKED) item = self.tree.GetNextItem(item) return items def GetMarkedItems(self): item = self.tree.GetFirstChild(self.root) items = [] while item.IsOk(): if self.tree.GetCheckedState(item) == wx.CHK_CHECKED: items.append(item) item = self.tree.GetNextItem(item) return items def OnRemoveDirectory(self, event): items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("Must select one thing in tree") return parent = self.tree.GetItemParent(items[0]) path = self.ItemToAbsPath(items[0]) if not self.ItemIsDirectory(items[0]): ErrorMessage("Selected entity is not a directory") return try: os.rmdir(path) except OSError: ErrorMessage("Selected directory is not empty") self.refresh_tree(parent, self.ItemToAbsPath(parent)) def OnRemoveFile(self, event): items = self.GetMarkedItems() if len(items) == 0: ErrorMessage("Must select at least one file in tree") return if any([self.ItemIsDirectory(item) for item in items]): ErrorMessage("Cannot remove directories") return parents = [] for item in items: parents.append(self.tree.GetItemParent(item)) path = self.ItemToAbsPath(item) try: os.remove(path) except OSError: ErrorMessage("Cannot remove file" + path) for parent in set(parents): self.refresh_tree(parent, self.ItemToAbsPath(parent)) def OnRenameFile(self, event): items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("Must select one thing in tree") return fname = self.tree.GetItemText(items[0],col_tree) parent = self.tree.GetItemParent(items[0]) path = self.ItemToAbsPath(items[0]) if self.ItemIsDirectory(items[0]): ErrorMessage("Selected entity is not a file") return dlg = wx.TextEntryDialog( self, 'New file name', 'Was ' + path, fname) if dlg.ShowModal() == wx.ID_OK: try: os.rename(path, os.path.join(os.path.dirname(path), dlg.GetValue())) self.refresh_tree(parent, self.ItemToAbsPath(parent)) except OSError: ErrorMessage("Rename target already exists") dlg.Destroy() def OnRenameDirectory(self, event): items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("Must select one thing in tree") return fname = self.tree.GetItemText(items[0],col_tree) parent = self.tree.GetItemParent(items[0]) path = self.ItemToAbsPath(items[0]) if not self.ItemIsDirectory(items[0]): ErrorMessage("Selected entity is not a directory") return dlg = wx.TextEntryDialog( self, 'New directory name', 'Was' + path, fname) if dlg.ShowModal() == wx.ID_OK: try: os.rename(path, os.path.join(os.path.dirname(path),dlg.GetValue())) self.refresh_tree(parent, self.ItemToAbsPath(parent)) except OSError: ErrorMessage("Rename target already exists") dlg.Destroy() def OnNewDirectory(self, event): """ Event handler to make a new directory """ items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("Must select one thing in tree") path = self.ItemToAbsPath(items[0]) if os.path.isdir(path): # Put it in the directory selected root = path parent = items[0] else: # Put it in the containing folder root = os.path.dirname(path) parent = self.tree.GetItemParent(items[0]) dlg = wx.TextEntryDialog( self, 'New directory to be added to ' + root, 'New directory', '') if dlg.ShowModal() == wx.ID_OK: fname = os.path.join(root, dlg.GetValue()) if os.path.exists(fname): ErrorMessage("Cannot create directory [{fname:s}] as it already exists".format(fname = fname)) else: os.mkdir(fname) self.refresh_tree(parent, self.ItemToAbsPath(parent)) dlg.Destroy() def OnFileNew(self, event): """ Event handler to make a new file """ items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("Must select one thing in tree") path = self.ItemToAbsPath(items[0]) if os.path.isdir(path): # Put it in the directory selected root = path parent = items[0] else: # Put it in the containing folder root = os.path.dirname(path) parent = self.tree.GetItemParent(items[0]) dlg = wx.TextEntryDialog( self, 'New file to be added to ' + root, 'New directory', '') if dlg.ShowModal() == wx.ID_OK: fname = os.path.join(root, dlg.GetValue()) else: fname = None dlg.Destroy() if fname is None: return if os.path.exists(fname): ErrorMessage("Cannot create file [{fname:s}] as it already exists".format(fname = fname)) # Make the file with open(fname, 'w'): pass self.refresh_tree(parent, self.ItemToAbsPath(parent)) def OnLoadBookmark(self, event, path): self.root_path = path self.build_tree() def OnChangePath(self, event): # In this case we include a "New directory" button. dlg = wx.DirDialog(self, "Choose a directory:", style=wx.DD_DEFAULT_STYLE #| wx.DD_DIR_MUST_EXIST #| wx.DD_CHANGE_DIR ) # If the user selects OK, then we process the dialog's data. # This is done by getting the path data from the dialog - BEFORE # we destroy it. if dlg.ShowModal() == wx.ID_OK: self.root_path = dlg.GetPath() self.build_tree() # Only destroy a dialog after you're done with it. dlg.Destroy() def OnChangeDrive(self, event): if 'win' in sys.platform: try: import win32api except ImportError: ErrorMessage("Unable to import win32api") return else: print('no such thing as drive on this platform') drives = win32api.GetLogicalDriveStrings() drives = drives.split('\000')[:-1] dlg = wx.SingleChoiceDialog( self, 'Select Working Drive:', 'Drive?', drives, wx.CHOICEDLG_STYLE ) if dlg.ShowModal() == wx.ID_OK: self.root_path = dlg.GetStringSelection() self.build_tree() dlg.Destroy() def OnChar(self, event): keycode = event.GetUnicodeKey() if keycode != wx.WXK_NONE: # It's a printable character wx.LogMessage("You pressed '%c'"%keycode) else: wx.LogMessage("You pressed a non ASCII key '%c'"%keycode) def OnFileCopy(self, event = None): source_items = self.GetMarkedItems() if len(source_items) == 0: ErrorMessage("At least one file must be marked") return items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("One target directory must be selected") return if not self.ItemIsDirectory(items[0]): ErrorMessage("Target must be a directory") return new_dir = self.ItemToAbsPath(items[0]) # Prepare paths (to make sure we don't have collision) paths, parents = [], [] for item in source_items: parents.append(self.tree.GetItemParent(item)) old_path = self.ItemToAbsPath(item) old_fname = self.tree.GetItemText(item, col_tree) new_path = os.path.join(new_dir, old_fname) paths.append((old_path, new_path)) old, new = zip(*paths) if len(paths) != len(set(new)): ErrorMessage("At least two destination files have the same name") return if any([os.path.exists(n) for n in new]): dlg = wx.MessageDialog(self, 'Some output files will be over-written, Yes to continue and over-write', 'Over-write?', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION ) if dlg.ShowModal() == wx.ID_NO: dlg.Destroy() return # And here goes the copy for (old, new) in paths: shutil.copy2(old, new) for parent in set(parents): self.refresh_tree(parent, self.ItemToAbsPath(parent)) def OnFileMove(self, event = None): source_items = self.GetMarkedItems() if len(source_items) == 0: ErrorMessage("At least one file must be marked") return items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("One target directory must be selected") return if not self.ItemIsDirectory(items[0]): ErrorMessage("Target must be a directory") return new_dir = self.ItemToAbsPath(items[0]) # Prepare paths (to make sure we don't have collision) paths, parents = [], [] for item in source_items: parents.append(self.tree.GetItemParent(item)) old_path = self.ItemToAbsPath(item) old_fname = self.tree.GetItemText(item, col_tree) new_path = os.path.join(new_dir, old_fname) paths.append((old_path, new_path)) old, new = zip(*paths) print(old, new) if len(paths) != len(set(new)): ErrorMessage("At least two destination files have the same name") return if any([os.path.exists(n) for n in new]): dlg = wx.MessageDialog(self, 'Some output files will be over-written, Yes to continue and over-write', 'Over-write?', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION ) if dlg.ShowModal() == wx.ID_NO: dlg.Destroy() return # And here goes the move for (old, new) in paths: shutil.move(old, new) for parent in set(parents): self.refresh_tree(parent, self.ItemToAbsPath(parent)) def OnClose(self, evnt = None): dlg = wx.MessageDialog(self, 'Quit?', 'Quit?', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION ) if dlg.ShowModal() == wx.ID_YES: pref_path = os.path.join(app_root_path, 'preferences.json') if os.path.exists(pref_path): with open(pref_path, 'r') as fp: options = json.load(fp) options['size'] = list(self.GetSize()) with open(pref_path, 'w') as fp: json_string = json.dumps(options) fp.write(json_string) self.Destroy() dlg.Destroy() def OnModifyBookmarks(self, event = None): dlg = ModifyBookmarksDialog() dlg.ShowModal() dlg.Destroy() def OnModifyApplications(self, event = None): dlg = ModifyApplicationsDialog() dlg.ShowModal() dlg.Destroy() def OnApplicationLaunch(self, event, menu): items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("One file must be selected") return file_path = os.path.normpath(self.ItemToAbsPath(items[0])) name = menu.GetLabel() app_path = None for app in self.apps: if app['name'].replace('&','') == name: # Remove accelerator symbol app_path = app['path'] break if app_path is None: ErrorMessage("Could not match app name: " + name) return # See http://stackoverflow.com/a/12144179/1360263 # define a command that starts new terminal if platform.system() == "Windows": if hasattr(sys, 'frozen'): new_window_command = "cmd /C " else: new_window_command = "" else: #XXX this can be made more portable new_window_command = "x-terminal-emulator -e".split() command = ' '.join(['"' + p + '"' for p in [app_path, file_path]]) #if hasattr(sys, 'frozen'): # call_string = new_window_command + '" ' + command + ' "' #else: call_string = command print('calling', call_string) subprocess.Popen(call_string, cwd = os.path.dirname(file_path), shell = False) def OnRunExecutable(self, event): items = self.tree.GetSelections() if len(items) != 1: ErrorMessage("One file must be selected") return exe_path = self.ItemToAbsPath(items[0]) # See http://stackoverflow.com/a/12144179/1360263 # define a command that starts new terminal if platform.system() == "Windows": new_window_command = "cmd.exe /c " else: #XXX this can be made more portable new_window_command = "x-terminal-emulator -e".split() dlg = RunExecutableDialog(os.path.dirname(exe_path), False) if dlg.ShowModal() == wx.ID_OK: path, new_console, arguments = dlg.get() dlg.Destroy() # Check if the output path is invalid if path is None and new_console is None: return else: dlg.Destroy() return if arguments: arguments = ' '.join(['"' + arg + '"' for arg in arguments if arg]) else: arguments = '' exe_path = '"' + exe_path + '"' call_string = new_window_command + '" ' + exe_path + arguments + ' "' if new_console: creationflags = CREATE_NEW_CONSOLE else: creationflags = 0 subprocess.Popen(call_string, cwd = path, shell = False, creationflags=creationflags) def make_options_menu(self, parent): """ As the name implies, construct the options menu and return it """ def set_bookmarks(): book_path = os.path.join(app_root_path, 'bookmarks.json') if os.path.exists(book_path): bookmarks = json.load(open(book_path, 'r')) for i,mark in enumerate(bookmarks): menu.BookMarks[i].SetText('&' + str(i+1)+ ': ' + mark['name']) parent.Bind(wx.EVT_MENU, lambda evt, path = mark['path']: parent.OnLoadBookmark(evt, path), menu.BookMarks[i]) menu = wx.Menu() menu.Refresh = wx.MenuItem(menu, -1, "&Refresh\tF5", "", wx.ITEM_NORMAL) menu.sep = wx.MenuItem(menu, -1, "", "", wx.ITEM_SEPARATOR) menu.book = wx.MenuItem(menu, -1, "**Bookmarks**", "", wx.ITEM_NORMAL) menu.BookMarks = [] for i in range(10): menu.BookMarks.append(wx.MenuItem(menu, -1, " ", " ", wx.ITEM_NORMAL)) menu.ModifyBookmarks = wx.MenuItem(menu, -1, "Modify...", "", wx.ITEM_NORMAL) for el in [menu.Refresh, menu.sep, menu.book] + menu.BookMarks + [menu.ModifyBookmarks]: if wx_phoenix: menu.Append(el) else: menu.AppendItem(el) parent.Bind(wx.EVT_MENU, lambda evt: self.build_tree(), menu.Refresh) parent.Bind(wx.EVT_MENU, parent.OnModifyBookmarks, menu.ModifyBookmarks) set_bookmarks() return menu def make_directory_menu(self, parent): """ As the name implies, construct the directory menu and return it """ menu = wx.Menu() menu.Drive = wx.MenuItem(menu, -1, "Change &Drive", "", wx.ITEM_NORMAL) menu.Path = wx.MenuItem(menu, -1, "Change &Path", "", wx.ITEM_NORMAL) menu.Sort = wx.MenuItem(menu, -1, "&Sort (WIP)", "", wx.ITEM_NORMAL) menu.New = wx.MenuItem(menu, -1, "&New", "", wx.ITEM_NORMAL) menu.Erase = wx.MenuItem(menu, -1, "&Erase", "", wx.ITEM_NORMAL) menu.Rename = wx.MenuItem(menu, -1, "&Rename", "", wx.ITEM_NORMAL) menu.Write = wx.MenuItem(menu, -1, "&Write", "", wx.ITEM_NORMAL) for el in [menu.Drive, menu.Path, menu.Sort, menu.New, menu.Erase, menu.Rename, menu.Write]: if wx_phoenix: menu.Append(el) else: menu.AppendItem(el) parent.Bind(wx.EVT_MENU, parent.OnNewDirectory, menu.New) parent.Bind(wx.EVT_MENU, parent.OnRemoveDirectory, menu.Erase) parent.Bind(wx.EVT_MENU, parent.OnRenameDirectory, menu.Rename) parent.Bind(wx.EVT_MENU, parent.OnChangePath, menu.Path) parent.Bind(wx.EVT_MENU, parent.OnChangeDrive, menu.Drive) parent.Bind(wx.EVT_MENU, parent.OnWriteDirectory, menu.Write) return menu def make_file_menu(self, parent): """ As the name implies, construct the file menu and return it """ menu = wx.Menu() menu.Find = wx.MenuItem(menu, -1, "&Find (WIP)", "", wx.ITEM_NORMAL) menu.Copy = wx.MenuItem(menu, -1, "&Copy", "", wx.ITEM_NORMAL) menu.Rename = wx.MenuItem(menu, -1, "&Rename", "", wx.ITEM_NORMAL) menu.Move = wx.MenuItem(menu, -1, "&Move", "", wx.ITEM_NORMAL) menu.Erase = wx.MenuItem(menu, -1, "&Erase", "", wx.ITEM_NORMAL) menu.New = wx.MenuItem(menu, -1, "&New", "", wx.ITEM_NORMAL) menu.Attrib = wx.MenuItem(menu, -1, "&Attrib (WIP)", "", wx.ITEM_NORMAL) for el in [menu.Find, menu.Copy, menu.Rename, menu.Move, menu.New, menu.Erase, menu.Attrib]: if wx_phoenix: menu.Append(el) else: menu.AppendItem(el) parent.Bind(wx.EVT_MENU, parent.OnFileCopy, menu.Copy) parent.Bind(wx.EVT_MENU, parent.OnFileMove, menu.Move) parent.Bind(wx.EVT_MENU, parent.OnFileNew, menu.New) parent.Bind(wx.EVT_MENU, parent.OnRemoveFile, menu.Erase) parent.Bind(wx.EVT_MENU, parent.OnRenameFile, menu.Rename) return menu def make_applications_menu(self, parent): """ As the name implies, construct the applications menu and return it """ menu = wx.Menu() apps_json_path = os.path.join(app_root_path, 'apps.json') if os.path.exists(apps_json_path): with open(apps_json_path, 'r') as fp: apps = json.load(fp) for app in apps: el = wx.MenuItem(menu, -1, app['name'], "", wx.ITEM_NORMAL) if wx_phoenix: menu.Append(el) else: menu.AppendItem(el) parent.Bind(wx.EVT_MENU, lambda evt, menu = el: parent.OnApplicationLaunch(evt, menu), el) self.apps = apps menu.Append(wx.MenuItem(menu, -1, "", "", wx.ITEM_SEPARATOR)) menu.ModifyApplications = wx.MenuItem(menu, -1, "Modify...", "", wx.ITEM_NORMAL) menu.Append(menu.ModifyApplications) parent.Bind(wx.EVT_MENU, parent.OnModifyApplications, menu.ModifyApplications) return menu def make_menu_bar(self): """ As the name implies, construct the menus and attach them all to the menubar """ # Menu Bar self.menuBar = wx.MenuBar() # Build Run menu self.menuRun = wx.Menu() self.menuRunRun = wx.MenuItem(self.menuRun, -1, "&Run highlighted program", "", wx.ITEM_NORMAL) if wx_phoenix: self.menuRun.Append(self.menuRunRun) else: self.menuRun.AppendItem(self.menuRunRun) self.menuRunId = self.menuBar.Append(self.menuRun, "&Run") self.Bind(wx.EVT_MENU, lambda x, evt: x, self.menuRunRun) # Build File menu self.menuFile = self.make_file_menu(self) self.menuBar.Append(self.menuFile, "&File") # Build Directory menu self.menuDirectory = self.make_directory_menu(self) self.menuBar.Append(self.menuDirectory, "&Directory") # Build Application menu (empty to start) self.menuApplication = self.make_applications_menu(self) self.menuBar.Append(self.menuApplication, "&Application") # Build options menu self.menuOptions = self.make_options_menu(self) self.menuBar.Append(self.menuOptions, "&Options") # Exit menu self.menuExit = wx.Menu() self.menuExitExit = wx.MenuItem(self.menuExit, -1, "E&xit", "", wx.ITEM_NORMAL) if wx_phoenix: self.menuExit.Append(self.menuExitExit) else: self.menuExit.AppendItem(self.menuExitExit) self.menuBar.Append(self.menuExit, 'E&xit') #Actually set it self.SetMenuBar(self.menuBar)
class SidePanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) self.code = '' self.bac = None self.bdm = None self.ean = None self.cam_ean = None self.parent = parent self.dirname = '' # Name self.name = wx.StaticText(self, -1, '(No file loaded)') self.font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) self.name.SetFont(self.font) # Buttons self.open = wx.Button(self, wx.ID_OPEN, "Load") self.copy = wx.Button(self, wx.ID_COPY, "Copy") self.copy.Disable() # Entry List self.entry_list = TreeListCtrl(self, style=TL_MULTIPLE) self.entry_list.AppendColumn("BAC Entry") self.entry_list.AppendColumn("Copied", width=64) self.entry_list.Bind(EVT_TREELIST_ITEM_CONTEXT_MENU, self.on_right_click) # self.cdo = wx.CustomDataObject("BDMEntry") self.Bind(wx.EVT_BUTTON, self.on_open, id=wx.ID_OPEN) self.Bind(wx.EVT_BUTTON, self.on_copy, id=wx.ID_COPY) self.Bind(wx.EVT_MENU, self.on_copy, id=wx.ID_COPY) accelerator_table = wx.AcceleratorTable([ (wx.ACCEL_CTRL, ord('c'), wx.ID_COPY), ]) self.entry_list.SetAcceleratorTable(accelerator_table) self.SetDropTarget(FileDropTarget(self, "load_side_moveset")) # Button Sizer button_sizer = wx.BoxSizer(wx.HORIZONTAL) button_sizer.Add(self.open) button_sizer.AddSpacer(5) button_sizer.Add(self.copy) # Use some sizers to see layout options sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.name, 0, wx.CENTER) sizer.Add(button_sizer) sizer.Add(self.entry_list, 1, wx.ALL | wx.EXPAND, 10) # Layout sizers self.SetSizer(sizer) self.SetAutoLayout(1) def on_open(self, _): pub.sendMessage('open_side_moveset') def build_tree(self): self.entry_list.DeleteAllItems() root = self.entry_list.GetRootItem() for i, entry in enumerate(self.bac.entries): if not entry.sub_entries: continue self.entry_list.AppendItem( root, f'{entry.index}: {KNOWN_ENTRIES.get(entry.index, "Unknown")}', data=entry) self.copy.Enable() self.parent.copied = None def on_right_click(self, _): selected = self.entry_list.GetSelections() if not selected: return menu = wx.Menu() menu.Append(wx.ID_COPY) self.PopupMenu(menu) menu.Destroy() def on_copy(self, _): selected = self.entry_list.GetSelections() if not selected: return # Deselect all item = self.entry_list.GetFirstItem() while item.IsOk(): self.entry_list.SetItemText(item, 1, '') item = self.entry_list.GetNextItem(item) # Check and add to copied copied = [] for item in selected: copied.append(self.entry_list.GetItemData(item)) self.entry_list.SetItemText(item, 1, CHECK) self.parent.copied = pickle.dumps(copied) pub.sendMessage('set_status_bar', text=f'Copied {len(selected)} entries')
class ParameterView(wx.Panel): title = 'Parameters' default_size = (640, 500) def __init__(self, *args, **kw): wx.Panel.__init__(self, *args, **kw) #sizers vbox = wx.BoxSizer(wx.VERTICAL) text_hbox = wx.BoxSizer(wx.HORIZONTAL) self.tree = TreeListCtrl( self, -1, style=wx.TR_DEFAULT_STYLE | wx.TR_HAS_BUTTONS | wx.TR_TWIST_BUTTONS | wx.TR_ROW_LINES #| wx.TR_COLUMN_LINES | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT) # CRUFT: wx3 AddColumn => wx4 AppendColumn if phoenix: self.tree.AddColumn = self.tree.AppendColumn self.tree.GetItemPyData = self.tree.GetItemData self.tree.SetItemPyData = self.tree.SetItemData self.tree.GetNext = self.tree.GetNextItem self.tree.ExpandAll = self.tree.Expand # Create columns. self.tree.AddColumn("Model") self.tree.AddColumn("Parameter") self.tree.AddColumn("Value") self.tree.AddColumn("Minimum") self.tree.AddColumn("Maximum") self.tree.AddColumn("Fit?") # Align the textctrl box with treelistctrl. self.tree.SetColumnWidth(0, 180) self.tree.SetColumnWidth(1, 150) self.tree.SetColumnWidth(2, 73) self.tree.SetColumnWidth(3, 73) self.tree.SetColumnWidth(4, 73) self.tree.SetColumnWidth(5, 40) # Determine which colunms are editable. if not phoenix: # CRUFT: wx4 needs to witch to DataViewCtrl self.tree.SetMainColumn(0) # the one with the tree in it... self.tree.SetColumnEditable(0, False) self.tree.SetColumnEditable(1, False) self.tree.SetColumnEditable(2, True) self.tree.SetColumnEditable(3, True) self.tree.SetColumnEditable(4, True) self.tree.SetColumnEditable(5, False) self.tree.GetMainWindow().Bind(wx.EVT_RIGHT_UP, self.OnRightUp) self.tree.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.OnEndEdit) ''' self.tree.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP,self.OnTreeTooltip) wx.EVT_MOTION(self.tree, self.OnMouseMotion) ''' vbox.Add(self.tree, 1, wx.EXPAND) self.SetSizer(vbox) self.SetAutoLayout(True) self._need_update_parameters = self._need_update_model = False self.Bind(wx.EVT_SHOW, self.OnShow) # ============= Signal bindings ========================= ''' def OnTreeTooltip(self, event): itemtext = self.tree.GetItemText(event.GetItem()) event.SetToolTip("This is a ToolTip for %s!" % itemtext) event.Skip() def OnMouseMotion(self, event): pos = event.GetPosition() item, flags, col = self.tree.HitTest(pos) if wx.TREE_HITTEST_ONITEMLABEL: self.tree.SetToolTipString("tool tip") else: self.tree.SetToolTipString("") event.Skip() ''' def OnShow(self, event): if not event.Show: return #print "showing parameter" if self._need_update_model: #print "-model update" self.update_model(self.model) elif self._need_update_parameters: #print "-parameter update" self.update_parameters(self.model) event.Skip() # ============ Operations on the model =============== def get_state(self): return self.model def set_state(self, state): self.set_model(state) def set_model(self, model): self.model = model self.update_model(model) def update_model(self, model): if self.model != model: return if not IS_MAC and not self.IsShown(): self._need_update_model = True else: self._need_update_model = self._need_update_parameters = False self._update_model() def update_parameters(self, model): if self.model != model: return if not IS_MAC and not self.IsShown(): self._need_update_parameters = True else: self._need_update_parameters = False self._update_tree_nodes() def _update_model(self): # Delete the previous tree (if any). self.tree.DeleteAllItems() if self.model is None: return parameters = self.model.model_parameters() # Add a root node. if phoenix: # CRUFT: wx 3/4 self.root = self.tree.GetRootItem() else: self.root = self.tree.AddRoot("Model") # Add nodes from our data set . self._add_tree_nodes(self.root, parameters) self._update_tree_nodes() self.tree.ExpandAll(self.root) def _add_tree_nodes(self, branch, nodes): if isinstance(nodes, dict) and nodes != {}: for k in sorted(nodes.keys()): child = self.tree.AppendItem(branch, k) self._add_tree_nodes(child, nodes[k]) elif ((isinstance(nodes, tuple) and nodes != ()) or (isinstance(nodes, list) and nodes != [])): for i, v in enumerate(nodes): child = self.tree.AppendItem(branch, '[%d]' % i) self._add_tree_nodes(child, v) elif isinstance(nodes, BaseParameter): self.tree.SetItemPyData(branch, nodes) def _update_tree_nodes(self): node = self.tree.GetRootItem() while node.IsOk(): self._set_leaf(node) node = self.tree.GetNext(node) def _set_leaf(self, branch): par = self.tree.GetItemPyData(branch) if par is None: return if par.fittable: if par.fixed: fitting_parameter = 'No' low, high = '', '' else: fitting_parameter = 'Yes' low, high = (str(v) for v in par.bounds.limits) else: fitting_parameter = '' low, high = '', '' if phoenix: # CRUFT: wx 3/4 self.tree.SetItemText(branch, 1, str(par.name)) self.tree.SetItemText(branch, 2, str(nice(par.value))) self.tree.SetItemText(branch, 3, low) self.tree.SetItemText(branch, 4, high) self.tree.SetItemText(branch, 5, fitting_parameter) else: self.tree.SetItemText(branch, str(par.name), 1) self.tree.SetItemText(branch, str(nice(par.value)), 2) self.tree.SetItemText(branch, low, 3) self.tree.SetItemText(branch, high, 4) self.tree.SetItemText(branch, fitting_parameter, 5) def OnRightUp(self, evt): pos = evt.GetPosition() branch, flags, column = self.tree.HitTest(pos) if column == 5: par = self.tree.GetItemPyData(branch) if par is None: return if par.fittable: fitting_parameter = self.tree.GetItemText(branch, column) if fitting_parameter == 'No': par.fixed = False fitting_parameter = 'Yes' low, high = (str(v) for v in par.bounds.limits) elif fitting_parameter == 'Yes': par.fixed = True fitting_parameter = 'No' low, high = '', '' self.tree.SetItemText(branch, low, 3) self.tree.SetItemText(branch, high, 4) self.tree.SetItemText(branch, fitting_parameter, 5) signal.update_model(model=self.model, dirty=False) def OnEndEdit(self, evt): item = self.tree.GetSelection() self.node_object = self.tree.GetItemPyData(evt.GetItem()) # TODO: Not an efficient way of updating values of Parameters # but it is hard to find out which column changed during edit # operation. This may be fixed in the future. wx.CallAfter(self.get_new_name, item, 1) wx.CallAfter(self.get_new_value, item, 2) wx.CallAfter(self.get_new_min, item, 3) wx.CallAfter(self.get_new_max, item, 4) def get_new_value(self, item, column): new_value = self.tree.GetItemText(item, column) # Send update message to other tabs/panels only if parameter value # is updated . if new_value != str(self.node_object.value): self.node_object.clip_set(float(new_value)) signal.update_parameters(model=self.model) def get_new_name(self, item, column): new_name = self.tree.GetItemText(item, column) # Send update message to other tabs/panels only if parameter name # is updated. if new_name != str(self.node_object.name): self.node_object.name = new_name signal.update_model(model=self.model, dirty=False) def get_new_min(self, item, column): low = self.tree.GetItemText(item, column) if low == '': return low = float(low) high = self.node_object.bounds.limits[1] # Send update message to other tabs/panels only if parameter min range # value is updated. if low != self.node_object.bounds.limits[0]: self.node_object.range(low, high) signal.update_model(model=self.model, dirty=False) def get_new_max(self, item, column): high = self.tree.GetItemText(item, column) if high == '': return low = self.node_object.bounds.limits[0] high = float(high) # Send update message to other tabs/panels only if parameter max range # value is updated. if high != self.node_object.bounds.limits[1]: self.node_object.range(low, high) signal.update_model(model=self.model, dirty=False)