├── requirements.txt ├── .gitignore ├── INSTALL ├── .github └── workflows │ └── ruff.yml ├── attribute_dialog.py ├── add_node_dialog.py ├── README.md ├── xml_viewer.py ├── utils.py ├── edit_dialog.py ├── editor_page.py ├── boom_xml_editor.py ├── boom_attribute_ed.py ├── test.xml ├── books.xml ├── boom_tree.py └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | pypubsub 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.wpu 2 | *.wpr 3 | *.pyc 4 | *.txt 5 | drafts/ 6 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | How to Install Boomslang 2 | 3 | Currently the only way to install Boomslang is to download the code from Github at https://github.com/driscollis/boomslang 4 | 5 | You will need the following installed: 6 | 7 | - Python 3.5+ 8 | - wxPython 4+ 9 | - lxml 3.8+ 10 | 11 | Assuming all of the above is installed, then all you need to do is download the code from Github and run main.py -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: [workflow_dispatch, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - name: Install Python 9 | uses: actions/setup-python@v4 10 | with: 11 | python-version: "3.12" 12 | - name: Install dependencies 13 | run: | 14 | python -m pip install --upgrade pip 15 | pip install ruff 16 | # Include `--format=github` to enable automatic inline annotations. 17 | - name: Run Ruff 18 | run: ruff check --output-format=github . 19 | continue-on-error: false 20 | - name: Run Ruff format 21 | run: ruff format --check . 22 | continue-on-error: false 23 | -------------------------------------------------------------------------------- /attribute_dialog.py: -------------------------------------------------------------------------------- 1 | from edit_dialog import EditDialog 2 | from pubsub import pub 3 | 4 | 5 | class AttributeDialog(EditDialog): 6 | """ 7 | Dialog class for adding attributes 8 | """ 9 | 10 | def on_save(self, event): 11 | """ 12 | Event handler that is called when the Save button is 13 | pressed. 14 | 15 | Updates the XML object with the new node element and 16 | tells the UI to update to display the new element 17 | before destroying the dialog 18 | """ 19 | attr = self.value_one.GetValue() 20 | value = self.value_two.GetValue() 21 | if attr: 22 | self.xml_obj.attrib[attr] = value 23 | pub.sendMessage("ui_updater_{}".format(self.page_id), xml_obj=self.xml_obj) 24 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 25 | else: 26 | # TODO - Show a dialog telling the user that there is no attr to save 27 | raise NotImplementedError 28 | 29 | self.Close() 30 | -------------------------------------------------------------------------------- /add_node_dialog.py: -------------------------------------------------------------------------------- 1 | import lxml.etree as ET 2 | import wx 3 | 4 | from edit_dialog import EditDialog 5 | from pubsub import pub 6 | 7 | 8 | class NodeDialog(EditDialog): 9 | """ 10 | A class for adding nodes to your XML objects 11 | """ 12 | 13 | def on_save(self, event): 14 | """ 15 | Event handler that is called when the Save button is 16 | pressed. 17 | 18 | Updates the XML object with the new node element and 19 | tells the UI to update to display the new element 20 | before destroying the dialog 21 | """ 22 | element = ET.SubElement(self.xml_obj, self.value_one.GetValue()) 23 | element.text = self.value_two.GetValue() 24 | pub.sendMessage("tree_update_{}".format(self.page_id), xml_obj=element) 25 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 26 | self.Close() 27 | 28 | 29 | if __name__ == "__main__": 30 | app = wx.App(False) 31 | dlg = NodeDialog("", title="Test", label_one="Element", label_two="Value") 32 | dlg.Destroy() 33 | app.MainLoop() 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boomslang XML 2 | 3 | A simple XML editor created using Python and wxPython. 4 | 5 | image 6 | 7 | This project requires the following: 8 | 9 | - Python 3.5+ but has been tested on Python 3.12 as well 10 | - [wxPython 4+](https://pypi.python.org/pypi/wxPython/) - Updated to work with wxPython 4.2.3 11 | - [lxml](https://pypi.python.org/pypi/lxml/) 12 | - [PyPubSub](https://pypubsub.readthedocs.io/en/v4.0.3/) 13 | 14 | This project has been tested on Windows 7 and 10, Mac OSX Sierra, and Xubuntu 16.04 15 | 16 | # Roadmap 17 | 18 | The following are features that I'd like to add soon: 19 | 20 | - Allow adding multiline strings in a friendly way 21 | - View raw XML and be able to edit it 22 | - Add packaging 23 | 24 | **Long term goals**: 25 | 26 | - Plugins 27 | - Diff tool 28 | 29 | # Known Bugs 30 | 31 | - Nodes cannot have spaces in their names. Not sure if this is fixable yet without doing some research 32 | - Top level nodes with values aren't editable when expanded to show their children 33 | - Cannot delete attributes 34 | - When multiple XML files are open, the current directory isn't saved correctly which can make opening/saving files confusing 35 | -------------------------------------------------------------------------------- /xml_viewer.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import wx.stc as stc 3 | 4 | 5 | class XmlSTC(stc.StyledTextCtrl): 6 | def __init__(self, parent, xml_file): 7 | stc.StyledTextCtrl.__init__(self, parent) 8 | 9 | self.SetLexer(stc.STC_LEX_XML) 10 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "size:12,face:Courier New") 11 | faces = { 12 | "mono": "Courier New", 13 | "helv": "Arial", 14 | "size": 12, 15 | } 16 | 17 | # XML styles 18 | # Default 19 | self.StyleSetSpec( 20 | stc.STC_H_DEFAULT, "fore:#000000,face:%(helv)s,size:%(size)d" % faces 21 | ) 22 | 23 | # Number 24 | self.StyleSetSpec(stc.STC_H_NUMBER, "fore:#007F7F,size:%(size)d" % faces) 25 | # Tag 26 | self.StyleSetSpec(stc.STC_H_TAG, "fore:#007F7F,bold,size:%(size)d" % faces) 27 | # Value 28 | self.StyleSetSpec(stc.STC_H_VALUE, "fore:#7F0000,size:%(size)d" % faces) 29 | # Attribute 30 | self.StyleSetSpec(stc.STC_H_ATTRIBUTE, "fore:#FF5733,size:%(size)d" % faces) 31 | 32 | with open(xml_file) as fobj: 33 | text = fobj.read() 34 | 35 | self.SetText(text) 36 | 37 | 38 | class XmlViewer(wx.Dialog): 39 | def __init__(self, xml_file): 40 | wx.Dialog.__init__( 41 | self, 42 | parent=None, 43 | title="XML Viewer", 44 | style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, 45 | ) 46 | self.xml_view = XmlSTC(self, xml_file) 47 | 48 | sizer = wx.BoxSizer(wx.VERTICAL) 49 | sizer.Add(self.xml_view, 1, wx.EXPAND) 50 | self.SetSizer(sizer) 51 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import wx 4 | 5 | wildcard = "XML (*.xml)|*.xml|All files (*.*)|*.*" 6 | 7 | 8 | def open_file(self, default_dir=os.path.expanduser("~")): 9 | """ 10 | A utility function for opening a file dialog to allow the user 11 | to open an XML file of their choice 12 | """ 13 | path = None 14 | dlg = wx.FileDialog( 15 | self, 16 | message="Choose a file", 17 | defaultDir=default_dir, 18 | defaultFile="", 19 | wildcard=wildcard, 20 | style=wx.FD_OPEN | wx.FD_CHANGE_DIR, 21 | ) 22 | if dlg.ShowModal() == wx.ID_OK: 23 | path = dlg.GetPath() 24 | 25 | dlg.Destroy() 26 | 27 | if path: 28 | return path 29 | 30 | 31 | def save_file(self): 32 | """ 33 | A utility function that allows the user to save their XML file 34 | to a specific location using a file dialog 35 | """ 36 | path = None 37 | dlg = wx.FileDialog( 38 | self, 39 | message="Save file as ...", 40 | defaultDir=self.current_directory, 41 | defaultFile="", 42 | wildcard=wildcard, 43 | style=wx.FD_SAVE, 44 | ) 45 | if dlg.ShowModal() == wx.ID_OK: 46 | path = dlg.GetPath() 47 | dlg.Destroy() 48 | 49 | if path: 50 | return path 51 | 52 | 53 | def get_md5(path): 54 | """ 55 | Returns the MD5 hash of the given file 56 | """ 57 | hash_md5 = hashlib.md5() 58 | with open(path, "rb") as f: 59 | while True: 60 | data = f.read(4096) 61 | hash_md5.update(data) 62 | if not data: 63 | break 64 | return hash_md5.hexdigest() 65 | 66 | 67 | def is_save_current(saved_file_path, tmp_file_path): 68 | """ 69 | Returns a bool that determines if the saved file and the 70 | tmp file's MD5 hash are the same 71 | """ 72 | saved_md5 = get_md5(saved_file_path) 73 | tmp_md5 = get_md5(tmp_file_path) 74 | 75 | return saved_md5 == tmp_md5 76 | 77 | 78 | def warn_nothing_to_save(): 79 | """ 80 | Warns the user that there is nothing to save 81 | """ 82 | msg = "No Files Open! Nothing to save." 83 | dlg = wx.MessageDialog( 84 | parent=None, message=msg, caption="Warning", style=wx.OK | wx.ICON_EXCLAMATION 85 | ) 86 | dlg.ShowModal() 87 | dlg.Destroy() 88 | -------------------------------------------------------------------------------- /edit_dialog.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | 4 | class EditDialog(wx.Dialog): 5 | """ 6 | Super class to derive attribute and element edit 7 | dialogs from 8 | """ 9 | 10 | def __init__(self, xml_obj, page_id, title, label_one, label_two): 11 | """ 12 | @param xml_obj: The lxml XML object 13 | @param page_id: A unique id based on the current page being viewed 14 | @param title: The title of the dialog 15 | @param label_one: The label text for the first text control 16 | @param label_two: The label text for the second text control 17 | """ 18 | wx.Dialog.__init__(self, None, title=title) 19 | self.xml_obj = xml_obj 20 | self.page_id = page_id 21 | 22 | flex_sizer = wx.FlexGridSizer(2, 2, gap=wx.Size(5, 5)) 23 | btn_sizer = wx.BoxSizer(wx.HORIZONTAL) 24 | main_sizer = wx.BoxSizer(wx.VERTICAL) 25 | 26 | attr_lbl = wx.StaticText(self, label=label_one) 27 | flex_sizer.Add(attr_lbl, 0, wx.ALL, 5) 28 | value_lbl = wx.StaticText(self, label=label_two) 29 | flex_sizer.Add(value_lbl, 0, wx.ALL, 5) 30 | 31 | self.value_one = wx.TextCtrl(self) 32 | flex_sizer.Add(self.value_one, 1, wx.ALL | wx.EXPAND, 5) 33 | self.value_two = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) 34 | self.value_two.Bind(wx.EVT_KEY_DOWN, self.on_enter) 35 | flex_sizer.Add(self.value_two, 1, wx.ALL | wx.EXPAND, 5) 36 | flex_sizer.AddGrowableCol(1, 1) 37 | flex_sizer.AddGrowableCol(0, 1) 38 | 39 | save_btn = wx.Button(self, label="Save") 40 | save_btn.Bind(wx.EVT_BUTTON, self.on_save) 41 | btn_sizer.Add(save_btn, 0, wx.ALL | wx.CENTER, 5) 42 | 43 | cancel_btn = wx.Button(self, label="Cancel") 44 | cancel_btn.Bind(wx.EVT_BUTTON, self.on_cancel) 45 | btn_sizer.Add(cancel_btn, 0, wx.ALL | wx.CENTER, 5) 46 | 47 | main_sizer.Add(flex_sizer, 0, wx.EXPAND) 48 | main_sizer.Add(btn_sizer, 0, wx.CENTER) 49 | self.SetSizer(main_sizer) 50 | 51 | self.ShowModal() 52 | 53 | def on_enter(self, event): 54 | """ 55 | Event handler that fires when a key is pressed in the 56 | attribute value text control 57 | """ 58 | keycode = event.GetKeyCode() 59 | if keycode == wx.WXK_RETURN or keycode == wx.WXK_NUMPAD_ENTER: 60 | self.on_save(event=None) 61 | event.Skip() 62 | 63 | def on_cancel(self, event): 64 | """ 65 | Event handler that is called when the Cancel button is 66 | pressed. 67 | 68 | Will destroy the dialog 69 | """ 70 | self.Close() 71 | -------------------------------------------------------------------------------- /editor_page.py: -------------------------------------------------------------------------------- 1 | import lxml.etree as ET 2 | import os 3 | import sys 4 | import time 5 | import utils 6 | import wx 7 | 8 | from boom_attribute_ed import AttributeEditorPanel 9 | from boom_tree import BoomTreePanel 10 | from boom_xml_editor import XmlEditorPanel 11 | from pubsub import pub 12 | 13 | 14 | class NewPage(wx.Panel): 15 | """ 16 | Create a new page for each opened XML document. This is the 17 | top-level widget for the majority of the application 18 | """ 19 | 20 | def __init__(self, parent, xml_path, size, opened_files): 21 | wx.Panel.__init__(self, parent) 22 | self.page_id = id(self) 23 | self.xml_root = None 24 | self.size = size 25 | self.opened_files = opened_files 26 | self.current_file = xml_path 27 | self.title = os.path.basename(xml_path) 28 | 29 | self.app_location = os.path.dirname(os.path.abspath(sys.argv[0])) 30 | 31 | self.tmp_location = os.path.join(self.app_location, "drafts") 32 | 33 | pub.subscribe(self.save, "save_{}".format(self.page_id)) 34 | pub.subscribe(self.auto_save, "on_change_{}".format(self.page_id)) 35 | 36 | self.parse_xml(xml_path) 37 | 38 | current_time = time.strftime("%Y-%m-%d.%H.%M.%S", time.localtime()) 39 | self.full_tmp_path = os.path.join( 40 | self.tmp_location, current_time + "-" + os.path.basename(xml_path) 41 | ) 42 | 43 | if not os.path.exists(self.tmp_location): 44 | try: 45 | os.makedirs(self.tmp_location) 46 | except IOError: 47 | raise IOError("Unable to create file at {}".format(self.tmp_location)) 48 | 49 | if self.xml_root is not None: 50 | self.create_editor() 51 | 52 | def create_editor(self): 53 | """ 54 | Create the XML editor widgets 55 | """ 56 | page_sizer = wx.BoxSizer(wx.VERTICAL) 57 | 58 | splitter = wx.SplitterWindow(self) 59 | tree_panel = BoomTreePanel(splitter, self.xml_root, self.page_id) 60 | 61 | xml_editor_notebook = wx.Notebook(splitter) 62 | xml_editor_panel = XmlEditorPanel(xml_editor_notebook, self.page_id) 63 | xml_editor_notebook.AddPage(xml_editor_panel, "Nodes") 64 | 65 | attribute_panel = AttributeEditorPanel(xml_editor_notebook, self.page_id) 66 | xml_editor_notebook.AddPage(attribute_panel, "Attributes") 67 | 68 | splitter.SplitVertically(tree_panel, xml_editor_notebook) 69 | splitter.SetMinimumPaneSize(int(self.size[0] / 2)) 70 | page_sizer.Add(splitter, 1, wx.ALL | wx.EXPAND, 5) 71 | 72 | self.SetSizer(page_sizer) 73 | self.Layout() 74 | 75 | self.Bind(wx.EVT_CLOSE, self.on_close) 76 | 77 | def auto_save(self, event): 78 | """ 79 | Event handler that is called via pubsub to save the 80 | current version of the XML to disk in a temporary location 81 | """ 82 | self.xml_tree.write(self.full_tmp_path) 83 | pub.sendMessage("on_change_status", save_path=self.full_tmp_path) 84 | 85 | def parse_xml(self, xml_path): 86 | """ 87 | Parses the XML from the file that is passed in 88 | """ 89 | self.current_directory = os.path.dirname(xml_path) 90 | try: 91 | self.xml_tree = ET.parse(xml_path) 92 | except IOError: 93 | print("Bad file") 94 | return 95 | except Exception as e: 96 | print("Really bad error") 97 | print(e) 98 | return 99 | 100 | self.xml_root = self.xml_tree.getroot() 101 | 102 | def save(self, location=None): 103 | """ 104 | Save the XML to disk 105 | """ 106 | if not location: 107 | path = utils.save_file(self) 108 | else: 109 | path = location 110 | 111 | if path: 112 | if ".xml" not in path: 113 | path += ".xml" 114 | 115 | # Save the xml 116 | self.xml_tree.write(path) 117 | self.changed = False 118 | 119 | def on_close(self, event): 120 | """ 121 | Event handler that is called when the panel is being closed 122 | """ 123 | if self.current_file in self.opened_files: 124 | self.opened_files.remove(self.current_file) 125 | 126 | if os.path.exists(self.full_tmp_path): 127 | try: 128 | os.remove(self.full_tmp_path) 129 | except IOError: 130 | print("Unable to delete file: {}".format(self.full_tmp_path)) 131 | -------------------------------------------------------------------------------- /boom_xml_editor.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import wx.lib.scrolledpanel as scrolled 3 | 4 | from functools import partial 5 | from pubsub import pub 6 | 7 | 8 | class XmlEditorPanel(scrolled.ScrolledPanel): 9 | """ 10 | The panel in the notebook that allows editing of XML element values 11 | """ 12 | 13 | def __init__(self, parent, page_id): 14 | """Constructor""" 15 | scrolled.ScrolledPanel.__init__(self, parent, style=wx.SUNKEN_BORDER) 16 | self.main_sizer = wx.BoxSizer(wx.VERTICAL) 17 | self.page_id = page_id 18 | self.widgets = [] 19 | self.label_spacer = None 20 | 21 | pub.subscribe(self.update_ui, "ui_updater_{}".format(self.page_id)) 22 | 23 | self.SetSizer(self.main_sizer) 24 | 25 | def update_ui(self, xml_obj): 26 | """ 27 | Update the panel's user interface based on the data 28 | """ 29 | self.label_sizer = wx.BoxSizer(wx.HORIZONTAL) 30 | self.clear() 31 | 32 | tag_lbl = wx.StaticText(self, label="Tags") 33 | value_lbl = wx.StaticText(self, label="Value") 34 | self.label_sizer.Add(tag_lbl, 0, wx.ALL, 5) 35 | self.label_sizer.Add((55, 0)) 36 | self.label_sizer.Add(value_lbl, 0, wx.ALL, 5) 37 | self.main_sizer.Add(self.label_sizer) 38 | 39 | self.widgets.extend([tag_lbl, value_lbl]) 40 | 41 | if xml_obj is not None: 42 | lbl_size = (75, 25) 43 | for child in xml_obj.getchildren(): 44 | if child.getchildren(): 45 | continue 46 | sizer = wx.BoxSizer(wx.HORIZONTAL) 47 | tag_txt = wx.StaticText(self, label=child.tag, size=lbl_size) 48 | sizer.Add(tag_txt, 0, wx.ALL, 5) 49 | self.widgets.append(tag_txt) 50 | 51 | text = child.text if child.text else "" 52 | 53 | value_txt = wx.TextCtrl(self, value=text) 54 | value_txt.Bind(wx.EVT_TEXT, partial(self.on_text_change, xml_obj=child)) 55 | sizer.Add(value_txt, 1, wx.ALL | wx.EXPAND, 5) 56 | self.widgets.append(value_txt) 57 | 58 | self.main_sizer.Add(sizer, 0, wx.EXPAND) 59 | else: 60 | if getattr(xml_obj, "tag") and getattr(xml_obj, "text"): 61 | if xml_obj.getchildren() == []: 62 | self.add_single_tag_elements(xml_obj, lbl_size) 63 | 64 | add_node_btn = wx.Button(self, label="Add Node") 65 | add_node_btn.Bind(wx.EVT_BUTTON, self.on_add_node) 66 | self.main_sizer.Add(add_node_btn, 0, wx.ALL | wx.CENTER, 5) 67 | self.widgets.append(add_node_btn) 68 | 69 | self.SetAutoLayout(1) 70 | self.SetupScrolling() 71 | 72 | def add_single_tag_elements(self, xml_obj, lbl_size): 73 | """ 74 | Adds the single tag elements to the panel 75 | 76 | This function is only called when there should be just one 77 | tag / value 78 | """ 79 | sizer = wx.BoxSizer(wx.HORIZONTAL) 80 | tag_txt = wx.StaticText(self, label=xml_obj.tag, size=lbl_size) 81 | sizer.Add(tag_txt, 0, wx.ALL, 5) 82 | self.widgets.append(tag_txt) 83 | 84 | value_txt = wx.TextCtrl(self, value=xml_obj.text) 85 | value_txt.Bind(wx.EVT_TEXT, partial(self.on_text_change, xml_obj=xml_obj)) 86 | sizer.Add(value_txt, 1, wx.ALL | wx.EXPAND, 5) 87 | self.widgets.append(value_txt) 88 | 89 | self.main_sizer.Add(sizer, 0, wx.EXPAND) 90 | 91 | def clear(self): 92 | """ 93 | Clears the widgets from the panel in preparation for an update 94 | """ 95 | sizers = {} 96 | for widget in self.widgets: 97 | sizer = widget.GetContainingSizer() 98 | if sizer: 99 | sizer_id = id(sizer) 100 | if sizer_id not in sizers: 101 | sizers[sizer_id] = sizer 102 | widget.Destroy() 103 | 104 | for sizer in sizers: 105 | self.main_sizer.Remove(sizers[sizer]) 106 | 107 | self.widgets = [] 108 | self.Layout() 109 | 110 | def on_text_change(self, event, xml_obj): 111 | """ 112 | An event handler that is called when the text changes in the text 113 | control. This will update the passed in xml object to something 114 | new 115 | """ 116 | xml_obj.text = event.GetString() 117 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 118 | 119 | def on_add_node(self, event): 120 | """ 121 | Event handler that adds an XML node using pubsub 122 | """ 123 | pub.sendMessage("add_node_{}".format(self.page_id)) 124 | -------------------------------------------------------------------------------- /boom_attribute_ed.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | from attribute_dialog import AttributeDialog 4 | from functools import partial 5 | from pubsub import pub 6 | 7 | 8 | class State: 9 | """ 10 | Class for keeping track of the state of the key portion 11 | of the attribute 12 | """ 13 | 14 | def __init__(self, key, val_widget): 15 | self.current_key = key 16 | self.previous_key = None 17 | self.val_widget = val_widget 18 | 19 | 20 | class AttributeEditorPanel(wx.Panel): 21 | """ 22 | A class that holds all UI elements for editing 23 | XML attribute elements 24 | """ 25 | 26 | def __init__(self, parent, page_id): 27 | wx.Panel.__init__(self, parent) 28 | self.page_id = page_id 29 | self.xml_obj = None 30 | self.widgets = [] 31 | 32 | pub.subscribe(self.update_ui, "ui_updater_{}".format(self.page_id)) 33 | 34 | self.main_sizer = wx.BoxSizer(wx.VERTICAL) 35 | self.SetSizer(self.main_sizer) 36 | 37 | def update_ui(self, xml_obj): 38 | """ 39 | Update the user interface to have elements for editing 40 | XML attributes 41 | 42 | Called via pubsub 43 | """ 44 | self.clear() 45 | self.xml_obj = xml_obj 46 | 47 | sizer = wx.BoxSizer(wx.HORIZONTAL) 48 | attr_lbl = wx.StaticText(self, label="Attribute") 49 | value_lbl = wx.StaticText(self, label="Value") 50 | sizer.Add(attr_lbl, 0, wx.ALL, 5) 51 | sizer.Add((133, 0)) 52 | sizer.Add(value_lbl, 0, wx.ALL, 5) 53 | self.widgets.extend([attr_lbl, value_lbl]) 54 | 55 | self.main_sizer.Add(sizer) 56 | 57 | for key in xml_obj.attrib: 58 | _ = wx.BoxSizer(wx.HORIZONTAL) 59 | attr_name = wx.TextCtrl(self, value=key) 60 | _.Add(attr_name, 1, wx.ALL | wx.EXPAND, 5) 61 | self.widgets.append(attr_name) 62 | 63 | val = str(xml_obj.attrib[key]) 64 | attr_val = wx.TextCtrl(self, value=val) 65 | _.Add(attr_val, 1, wx.ALL | wx.EXPAND, 5) 66 | 67 | # keep track of the attribute text control's state 68 | attr_state = State(key, attr_val) 69 | 70 | attr_name.Bind(wx.EVT_TEXT, partial(self.on_key_change, state=attr_state)) 71 | attr_val.Bind(wx.EVT_TEXT, partial(self.on_val_change, attr=attr_name)) 72 | 73 | self.widgets.append(attr_val) 74 | self.main_sizer.Add(_, 0, wx.EXPAND) 75 | else: 76 | add_attr_btn = wx.Button(self, label="Add Attribute") 77 | add_attr_btn.Bind(wx.EVT_BUTTON, self.on_add_attr) 78 | self.main_sizer.Add(add_attr_btn, 0, wx.ALL | wx.CENTER, 5) 79 | self.widgets.append(add_attr_btn) 80 | 81 | self.Layout() 82 | 83 | def on_add_attr(self, event): 84 | """ 85 | Event handler to add an attribute 86 | """ 87 | dlg = AttributeDialog( 88 | self.xml_obj, 89 | page_id=self.page_id, 90 | title="Add Attribute", 91 | label_one="Attribute", 92 | label_two="Value", 93 | ) 94 | dlg.Destroy() 95 | 96 | def clear(self): 97 | """ 98 | Clears the panel of widgets 99 | """ 100 | sizers = {} 101 | for widget in self.widgets: 102 | sizer = widget.GetContainingSizer() 103 | if sizer: 104 | sizer_id = id(sizer) 105 | if sizer_id not in sizers: 106 | sizers[sizer_id] = sizer 107 | widget.Destroy() 108 | 109 | for sizer in sizers: 110 | self.main_sizer.Remove(sizers[sizer]) 111 | 112 | self.widgets = [] 113 | self.Layout() 114 | 115 | def on_key_change(self, event, state): 116 | """ 117 | Event handler that is called on text change in the 118 | attribute key field 119 | """ 120 | new_key = event.GetString() 121 | if new_key not in self.xml_obj.attrib: 122 | if state.current_key in self.xml_obj.attrib: 123 | self.xml_obj.attrib.pop(state.current_key) 124 | self.xml_obj.attrib[new_key] = state.val_widget.GetValue() 125 | state.previous_key = state.current_key 126 | state.current_key = new_key 127 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 128 | 129 | def on_val_change(self, event, attr): 130 | """ 131 | Event handler that is called on text change in the 132 | attribute value field 133 | """ 134 | new_val = event.GetString() 135 | self.xml_obj.attrib[attr.GetValue()] = new_val 136 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 137 | -------------------------------------------------------------------------------- /test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gambardella, Matthew 5 | XML Developer's Guide 6 | Computer 7 | 44.95 8 | 2000-10-01 9 | An in-depth look at creating applications 10 | with XML. 11 | 12 | 13 | Ralls, Kim 14 | Midnight Rain 15 | Fantasy 16 | 5.95 17 | 2000-12-16 18 | A former architect battles corporate zombies, 19 | an evil sorceress, and her own childhood to become queen 20 | of the world. 21 | 22 | 23 | Corets, Eva 24 | Maeve Ascendant 25 | Fantasy 26 | 5.95 27 | 2000-11-17 28 | After the collapse of a nanotechnology 29 | society in England, the young survivors lay the 30 | foundation for a new society. 31 | 32 | 33 | Corets, Eva 34 | Oberon's Legacy 35 | Fantasy 36 | 5.95 37 | 2001-03-10 38 | In post-apocalypse England, the mysterious 39 | agent known only as Oberon helps to create a new life 40 | for the inhabitants of London. Sequel to Maeve 41 | Ascendant. 42 | 43 | 44 | Corets, Eva 45 | The Sundered Grail 46 | Fantasy 47 | 5.95 48 | 2001-09-10 49 | The two daughters of Maeve, half-sisters, 50 | battle one another for control of England. Sequel to 51 | Oberon's Legacy. 52 | 53 | 54 | Randall, Cynthia 55 | Lover Birds 56 | Romance 57 | 4.95 58 | 2000-09-02 59 | When Carla meets Paul at an ornithology 60 | conference, tempers fly as feathers get ruffled. 61 | 62 | 63 | Thurman, Paula 64 | Splish Splash 65 | Romance 66 | 4.95 67 | 2000-11-02 68 | A deep sea diver finds true love twenty 69 | thousand leagues beneath the sea. 70 | 71 | 72 | Knorr, Stefan 73 | Creepy Crawlies 74 | Horror 75 | 4.95 76 | 2000-12-06 77 | An anthology of horror stories about roaches, 78 | centipedes, scorpions and other insects. 79 | 80 | 81 | Kress, Peter 82 | Paradox Lost 83 | Science Fiction 84 | 6.95 85 | 2000-11-02 86 | After an inadvertant trip through a Heisenberg 87 | Uncertainty Device, James Salway discovers the problems 88 | of being quantum. 89 | 90 | 91 | O'Brien, Tim 92 | Microsoft .NET: The Programming Bible 93 | Computer 94 | 36.95 95 | 2000-12-09 96 | Microsoft's .NET initiative is explored in 97 | detail in this deep programmer's reference. 98 | 99 | 100 | O'Brien, Tim 101 | MSXML3: A Comprehensive Guide 102 | Computer 103 | 36.95 104 | 2000-12-01 105 | The Microsoft MSXML3 parser is covered in 106 | detail, with attention to XML DOM interfaces, XSLT processing, 107 | SAX and more. 108 | 109 | 110 | Galos, Mike 111 | Visual Studio 7: A Comprehensive Guide 112 | Computer 113 | 49.95 114 | 2001-04-16 115 | Microsoft Visual Studio 7 is explored in depth, 116 | looking at how Visual Basic, Visual C++, C#, and ASP+ are 117 | integrated into a comprehensive development 118 | environment. 119 | 120 | 121 | -------------------------------------------------------------------------------- /books.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gambardella, Matthew 5 | XML Developer's Guide 6 | Computer 7 | 44.95 8 | 2000-10-01 9 | An in-depth look at creating applications 10 | with XML. 11 | 12 | 13 | Ralls, Kim 14 | Midnight Rain 15 | Fantasy 16 | 5.95 17 | 2000-12-16 18 | A former architect battles corporate zombies, 19 | an evil sorceress, and her own childhood to become queen 20 | of the world. 21 | 22 | 23 | Corets, Eva 24 | Maeve Ascendant 25 | Fantasy 26 | 5.95 27 | 2000-11-17 28 | After the collapse of a nanotechnology 29 | society in England, the young survivors lay the 30 | foundation for a new society. 31 | 32 | 33 | Corets, Eva 34 | Oberon's Legacy 35 | Fantasy 36 | 5.95 37 | 2001-03-10 38 | In post-apocalypse England, the mysterious 39 | agent known only as Oberon helps to create a new life 40 | for the inhabitants of London. Sequel to Maeve 41 | Ascendant. 42 | 43 | 44 | Corets, Eva 45 | The Sundered Grail 46 | Fantasy 47 | 5.95 48 | 2001-09-10 49 | The two daughters of Maeve, half-sisters, 50 | battle one another for control of England. Sequel to 51 | Oberon's Legacy. 52 | 53 | 54 | Randall, Cynthia 55 | Lover Birds 56 | Romance 57 | 4.95 58 | 2000-09-02 59 | When Carla meets Paul at an ornithology 60 | conference, tempers fly as feathers get ruffled. 61 | 62 | 63 | Thurman, Paula 64 | Splish Splash 65 | Romance 66 | 4.95 67 | 2000-11-02 68 | A deep sea diver finds true love twenty 69 | thousand leagues beneath the sea. 70 | 71 | 72 | Knorr, Stefan 73 | Creepy Crawlies 74 | Horror 75 | 4.95 76 | 2000-12-06 77 | An anthology of horror stories about roaches, 78 | centipedes, scorpions and other insects. 79 | 80 | 81 | Kress, Peter 82 | Paradox Lost 83 | Science Fiction 84 | 6.95 85 | 2000-11-02 86 | After an inadvertant trip through a Heisenberg 87 | Uncertainty Device, James Salway discovers the problems 88 | of being quantum. 89 | 90 | 91 | O'Brien, Tim 92 | Microsoft .NET: The Programming Bible 93 | Computer 94 | 36.95 95 | 2000-12-09 96 | Microsoft's .NET initiative is explored in 97 | detail in this deep programmer's reference. 98 | 99 | 100 | O'Brien, Tim 101 | MSXML3: A Comprehensive Guide 102 | Computer 103 | 36.95 104 | 2000-12-01 105 | The Microsoft MSXML3 parser is covered in 106 | detail, with attention to XML DOM interfaces, XSLT processing, 107 | SAX and more. 108 | 109 | 110 | Galos, Mike 111 | Visual Studio 7: A Comprehensive Guide 112 | Computer 113 | 49.95 114 | 2001-04-16 115 | Microsoft Visual Studio 7 is explored in depth, 116 | looking at how Visual Basic, Visual C++, C#, and ASP+ are 117 | integrated into a comprehensive development 118 | environment. 119 | 120 | -------------------------------------------------------------------------------- /boom_tree.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | from add_node_dialog import NodeDialog 4 | from pubsub import pub 5 | 6 | 7 | class XmlTree(wx.TreeCtrl): 8 | """ 9 | The class that holds all the functionality for the tree control 10 | widget 11 | """ 12 | 13 | def __init__(self, parent, wx_id, pos, size, style): 14 | wx.TreeCtrl.__init__(self, parent, wx_id, pos, size, style) 15 | self.expanded = {} 16 | self.xml_root = parent.xml_root 17 | self.page_id = parent.page_id 18 | pub.subscribe(self.update_tree, "tree_update_{}".format(self.page_id)) 19 | 20 | root = self.AddRoot(self.xml_root.tag) 21 | self.expanded[id(self.xml_root)] = "" 22 | self.SetItemData(root, self.xml_root) 23 | wx.CallAfter( 24 | pub.sendMessage, "ui_updater_{}".format(self.page_id), xml_obj=self.xml_root 25 | ) 26 | 27 | if self.xml_root.getchildren(): 28 | for top_level_item in self.xml_root.getchildren(): 29 | child = self.AppendItem(root, top_level_item.tag) 30 | if top_level_item.getchildren(): 31 | self.SetItemHasChildren(child) 32 | self.SetItemData(child, top_level_item) 33 | 34 | self.Expand(root) 35 | self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.on_item_expanding) 36 | self.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_selection) 37 | 38 | def add_elements(self, item, book): 39 | """ 40 | Add items to the tree control 41 | """ 42 | for element in book.getchildren(): 43 | child = self.AppendItem(item, element.tag) 44 | self.SetItemData(child, element) 45 | if element.getchildren(): 46 | self.SetItemHasChildren(child) 47 | 48 | def on_item_expanding(self, event): 49 | """ 50 | A handler that fires when a tree item is being expanded 51 | 52 | This will cause the sub-elements of the tree to be created 53 | and added to the tree 54 | """ 55 | item = event.GetItem() 56 | xml_obj = self.GetItemData(item) 57 | 58 | if id(xml_obj) not in self.expanded and xml_obj is not None: 59 | for top_level_item in xml_obj.getchildren(): 60 | child = self.AppendItem(item, top_level_item.tag) 61 | self.SetItemData(child, top_level_item) 62 | if top_level_item.getchildren(): 63 | self.SetItemHasChildren(child) 64 | 65 | self.expanded[id(xml_obj)] = "" 66 | 67 | def on_tree_selection(self, event): 68 | """ 69 | A handler that fires when an item in the tree is selected 70 | 71 | This will cause an update to be sent to the XmlEditorPanel 72 | to allow editing of the XML 73 | """ 74 | item = event.GetItem() 75 | xml_obj = self.GetItemData(item) 76 | pub.sendMessage("ui_updater_{}".format(self.page_id), xml_obj=xml_obj) 77 | 78 | def update_tree(self, xml_obj): 79 | """ 80 | Update the tree with the new data 81 | """ 82 | selection = self.GetSelection() 83 | selected_tree_xml_obj = self.GetItemData(selection) 84 | 85 | if id(selected_tree_xml_obj) in self.expanded: 86 | child = self.AppendItem(selection, xml_obj.tag) 87 | if xml_obj.getchildren(): 88 | self.SetItemHasChildren(child) 89 | self.SetItemData(child, xml_obj) 90 | 91 | if selected_tree_xml_obj.getchildren(): 92 | self.SetItemHasChildren(selection) 93 | 94 | 95 | class BoomTreePanel(wx.Panel): 96 | """ 97 | The panel class that contains the XML tree control 98 | """ 99 | 100 | def __init__(self, parent, xml_obj, page_id): 101 | wx.Panel.__init__(self, parent) 102 | self.xml_root = xml_obj 103 | self.copied_data = None 104 | self.page_id = page_id 105 | 106 | pub.subscribe(self.add_node, "add_node_{}".format(self.page_id)) 107 | pub.subscribe(self.remove_node, "remove_node_{}".format(self.page_id)) 108 | 109 | self.tree = XmlTree( 110 | self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_HAS_BUTTONS 111 | ) 112 | self.tree.Bind(wx.EVT_CONTEXT_MENU, self.on_context_menu) 113 | 114 | sizer = wx.BoxSizer(wx.VERTICAL) 115 | sizer.Add(self.tree, 1, wx.EXPAND) 116 | self.SetSizer(sizer) 117 | 118 | def on_context_menu(self, event): 119 | """ 120 | Event handler that creates a context menu on right-click 121 | of a tree control's item 122 | """ 123 | if not hasattr(self, "add_node_id"): 124 | self.add_node_id = wx.NewId() 125 | self.remove_node_id = wx.NewId() 126 | self.copy_id = wx.NewId() 127 | self.paste_id = wx.NewId() 128 | 129 | self.Bind(wx.EVT_MENU, self.on_add_remove_node, id=self.add_node_id) 130 | self.Bind(wx.EVT_MENU, self.on_add_remove_node, id=self.remove_node_id) 131 | self.Bind(wx.EVT_MENU, self.on_copy, id=self.copy_id) 132 | self.Bind(wx.EVT_MENU, self.on_paste, id=self.paste_id) 133 | 134 | # Build the context menu 135 | menu = wx.Menu() 136 | menu.Append(self.copy_id, "Copy") 137 | menu.Append(self.paste_id, "Paste") 138 | menu.AppendSeparator() 139 | menu.Append(self.add_node_id, "Add Node") 140 | menu.Append(self.remove_node_id, "Remove Node") 141 | 142 | self.PopupMenu(menu) 143 | menu.Destroy() 144 | 145 | def on_add_remove_node(self, event): 146 | """ 147 | Event handler for adding or removing nodes 148 | """ 149 | evt_id = event.GetId() 150 | if evt_id == self.add_node_id: 151 | self.add_node() 152 | elif evt_id == self.remove_node_id: 153 | self.remove_node() 154 | 155 | def on_copy(self, event): 156 | """ 157 | Copy the selected XML object into memory 158 | """ 159 | node = self.tree.GetSelection() 160 | self.copied_data = self.tree.GetItemData(node) 161 | 162 | def on_paste(self, event): 163 | """ 164 | Paste / Append the copied XML data to the selected node 165 | """ 166 | if self.copied_data: 167 | node = self.tree.GetSelection() 168 | parent_xml_node = self.tree.GetItemData(node) 169 | 170 | parent_xml_node.append(self.copied_data) 171 | pub.sendMessage( 172 | "tree_update_{}".format(self.page_id), xml_obj=self.copied_data 173 | ) 174 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 175 | 176 | def add_node(self): 177 | """ 178 | Add a sub-node to the selected item in the tree 179 | """ 180 | node = self.tree.GetSelection() 181 | data = self.tree.GetItemData(node) 182 | dlg = NodeDialog( 183 | data, 184 | page_id=self.page_id, 185 | title="New Node", 186 | label_one="Element Tag", 187 | label_two="Element Value", 188 | ) 189 | dlg.Destroy() 190 | 191 | def remove_node(self): 192 | """ 193 | Remove the selected node from the tree 194 | """ 195 | node = self.tree.GetSelection() 196 | xml_node = self.tree.GetItemData(node) 197 | 198 | if node: 199 | msg = "Are you sure you want to delete the {node} node" 200 | dlg = wx.MessageDialog( 201 | parent=None, 202 | message=msg.format(node=xml_node.tag), 203 | caption="Warning", 204 | style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_EXCLAMATION, 205 | ) 206 | if dlg.ShowModal() == wx.ID_YES: 207 | parent = xml_node.getparent() 208 | parent.remove(xml_node) 209 | self.tree.DeleteChildren(node) 210 | self.tree.Delete(node) 211 | pub.sendMessage("on_change_{}".format(self.page_id), event=None) 212 | dlg.Destroy() 213 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import utils 5 | import wx 6 | import wx.adv 7 | import wx.lib.agw.flatnotebook as fnb 8 | 9 | from datetime import datetime 10 | 11 | from editor_page import NewPage 12 | from pubsub import pub 13 | from xml_viewer import XmlViewer 14 | from wx.lib.wordwrap import wordwrap 15 | 16 | 17 | class Boomslang(wx.Frame): 18 | def __init__(self): 19 | self.size = (800, 600) 20 | wx.Frame.__init__(self, parent=None, title="Boomslang XML", size=(800, 600)) 21 | 22 | self.full_tmp_path = "" 23 | self.full_saved_path = "" 24 | self.changed = False 25 | self.notebook = None 26 | self.opened_files = [] 27 | self.last_opened_file = None 28 | self.current_page = None 29 | self.today = datetime.now() 30 | 31 | self.current_directory = os.path.expanduser("~") 32 | self.app_location = os.path.dirname(os.path.abspath(sys.argv[0])) 33 | self.recent_files_path = os.path.join(self.app_location, "recent_files.txt") 34 | 35 | pub.subscribe(self.save, "save") 36 | pub.subscribe(self.auto_save_status, "on_change_status") 37 | 38 | self.main_sizer = wx.BoxSizer(wx.VERTICAL) 39 | self.panel = wx.Panel(self) 40 | self.panel.SetSizer(self.main_sizer) 41 | 42 | self.create_menu_and_toolbar() 43 | 44 | self.Bind(wx.EVT_CLOSE, self.on_exit) 45 | 46 | self.Show() 47 | 48 | def create_new_editor(self, xml_path): 49 | """ 50 | Create the tree and xml editing widgets when the user loads 51 | an XML file 52 | """ 53 | if not self.notebook: 54 | self.notebook = fnb.FlatNotebook(self.panel) 55 | self.main_sizer.Add(self.notebook, 1, wx.ALL | wx.EXPAND, 5) 56 | style = self.notebook.GetAGWWindowStyleFlag() 57 | style |= fnb.FNB_X_ON_TAB 58 | self.notebook.SetAGWWindowStyleFlag(style) 59 | self.notebook.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CLOSING, self.on_page_closing) 60 | 61 | if xml_path not in self.opened_files: 62 | self.current_page = NewPage( 63 | self.notebook, xml_path, self.size, self.opened_files 64 | ) 65 | self.notebook.AddPage( 66 | self.current_page, os.path.basename(xml_path), select=True 67 | ) 68 | self.last_opened_file = xml_path 69 | 70 | self.opened_files.append(self.last_opened_file) 71 | 72 | self.panel.Layout() 73 | 74 | def create_menu_and_toolbar(self): 75 | """ 76 | Creates the menu bar, menu items, toolbar and accelerator table 77 | for the main frame 78 | """ 79 | menu_bar = wx.MenuBar() 80 | file_menu = wx.Menu() 81 | help_menu = wx.Menu() 82 | 83 | # add menu items to the file menu 84 | open_menu_item = file_menu.Append(wx.NewIdRef(), "Open", "") 85 | self.Bind(wx.EVT_MENU, self.on_open, open_menu_item) 86 | 87 | sub_menu = self.create_recent_items() 88 | file_menu.Append(wx.NewIdRef(), "Recent", sub_menu) 89 | 90 | save_menu_item = file_menu.Append(wx.NewIdRef(), "Save", "") 91 | self.Bind(wx.EVT_MENU, self.on_save, save_menu_item) 92 | 93 | exit_menu_item = file_menu.Append(wx.NewIdRef(), "Quit", "") 94 | self.Bind(wx.EVT_MENU, self.on_exit, exit_menu_item) 95 | menu_bar.Append(file_menu, "&File") 96 | 97 | # add menu items to the help menu 98 | about_menu_item = help_menu.Append(wx.NewIdRef(), "About") 99 | self.Bind(wx.EVT_MENU, self.on_about_box, about_menu_item) 100 | menu_bar.Append(help_menu, "&Help") 101 | 102 | self.SetMenuBar(menu_bar) 103 | 104 | # ----------------------------------------------------------------------- 105 | # Create toolbar 106 | self.toolbar = self.CreateToolBar() 107 | self.toolbar.SetToolBitmapSize((16, 16)) 108 | 109 | open_ico = wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, (16, 16)) 110 | open_tool = self.toolbar.AddTool( 111 | wx.ID_ANY, "Open", open_ico, "Open an XML File" 112 | ) 113 | self.Bind(wx.EVT_MENU, self.on_open, open_tool) 114 | 115 | save_ico = wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, (16, 16)) 116 | save_tool = self.toolbar.AddTool(wx.ID_ANY, "Save", save_ico, "Saves the XML") 117 | self.Bind(wx.EVT_MENU, self.on_save, save_tool) 118 | 119 | self.toolbar.AddSeparator() 120 | 121 | # Create the add node toolbar button 122 | add_ico = wx.ArtProvider.GetBitmap(wx.ART_PLUS, wx.ART_TOOLBAR, (16, 16)) 123 | add_tool = self.toolbar.AddTool( 124 | wx.ID_ANY, "Add Node", add_ico, "Adds an XML Node" 125 | ) 126 | self.Bind(wx.EVT_MENU, self.on_add_node, add_tool) 127 | 128 | # Create the delete node button 129 | remove_ico = wx.ArtProvider.GetBitmap(wx.ART_MINUS, wx.ART_TOOLBAR, (16, 16)) 130 | remove_node_tool = self.toolbar.AddTool( 131 | wx.ID_ANY, "Remove Node", remove_ico, "Removes the XML Node" 132 | ) 133 | self.Bind(wx.EVT_MENU, self.on_remove_node, remove_node_tool) 134 | 135 | # Create a preview XML button 136 | preview_ico = wx.ArtProvider.GetBitmap( 137 | wx.ART_REPORT_VIEW, wx.ART_TOOLBAR, (16, 16) 138 | ) 139 | preview_tool = self.toolbar.AddTool( 140 | wx.ID_ANY, "Preview XML", preview_ico, "Previews XML" 141 | ) 142 | self.Bind(wx.EVT_MENU, self.on_preview_xml, preview_tool) 143 | 144 | self.toolbar.Realize() 145 | 146 | # ----------------------------------------------------------------------- 147 | # Create an accelerator table 148 | accel_tbl = wx.AcceleratorTable( 149 | [ 150 | (wx.ACCEL_CTRL, ord("O"), open_menu_item.GetId()), 151 | (wx.ACCEL_CTRL, ord("S"), save_menu_item.GetId()), 152 | (wx.ACCEL_CTRL, ord("A"), add_tool.GetId()), 153 | (wx.ACCEL_CTRL, ord("X"), remove_node_tool.GetId()), 154 | ] 155 | ) 156 | 157 | self.SetAcceleratorTable(accel_tbl) 158 | 159 | # ----------------------------------------------------------------------- 160 | # Create status bar 161 | self.status_bar = self.CreateStatusBar(1) 162 | 163 | msg = f"Welcome to Boomslang XML (c) Michael Driscoll - 2017-{self.today.year}" 164 | self.status_bar.SetStatusText(msg) 165 | 166 | def create_recent_items(self): 167 | """ 168 | Create the recent items sub_menu and return it 169 | """ 170 | self.recent_dict = {} 171 | if os.path.exists(self.recent_files_path): 172 | submenu = wx.Menu() 173 | try: 174 | with open(self.recent_files_path) as fobj: 175 | for line in fobj: 176 | menu_id = wx.NewIdRef() 177 | submenu.Append(menu_id, line) 178 | self.recent_dict[menu_id] = line.strip() 179 | self.Bind(wx.EVT_MENU, self.on_open_recent_file, id=menu_id) 180 | return submenu 181 | except IOError: 182 | pass 183 | 184 | def auto_save_status(self, save_path): 185 | """ 186 | This function is called via PubSub to update the frame's status 187 | """ 188 | print(f"Autosaving to {save_path} @ {time.ctime()}") 189 | msg = f"Autosaved at {self.today:%H:%M:%}" 190 | self.status_bar.SetStatusText(msg) 191 | 192 | self.changed = True 193 | 194 | def open_xml_file(self, xml_path): 195 | """ 196 | Open the specified XML file and load it in the application 197 | """ 198 | self.create_new_editor(xml_path) 199 | 200 | def save(self, location=None): 201 | """ 202 | Update the frame with save status 203 | """ 204 | if self.current_page.xml_root is None: 205 | utils.warn_nothing_to_save() 206 | return 207 | 208 | pub.sendMessage(f"save_{self.current_page.page_id}") 209 | 210 | self.changed = False 211 | msg = f"Last saved at {self.today:%H:%M:%S}" 212 | self.status_bar.SetStatusText(msg) 213 | 214 | def on_about_box(self, event): 215 | """ 216 | Event handler that builds and shows an about box 217 | """ 218 | info = wx.adv.AboutDialogInfo() 219 | info.Name = "About Boomslang" 220 | info.Version = "0.1 Beta" 221 | info.Copyright = f"(C) 2017-{self.today.year} Mike Driscoll" 222 | info.Description = wordwrap( 223 | "Boomslang is a Python-based XML editor ", 350, wx.ClientDC(self.panel) 224 | ) 225 | info.WebSite = ( 226 | "https://github.com/driscollis/boomslang", 227 | "Boomslang on Github", 228 | ) 229 | info.Developers = ["Mike Driscoll"] 230 | info.License = wordwrap( 231 | "wxWindows Library Licence", 500, wx.ClientDC(self.panel) 232 | ) 233 | # Show the wx.AboutBox 234 | wx.adv.AboutBox(info) 235 | 236 | def on_add_node(self, event): 237 | """ 238 | Event handler that is fired when an XML node is added to the 239 | selected node 240 | """ 241 | pub.sendMessage(f"add_node_{self.current_page.page_id}") 242 | 243 | def on_remove_node(self, event): 244 | """ 245 | Event handler that is fired when an XML node is removed 246 | """ 247 | pub.sendMessage(f"remove_node_{self.current_page.page_id}") 248 | 249 | def on_open(self, event): 250 | """ 251 | Event handler that is called when you need to open an XML file 252 | """ 253 | xml_path = utils.open_file(self) 254 | 255 | if xml_path: 256 | self.last_opened_file = xml_path 257 | self.open_xml_file(xml_path) 258 | self.update_recent_files(xml_path) 259 | 260 | def on_page_closing(self, event): 261 | """ 262 | Event handler that is called when a page in the notebook is closing 263 | """ 264 | page = self.notebook.GetCurrentPage() 265 | page.Close() 266 | if not self.opened_files: 267 | wx.CallAfter(self.notebook.Destroy) 268 | self.notebook = None 269 | 270 | def on_preview_xml(self, event): 271 | """ 272 | Event handler called for previewing the current state of the XML 273 | in memory 274 | """ 275 | if self.last_opened_file: 276 | previewer = XmlViewer(xml_file=self.last_opened_file) 277 | previewer.ShowModal() 278 | previewer.Destroy() 279 | 280 | def update_recent_files(self, xml_path): 281 | """ 282 | Update the recent files file 283 | """ 284 | lines = [] 285 | try: 286 | with open(self.recent_files_path) as fobj: 287 | lines = fobj.readlines() 288 | except IOError: 289 | pass 290 | 291 | lines = [line.strip() for line in lines] 292 | 293 | if xml_path not in lines: 294 | try: 295 | with open(self.recent_files_path, "a") as fobj: 296 | fobj.write(xml_path) 297 | fobj.write("\n") 298 | except IOError: 299 | pass 300 | elif xml_path != lines[0]: 301 | for index, item in enumerate(lines): 302 | if xml_path == item: 303 | lines.pop(index) 304 | break 305 | lines.insert(0, xml_path) 306 | 307 | if len(lines) > 10: 308 | lines = lines[0:9] 309 | 310 | # rewrite the file 311 | try: 312 | with open(self.recent_files_path, "w") as fobj: 313 | for line in lines: 314 | fobj.write(line) 315 | fobj.write("\n") 316 | except IOError: 317 | pass 318 | 319 | def on_open_recent_file(self, event): 320 | """ 321 | Event handler that is called when a recent file is selected 322 | for opening 323 | """ 324 | self.open_xml_file(self.recent_dict[event.GetId()]) 325 | 326 | def on_save(self, event): 327 | """ 328 | Event handler that saves the data to disk 329 | """ 330 | self.save() 331 | 332 | def on_exit(self, event): 333 | """ 334 | Event handler that closes the application 335 | """ 336 | self.Destroy() 337 | 338 | 339 | # ------------------------------------------------------------------------------ 340 | # Run the program! 341 | if __name__ == "__main__": 342 | xml_path = "books.xml" 343 | app = wx.App(redirect=False) 344 | frame = Boomslang() 345 | app.MainLoop() 346 | --------------------------------------------------------------------------------