Add widget preview tool (#2520)

* PoC

* opt

* opt
This commit is contained in:
sommermorgentraum 2025-02-16 23:42:06 +08:00 committed by GitHub
parent 200f10397b
commit 7ad4ad99dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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, '<Enter>', on_enter)
self.canvas.tag_bind(item_id, '<Leave>', 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()