diff --git a/gui.py b/gui.py index e8c234c..2b14899 100644 --- a/gui.py +++ b/gui.py @@ -13,7 +13,7 @@ ######## import sys -from os.path import basename +from os.path import basename, abspath, dirname, join as joindir import tkinter as tk import tkinter.ttk as ttk @@ -30,10 +30,15 @@ ('Collection', 'Collections'), ('EFX', 'EFX'), ('Audio', 'Audio Files'), + ('Video', 'Video Files'), ('Show', 'Shows'), ('Chaser', 'Chasers'), + ('RGBMatrix', 'RGB Matrix Functions'), + ('Script', 'Scripts'), ] +ICONS = {} + # Global State: (oh noes) CLIPBOARD = set() @@ -67,7 +72,8 @@ def create_widgets(self): ttk.Label(self, textvariable=self.filenameText).pack() self.filterText = ttk.Entry(self) # TODO - self.filterText.pack(padx=0.1) + self.filterText.bind('', self.update_filter) + self.filterText.pack(padx=0.1, fill=tk.X, expand=1) self.functionList = ttk.Treeview(self) self.functionList.pack(fill=tk.BOTH, expand=1) @@ -104,6 +110,9 @@ def load_file(self): self.qfile = QLCFile(self.filename) self.filenameText.set(basename(self.filename)) + def update_filter(self, event): + self.update_treeview() + def update_treeview(self): ''' This should be able to be called multiple times without mucking up @@ -118,7 +127,7 @@ def update_treeview(self): # Top level tree items (Sections): for iid, plural in FUNCTION_TYPES: - self.functionList.insert('', 'end', '_' + iid, text=plural) + self.functionList.insert('', 'end', '_' + iid, text=plural, open=True, image=ICONS[iid]) allcount = 0 orphancount = 0 @@ -129,10 +138,15 @@ def update_treeview(self): # TODO: Show in folders mode # TODO: Create folders and sort out functions into new folders depending on usage. # TODO: Images for function type... + # TODO: Checking / Fixing Folders, what if something has a path that doesn't exist? + + filtertext = self.filterText.get() for f in funcs: allcount += 1 name = "{} [{}]".format(f.attrib["Name"],f.attrib["ID"]) + if not filtertext in name.lower(): continue + self.functionList.insert('_' + f.attrib["Type"],'end', 'FUNC:' + f.attrib["ID"], text=name) #[print(x) for x in self.qfile.subfunction_ids(f)] @@ -150,16 +164,10 @@ def copySelected(self): if iid.startswith('FUNC:')] CLIPBOARD.clear() - - for iid in selected_ids: - func = self.qfile.function_by_id(iid) - if not func: continue - + + for func in self.qfile.iter_functions_for_clipboard(selected_ids): CLIPBOARD.add(func) - for subfunc in self.qfile.subfunction_ids(func, recurse=True): - CLIPBOARD.add(subfunc) - print('%i items in clipboard' % len(CLIPBOARD)) @@ -221,6 +229,13 @@ def load_file(self, filename=None): app.master.title('QLC+ Multi-file Helper Utility') + iconsdir = joindir(dirname(abspath(__file__)), 'icons') + for functype, _ in FUNCTION_TYPES: + icon = tk.PhotoImage(file=joindir(iconsdir, functype.lower() + '.png')) + icon = icon.subsample(2, 2) + ICONS[functype] = icon + + for f in (sys.argv[1:]): try: app.load_file(f) diff --git a/icons/audio.png b/icons/audio.png new file mode 100644 index 0000000..2d953d0 Binary files /dev/null and b/icons/audio.png differ diff --git a/icons/chaser.png b/icons/chaser.png new file mode 100644 index 0000000..c0ab36d Binary files /dev/null and b/icons/chaser.png differ diff --git a/icons/collection.png b/icons/collection.png new file mode 100644 index 0000000..1ec63ee Binary files /dev/null and b/icons/collection.png differ diff --git a/icons/cuelist.png b/icons/cuelist.png new file mode 100644 index 0000000..e8f9790 Binary files /dev/null and b/icons/cuelist.png differ diff --git a/icons/efx.png b/icons/efx.png new file mode 100644 index 0000000..13957a0 Binary files /dev/null and b/icons/efx.png differ diff --git a/icons/rgbmatrix.png b/icons/rgbmatrix.png new file mode 100644 index 0000000..f29945e Binary files /dev/null and b/icons/rgbmatrix.png differ diff --git a/icons/scene.png b/icons/scene.png new file mode 100644 index 0000000..8cc5f45 Binary files /dev/null and b/icons/scene.png differ diff --git a/icons/script.png b/icons/script.png new file mode 100644 index 0000000..643faa5 Binary files /dev/null and b/icons/script.png differ diff --git a/icons/sequence.png b/icons/sequence.png new file mode 100644 index 0000000..267c2ad Binary files /dev/null and b/icons/sequence.png differ diff --git a/icons/show.png b/icons/show.png new file mode 100644 index 0000000..1d7a8af Binary files /dev/null and b/icons/show.png differ diff --git a/icons/video.png b/icons/video.png new file mode 100644 index 0000000..d82b92a Binary files /dev/null and b/icons/video.png differ diff --git a/reader.py b/reader.py index c51825b..f4cc303 100644 --- a/reader.py +++ b/reader.py @@ -23,7 +23,7 @@ def __init__(self, filename): def write(self, filename, *vargs, **kwargs): new_file = ET.ElementTree(self.root) - with open(filename, 'w') as f: + with open(filename, 'w', encoding="utf8") as f: # apparently etree cannot write doctypes :-( # oh well. we can. f.write('\n\n') @@ -126,6 +126,19 @@ def subfunction_id_replace(self, func, old_id, new_id): if f.text == old_id: f.text = new_id + def iter_functions_for_clipboard(self, ids): + ''' + given a list of ids, generate all functions which need to be copied, + including sub-functions / dependencies. + ''' + for iid in ids: + func = self.function_by_id(iid) + if not func: continue + + yield func + + for subfunc in self.subfunction_ids(func, recurse=True): + yield subfunc def paste_functions_here(self, clipboard): @@ -139,7 +152,7 @@ def paste_functions_here(self, clipboard): # print(new_functions) - fresh_id = self.highest_function_id() + 1 + fresh_id = self.highest_function_id()# + 1 current_ids = set(self.used_function_ids()) diff --git a/snippets.py b/snippets.py new file mode 100644 index 0000000..0272a4d --- /dev/null +++ b/snippets.py @@ -0,0 +1,86 @@ +''' + XML Snippets for building valid QXW files for testing. + + Structure: + + header + engine_header + + + engine_footer + virtual_console (default is empty_virtual_console) + simple_desk (default is empty_simple_desk) + footer +''' + +header = ''' + + + + Q Light Controller Plus + 4.12.0 + daniel.fairhead + + ''' +engine_header = ''' + + + + + + + + + + +''' + +engine_footer = '' + +footer = ''' + +''' + +empty_virtual_console = ''' + + + + None + Default + Default + None + Default + + + + + + + +''' + +empty_simple_desk = ''' + + + +''' + +def generic_fixture(uid, address, channels=1, universe=0, name=None): + return ''' + + Generic + Generic + 1 Channel + {uid} + {name} + {universe} +
{address}
+ {channels} +
+ '''.format(uid=uid, address=address, universe=universe, channels=channels, + name=name or 'Dimmer %i' % uid) + +def function_scene(uid, name, path=""): + return ''' + + '''.format(uid=uid, name=name, path=path) diff --git a/test_reader.py b/test_reader.py new file mode 100644 index 0000000..0111892 --- /dev/null +++ b/test_reader.py @@ -0,0 +1,142 @@ +import xml.etree.ElementTree as ET +from io import StringIO + +from unittest import TestCase + +from reader import NS, QLCFile +import snippets + +class BasicFileTest(TestCase): + def setUp(self): + super().setUp() + self.virtual_console = snippets.empty_virtual_console + self.simple_desk = snippets.empty_simple_desk + self.fixtures = [] + self.functions = [] + + def get_xml(self): + return '\n'.join(( + snippets.header, + snippets.engine_header, + '\n'.join(self.fixtures), + '\n'.join(self.functions), + snippets.engine_footer, + self.virtual_console, + self.simple_desk, + snippets.footer)) + + +class SanityTest(BasicFileTest): + def test_basicfile(self): + x = StringIO(self.get_xml()) + r = ET.parse(x).getroot() + + # TODO: callout to actual QLC+ to load the file and check it + + def test_some_fixtures(self): + self.fixtures = [ + snippets.generic_fixture(0, 1), + snippets.generic_fixture(1, 2), + snippets.generic_fixture(2, 3, channels=6), + ] + + x = StringIO(self.get_xml()) + r = ET.parse(x).getroot() + + def test_some_functions(self): + self.functions = [ + snippets.function_scene(0, "Basic Scene"), + snippets.function_scene(1, "Basic Scene 1"), + snippets.function_scene(2, "Basic Scene 2"), + ] + x = StringIO(self.get_xml()) + r = ET.parse(x).getroot() + + def test_some_fixtures_and_functions(self): + self.fixtures = [ + snippets.generic_fixture(0, 1), + snippets.generic_fixture(1, 2), + snippets.generic_fixture(2, 3, channels=6), + ] + + self.functions = [ + snippets.function_scene(0, "Basic Scene"), + snippets.function_scene(1, "Basic Scene 1"), + snippets.function_scene(2, "Basic Scene 2"), + ] + + x = StringIO(self.get_xml()) + r = ET.parse(x).getroot() + + # TODO: Functions USING fixtures... + +################################## + +class TestCopyingSimpleScene(BasicFileTest): + def test_copy_to_blank_file(self): + self.functions = [] + empty_file = QLCFile(StringIO(self.get_xml())) + + self.functions = [ + snippets.function_scene(0, "Basic Scene"), + snippets.function_scene(1, "Basic Scene 1"), + snippets.function_scene(2, "Basic Scene 2"), + ] + + full_file = QLCFile(StringIO(self.get_xml())) + + clipboard = list(full_file.iter_functions_for_clipboard(['0','1','2'])) + empty_file.paste_functions_here(clipboard) + + new_functions = empty_file.list_functions() + self.assertEqual(new_functions[0].attrib["Name"], "Basic Scene") + self.assertEqual(new_functions[1].attrib["Name"], "Basic Scene 1") + self.assertEqual(new_functions[2].attrib["Name"], "Basic Scene 2") + + self.assertEqual(new_functions[0].attrib["ID"], "0") + self.assertEqual(new_functions[1].attrib["ID"], "1") + self.assertEqual(new_functions[2].attrib["ID"], "2") + + def test_copy_to_nonempty_file(self): + + self.functions = [ + snippets.function_scene(0, "Basic Scene"), + snippets.function_scene(1, "Basic Scene 1"), + snippets.function_scene(2, "Basic Scene 2"), + ] + + to_file = QLCFile(StringIO(self.get_xml())) + + self.functions = [ + snippets.function_scene(0, "Basic Scene A"), + snippets.function_scene(1, "Basic Scene B"), + snippets.function_scene(2, "Basic Scene C"), + ] + + from_file = QLCFile(StringIO(self.get_xml())) + + clipboard = list(from_file.iter_functions_for_clipboard(['0','1','2'])) + to_file.paste_functions_here(clipboard) + + new_functions = to_file.list_functions() + self.assertEqual(len(new_functions), 6) + self.assertEqual(new_functions[0].attrib["Name"], "Basic Scene") + self.assertEqual(new_functions[1].attrib["Name"], "Basic Scene 1") + self.assertEqual(new_functions[2].attrib["Name"], "Basic Scene 2") + self.assertEqual(new_functions[3].attrib["Name"], "Basic Scene A") + self.assertEqual(new_functions[4].attrib["Name"], "Basic Scene B") + self.assertEqual(new_functions[5].attrib["Name"], "Basic Scene C") + + + self.assertEqual(new_functions[0].attrib["ID"], "0") + self.assertEqual(new_functions[1].attrib["ID"], "1") + self.assertEqual(new_functions[2].attrib["ID"], "2") + self.assertEqual(new_functions[3].attrib["ID"], "3") + self.assertEqual(new_functions[4].attrib["ID"], "4") + self.assertEqual(new_functions[5].attrib["ID"], "5") + + + + + +