├── 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 |
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 |
--------------------------------------------------------------------------------