diff --git a/firmware/tools/widget_preview.py b/firmware/tools/widget_preview.py new file mode 100644 index 00000000..5474c5d1 --- /dev/null +++ b/firmware/tools/widget_preview.py @@ -0,0 +1,275 @@ +# +# copyleft Elliot Alderson from F society +# copyleft Darlene Alderson from F society +# +# This file is part of PortaPack. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# +import re +import tkinter as tk +from typing import Dict, List, Tuple +import argparse +from dataclasses import dataclass + +""" +TODO: Multile class in one file seperation +TODO: Add more widget type + +widget compatible guide: + +1. add the preview color in the "widgets = {" table +(note that this color is only for easier to distinguish) + +2. add the widget type and also regex in the "parsers" table +(note that your regex should apply all the possible constructor overload (re-impl)) + +""" + +# pp hard coded vars +screen_width = 240 +SCREEN_W = 240 +screen_height = 320 +CHARACTER_WIDTH = 8 +LINE_HEIGHT = 16 +topbar_offset = 16 +# widgets actually drew after the top bar, +# for example if Y = 0 then Y on screen actually is 16 + +# scale factor +scale = 2 + +widgets = { + "Button": "lightgray", + "NewButton": "lightyellow", + "Text": "lightblue", + "Rectangle": "lightgreen", + "Image": "pink", + "ImageButton": "lightpink", + "NumberField": "peachpuff", + "ProgressBar": "lightcoral", + "Console": "wheat", + "Checkbox": "plum", + "Label": "lavender", + "TextField": "paleturquoise", + "OptionsField": "palegreen", + "VuMeter": "sandybrown", + "BigFrequency": "khaki" +} + +@dataclass +class Widget: + name: str + widget_type: str + x: int + y: int + width: int + height: int + text: str = "" + +class WidgetParser: + def __init__(self): + + self.parsers: Dict[str, re.Pattern] = { + 'Button': re.compile( # TODO: fix those that using language helper + r'Button\s+(\w+)\s*\{\s*\{([^}]+)\},\s*"([^"]+)"\s*\};', + re.MULTILINE + ), + 'NewButton': re.compile( + r'NewButton\s+(\w+)\s*\{\s*(?:\{([^}]+)\}|{}),\s*(?:"([^"]*)"|\{\}),\s*(?:[^,}]+(?:,\s*[^}]+)*|\{\})\};', + re.MULTILINE + ), + 'Text': re.compile( + r'Text\s+(\w+)\s*\{\s*\{([^}]+)\}(?:\s*,\s*"([^"]*)")?\s*\};', + re.MULTILINE + ), + 'ProgressBar': re.compile( + r'ProgressBar\s+(\w+)\s*\{\s*\{([^}]+)\}\s*\};', + re.MULTILINE + ), + 'Console': re.compile( + r'Console\s+(\w+)\s*\{\s*\{([^}]+)\}\s*\};', + re.MULTILINE + ), + 'Label': re.compile( + r'Label\s+(\w+)\s*\{\s*\{([^}]+)\},\s*"([^"]+)"\s*\};', + re.MULTILINE + ), + 'VuMeter': re.compile( + r'VuMeter\s+(\w+)\s*\{\s*\{([^}]+)\},\s*\d+,\s*(?:true|false)\s*\};', + re.MULTILINE + ), + 'BigFrequency': re.compile( + r'BigFrequency\s+(\w+)\s*\{\s*\{([^}]+)\},\s*\d+\s*\};', + re.MULTILINE + ) + } + + def parse_coordinates(self, coord_str: str) -> Tuple[int, int, int, int]: + """Parse coordinate string like '2 * 8, 8 * 16, 8 * 8, 3 * 16'""" + if not coord_str or coord_str.isspace(): + return (0, 0, 0, 0) # for empty constructor fallback + + # split and evaluate each coordinate expression + coords = [eval(x.strip()) for x in coord_str.split(',')] + + # ensure we have exactly 4 coordinates, pad with zeros if needed + while len(coords) < 4: + coords.append(0) + + # only take the first 4 coordinates if there are more + return tuple(coords[:4]) + + def parse_file(self, filepath: str) -> List[Widget]: + with open(filepath, 'r') as f: + content = f.read() + + widgets = [] + for widget_type, pattern in self.parsers.items(): + matches = pattern.finditer(content) + for match in matches: + name = match.group(1) # Widget name is always in group 1 + coords = self.parse_coordinates(match.group(2) if match.group(2) else "") + + # Only try to get text if the pattern has 3 or more groups + text = "" + try: + if match.lastindex >= 3: + text = match.group(3) if match.group(3) else "" + except IndexError: + pass + + widgets.append(Widget( + name=name, + widget_type=widget_type, + x=coords[0], + y=coords[1], + width=coords[2], + height=coords[3], + text=text + )) + + return widgets + +class WidgetPreview(tk.Tk): + def __init__(self, widgets: List[Widget]): + super().__init__() + self.title("Widget Preview") + + self.canvas = tk.Canvas(self, width=screen_width * scale, height=screen_height * scale, bg='white') + self.canvas.pack(padx=10, pady=10) + + self.all_text_elements = [] + self.draw_widgets(widgets) + + def draw_widgets(self, widgets: List[Widget]): + draw_top_bar(self) + for widget in widgets: + self.draw_widget(widget) + + def draw_widget(self, widget: Widget): + print(f"Drawing widget: {widget.name} ({widget.widget_type})") + + x1 = widget.x * scale + y1 = (widget.y + topbar_offset) * scale + x2 = x1 + (widget.width * scale) + y2 = y1 + (widget.height * scale) + + print(f"Coordinates: ({x1}, {y1}), ({x2}, {y2})") + + if widget.widget_type == "VuMeter": + segment_height = widget.height / 8 * scale + for i in range(8): + self.canvas.create_rectangle( + x1, y1 + (i * segment_height), + x2, y1 + ((i+1) * segment_height), + fill=widgets[widget.widget_type], + outline="gray" + ) + elif widget.widget_type == "BigFrequency": + # Draw 7-segment style display + self.canvas.create_rectangle( + x1, y1, x2, y2, + fill=widgets[widget.widget_type], + outline="gray" + ) + self.canvas.create_text( + (x1 + x2) / 2, + (y1 + y2) / 2, + text="433.92" # placeholder text + ) + + else: + # defualt rendering + rect_id = self.canvas.create_rectangle( + x1, y1, x2, y2, + fill=widgets[widget.widget_type] + ) + + type_text_id = self.canvas.create_text( + (x1 + x2) // 2, + (y1 + y2) // 2, + text=widget.widget_type + ) + + detail_text_id = self.canvas.create_text( + (x1 + x2) // 2, + (y1 + y2) // 2, + text=f"{widget.widget_type}|{widget.name}|{widget.text}", + state='hidden' + ) + + widget_texts = { + 'type': type_text_id, + 'detail': detail_text_id + } + self.all_text_elements.append(widget_texts) + + # hover handlers + def on_enter(event): + for texts in self.all_text_elements: + self.canvas.itemconfig(texts['type'], state='hidden') + self.canvas.itemconfig(texts['detail'], state='hidden') + self.canvas.itemconfig(detail_text_id, state='normal') + self.canvas.tag_raise(detail_text_id) + + def on_leave(event): + for texts in self.all_text_elements: + self.canvas.itemconfig(texts['type'], state='normal') + self.canvas.tag_raise(texts['type']) + self.canvas.itemconfig(texts['detail'], state='hidden') + + for item_id in [rect_id, type_text_id, detail_text_id]: + self.canvas.tag_bind(item_id, '', on_enter) + self.canvas.tag_bind(item_id, '', on_leave) + +def draw_top_bar(self): + self.canvas.create_rectangle(0, 0, screen_width * scale, topbar_offset * scale, fill='lightblue') + self.canvas.create_text(screen_width * scale // 2, topbar_offset * scale // 2, text='I\'m Top Bar, hover mouse on items to check details', fill='black') + +def main(): + parser = argparse.ArgumentParser(description='Preview UI widgets from hpp files') + parser.add_argument('file', help='Path to the hpp file') + args = parser.parse_args() + + widget_parser = WidgetParser() + widgets = widget_parser.parse_file(args.file) + + app = WidgetPreview(widgets) + app.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file