- editor.c: pure C Win32 native editor, compiles in 0.1s - Tree view: auto-expand engine directory - Code editor: Consolas font, multiline - Console: compile/run output - Menu: File/Build/Language/Help - 11 languages: zh/en/ja/ko/es/fr/de/ru/pt/it/tr - F5=Run F7=Build Ctrl+S=Save - Open file via tree double-click - Package exe via Save dialog - Engine folder on Desktop
338 lines
15 KiB
Python
338 lines
15 KiB
Python
"""
|
|
Lity3D Editor - 集成开发环境
|
|
双击打开 → 写代码 → F5 运行 → F7 打包
|
|
支持 11 种语言切换
|
|
"""
|
|
import tkinter as tk
|
|
from tkinter import ttk, filedialog, scrolledtext, messagebox
|
|
import os, subprocess, json, sys, time
|
|
|
|
ENGINE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, ENGINE_DIR)
|
|
import i18n
|
|
|
|
class LityEditor:
|
|
def __init__(self):
|
|
i18n.load_language()
|
|
self.root = tk.Tk()
|
|
self.root.geometry("1400x900")
|
|
self.root.configure(bg="#2b2d30")
|
|
self.current_file = None
|
|
self.process = None
|
|
self._setup_styles()
|
|
self._build_ui()
|
|
i18n.on_language_change(self._refresh_ui)
|
|
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
def _t(self, key):
|
|
return i18n.t(key)
|
|
|
|
def _setup_styles(self):
|
|
s = ttk.Style()
|
|
s.theme_use("clam")
|
|
s.configure("Dark.TFrame", background="#2b2d30")
|
|
s.configure("Dark.TLabel", background="#2b2d30", foreground="#cccccc")
|
|
|
|
def _build_ui(self):
|
|
self._build_menu()
|
|
self._build_toolbar()
|
|
self._build_main()
|
|
self._build_statusbar()
|
|
self._set_status(self._t("ready"))
|
|
|
|
def _refresh_ui(self):
|
|
self.root.title(self._t("app_title"))
|
|
for w in self.root.winfo_children():
|
|
if isinstance(w, tk.Menu):
|
|
w.destroy()
|
|
self._build_menu()
|
|
self._build_toolbar()
|
|
|
|
def _build_menu(self):
|
|
mb = tk.Menu(self.root, bg="#2b2d30", fg="#cccccc")
|
|
self.root.config(menu=mb)
|
|
|
|
fm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
|
|
fm.add_command(label=self._t("new_file"), accelerator="Ctrl+N", command=self._new_file)
|
|
fm.add_command(label=self._t("open_file"), accelerator="Ctrl+O", command=self._open_file)
|
|
fm.add_command(label=self._t("open_folder"), command=self._open_folder)
|
|
fm.add_separator()
|
|
fm.add_command(label=self._t("save"), accelerator="Ctrl+S", command=self._save_file)
|
|
fm.add_command(label=self._t("save_as"), command=self._save_as)
|
|
fm.add_separator()
|
|
fm.add_command(label=self._t("exit"), command=self._on_close)
|
|
mb.add_cascade(label=self._t("menu_file"), menu=fm)
|
|
|
|
em = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
|
|
em.add_command(label=self._t("undo"), accelerator="Ctrl+Z", command=lambda: self.code_text.edit_undo())
|
|
em.add_command(label=self._t("redo"), accelerator="Ctrl+Y", command=lambda: self.code_text.edit_redo())
|
|
em.add_separator()
|
|
em.add_command(label=self._t("cut"), accelerator="Ctrl+X", command=lambda: self.root.focus_get().event_generate("<<Cut>>"))
|
|
em.add_command(label=self._t("copy"), accelerator="Ctrl+C", command=lambda: self.root.focus_get().event_generate("<<Copy>>"))
|
|
em.add_command(label=self._t("paste"), accelerator="Ctrl+V", command=lambda: self.root.focus_get().event_generate("<<Paste>>"))
|
|
em.add_command(label=self._t("select_all"), accelerator="Ctrl+A", command=lambda: self.code_text.tag_add("sel", "1.0", "end"))
|
|
mb.add_cascade(label=self._t("menu_edit"), menu=em)
|
|
|
|
bm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
|
|
bm.add_command(label=self._t("run_game"), accelerator="F5", command=self._run_game)
|
|
bm.add_command(label=self._t("run_file"), accelerator="Ctrl+F5", command=self._run_current)
|
|
bm.add_separator()
|
|
bm.add_command(label=self._t("build_engine"), command=self._build_engine)
|
|
bm.add_command(label=self._t("build_exe"), accelerator="F7", command=self._build_exe)
|
|
bm.add_separator()
|
|
bm.add_command(label=self._t("kill_process"), accelerator="Shift+F5", command=self._kill_process)
|
|
mb.add_cascade(label=self._t("menu_build"), menu=bm)
|
|
|
|
sm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
|
|
sm.add_command(label=self._t("new_scene"), command=self._new_scene)
|
|
sm.add_command(label=self._t("open_scene"), command=self._open_scene)
|
|
sm.add_command(label=self._t("save_scene"), command=self._save_scene)
|
|
sm.add_separator()
|
|
sm.add_command(label=self._t("run_scene"), command=self._run_scene)
|
|
mb.add_cascade(label=self._t("menu_scene"), menu=sm)
|
|
|
|
lm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
|
|
for code, name in i18n.get_all_languages():
|
|
marker = " ✓" if code == i18n.get_language() else ""
|
|
lm.add_command(label=f"{name}{marker}", command=lambda c=code: self._switch_lang(c))
|
|
mb.add_cascade(label=self._t("menu_language"), menu=lm)
|
|
|
|
hm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
|
|
hm.add_command(label=self._t("docs"), command=self._open_docs)
|
|
hm.add_command(label=self._t("about"), command=lambda: messagebox.showinfo(
|
|
self._t("about"), "Lity3D Engine v1.0\nC+MASM Game Engine\nPython Scripting + D3D11\n11 Languages"))
|
|
mb.add_cascade(label=self._t("menu_help"), menu=hm)
|
|
|
|
self.root.bind("<Control-n>", lambda e: self._new_file())
|
|
self.root.bind("<Control-o>", lambda e: self._open_file())
|
|
self.root.bind("<Control-s>", lambda e: self._save_file())
|
|
self.root.bind("<F5>", lambda e: self._run_game())
|
|
self.root.bind("<F7>", lambda e: self._build_exe())
|
|
self.root.bind("<Shift-F5>", lambda e: self._kill_process())
|
|
|
|
def _switch_lang(self, code):
|
|
i18n.set_language(code)
|
|
i18n.save_language(code)
|
|
self._refresh_ui()
|
|
|
|
def _build_toolbar(self):
|
|
if hasattr(self, '_toolbar') and self._toolbar:
|
|
self._toolbar.destroy()
|
|
self._toolbar = ttk.Frame(self.root, style="Dark.TFrame")
|
|
self._toolbar.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2)
|
|
|
|
ttk.Button(self._toolbar, text=self._t("new_file"), command=self._new_file).pack(side=tk.LEFT, padx=1)
|
|
ttk.Button(self._toolbar, text=self._t("open_file"), command=self._open_file).pack(side=tk.LEFT, padx=1)
|
|
ttk.Button(self._toolbar, text=self._t("save"), command=self._save_file).pack(side=tk.LEFT, padx=1)
|
|
ttk.Separator(self._toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4)
|
|
ttk.Button(self._toolbar, text=self._t("run") + " (F5)", command=self._run_game).pack(side=tk.LEFT, padx=2)
|
|
ttk.Button(self._toolbar, text=self._t("stop"), command=self._kill_process).pack(side=tk.LEFT, padx=1)
|
|
ttk.Button(self._toolbar, text=self._t("build_exe") + " (F7)", command=self._build_exe).pack(side=tk.LEFT, padx=2)
|
|
ttk.Separator(self._toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4)
|
|
ttk.Button(self._toolbar, text=self._t("new_scene"), command=self._new_scene).pack(side=tk.LEFT, padx=1)
|
|
ttk.Button(self._toolbar, text=self._t("save_scene"), command=self._save_scene).pack(side=tk.LEFT, padx=1)
|
|
|
|
def _build_main(self):
|
|
main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
|
|
main.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
|
|
|
left = ttk.Frame(main, style="Dark.TFrame")
|
|
main.add(left, weight=1)
|
|
ttk.Label(left, text=self._t("explorer").upper(), style="Dark.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W, padx=5, pady=3)
|
|
self.file_tree = ttk.Treeview(left, show="tree", height=20)
|
|
self.file_tree.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
|
self.file_tree.bind("<Double-1>", self._on_tree_double)
|
|
self._refresh_tree()
|
|
|
|
right = ttk.Frame(main, style="Dark.TFrame")
|
|
main.add(right, weight=3)
|
|
self.notebook = ttk.Notebook(right)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True)
|
|
self.editor_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.editor_frame, text="editor.py")
|
|
self.code_text = scrolledtext.ScrolledText(self.editor_frame, bg="#1e1e1e", fg="#d4d4d4", insertbackground="#d4d4d4", font=("Consolas", 12), wrap=tk.NONE, undo=True)
|
|
self.code_text.pack(fill=tk.BOTH, expand=True)
|
|
|
|
bottom = ttk.Frame(right, style="Dark.TFrame")
|
|
bottom.pack(fill=tk.BOTH, expand=False)
|
|
self.console = scrolledtext.ScrolledText(bottom, bg="#1e1e1e", fg="#cccccc", font=("Consolas", 10), height=8, state=tk.DISABLED)
|
|
self.console.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
|
|
|
def _build_statusbar(self):
|
|
self.statusbar = ttk.Frame(self.root, style="Dark.TFrame")
|
|
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
|
|
self.status_label = ttk.Label(self.statusbar, text="", style="Dark.TLabel")
|
|
self.status_label.pack(side=tk.LEFT, padx=5)
|
|
self.process_label = ttk.Label(self.statusbar, text="", style="Dark.TLabel", foreground="#6a9955")
|
|
self.process_label.pack(side=tk.RIGHT, padx=5)
|
|
|
|
def _log(self, msg, tag="info"):
|
|
self.console.config(state=tk.NORMAL)
|
|
ts = time.strftime("%H:%M:%S")
|
|
pfx = {"info": " ", "error": "!!", "success": "OK"}[tag]
|
|
self.console.insert(tk.END, f"[{ts}] {pfx} {msg}\n")
|
|
self.console.see(tk.END)
|
|
self.console.config(state=tk.DISABLED)
|
|
|
|
def _set_status(self, msg):
|
|
self.status_label.config(text=msg)
|
|
|
|
def _refresh_tree(self):
|
|
if not hasattr(self, 'file_tree'): return
|
|
self.file_tree.delete(*self.file_tree.get_children())
|
|
root = self.file_tree.insert("", "end", text="lity3d", open=True)
|
|
skip = {".git", ".gitignore", "build", "__pycache__", ".lang", ".vs", ".vscode", "obj"}
|
|
for item in sorted(os.listdir(ENGINE_DIR)):
|
|
if item.startswith(".") or item in skip: continue
|
|
full = os.path.join(ENGINE_DIR, item)
|
|
if os.path.isdir(full):
|
|
dn = self.file_tree.insert(root, "end", text=item)
|
|
try:
|
|
for f in sorted(os.listdir(full)):
|
|
if f.startswith(".") or f.endswith((".pyc", ".exe", ".o", ".a")): continue
|
|
self.file_tree.insert(dn, "end", text=f, values=(os.path.join(item, f),))
|
|
except: pass
|
|
else:
|
|
self.file_tree.insert(root, "end", text=item, values=(item,))
|
|
|
|
def _on_tree_double(self, event):
|
|
sel = self.file_tree.selection()
|
|
if not sel: return
|
|
item = self.file_tree.item(sel[0])
|
|
vals = item.get("values", ())
|
|
path = vals[0] if vals else item["text"]
|
|
full = os.path.join(ENGINE_DIR, path) if not os.path.isabs(path) else path
|
|
if os.path.isfile(full): self._open_file_at(full)
|
|
|
|
def _new_file(self):
|
|
self.code_text.delete("1.0", tk.END)
|
|
self.current_file = None
|
|
self.notebook.tab(self.editor_frame, text="untitled*")
|
|
self._set_status(self._t("new_file_msg"))
|
|
|
|
def _open_file(self):
|
|
p = filedialog.askopenfilename(initialdir=ENGINE_DIR, filetypes=[("Python", "*.py"), ("C", "*.c *.h"), ("All", "*.*")])
|
|
if p: self._open_file_at(p)
|
|
|
|
def _open_file_at(self, path):
|
|
try:
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
self.code_text.delete("1.0", tk.END)
|
|
self.code_text.insert("1.0", f.read())
|
|
self.current_file = path
|
|
name = os.path.basename(path)
|
|
self.notebook.tab(self.editor_frame, text=name)
|
|
self._log(f"{self._t('opened')} {name}", "success")
|
|
except Exception as e:
|
|
self._log(str(e), "error")
|
|
|
|
def _open_folder(self):
|
|
filedialog.askdirectory(initialdir=ENGINE_DIR)
|
|
|
|
def _save_file(self):
|
|
if not self.current_file: self._save_as(); return
|
|
try:
|
|
with open(self.current_file, "w", encoding="utf-8") as f:
|
|
f.write(self.code_text.get("1.0", tk.END))
|
|
name = os.path.basename(self.current_file)
|
|
self.notebook.tab(self.editor_frame, text=name)
|
|
self._log(f"{self._t('saved')} {name}", "success")
|
|
except Exception as e:
|
|
self._log(str(e), "error")
|
|
|
|
def _save_as(self):
|
|
p = filedialog.asksaveasfilename(initialdir=ENGINE_DIR, defaultextension=".py", filetypes=[("Python", "*.py"), ("All", "*.*")])
|
|
if p: self.current_file = p; self._save_file()
|
|
|
|
def _run_game(self):
|
|
exe = os.path.join(ENGINE_DIR, "build", "lity.exe")
|
|
if os.path.exists(exe): self._run_process([exe])
|
|
else: self._log(self._t("build_first"), "error")
|
|
|
|
def _run_current(self):
|
|
if not self.current_file: self._log(self._t("no_file"), "error"); return
|
|
ext = os.path.splitext(self.current_file)[1]
|
|
if ext == ".py": self._run_process([sys.executable, self.current_file])
|
|
else: self._log(self._t("cant_run"), "error")
|
|
|
|
def _run_scene(self):
|
|
exe = os.path.join(ENGINE_DIR, "build", "lity.exe")
|
|
if os.path.exists(exe): self._run_process([exe])
|
|
else: self._log(self._t("build_first"), "error")
|
|
|
|
def _run_process(self, cmd):
|
|
self._kill_process()
|
|
self._log(f"> {' '.join(os.path.basename(c) for c in cmd)}")
|
|
self.process_label.config(text=self._t("running"))
|
|
try:
|
|
self.process = subprocess.Popen(cmd, cwd=ENGINE_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
self._poll()
|
|
except Exception as e:
|
|
self._log(str(e), "error")
|
|
self.process_label.config(text="")
|
|
|
|
def _poll(self):
|
|
if self.process and self.process.poll() is None:
|
|
line = self.process.stdout.readline()
|
|
if line:
|
|
self._log(line.decode(errors="replace").rstrip())
|
|
self.root.after(50, self._poll)
|
|
elif self.process:
|
|
self._log(f"{self._t('process_exit')} {self.process.returncode}", "success" if self.process.returncode == 0 else "error")
|
|
self.process_label.config(text="")
|
|
self.process = None
|
|
|
|
def _kill_process(self):
|
|
if self.process:
|
|
self.process.kill()
|
|
self._log(self._t("kill_done"), "error")
|
|
self.process_label.config(text="")
|
|
self.process = None
|
|
|
|
def _build_engine(self):
|
|
d = os.path.join(ENGINE_DIR, "build")
|
|
os.makedirs(d, exist_ok=True)
|
|
self._run_process(["cmake", "--build", d])
|
|
|
|
def _build_exe(self):
|
|
self._save_file()
|
|
exe = os.path.join(ENGINE_DIR, "build", "lity.exe")
|
|
if os.path.exists(exe):
|
|
out = filedialog.asksaveasfilename(initialdir=ENGINE_DIR, defaultextension=".exe", filetypes=[("EXE", "*.exe")])
|
|
if out:
|
|
import shutil
|
|
shutil.copy2(exe, out)
|
|
self._log(f"{self._t('packaged')} {out}", "success")
|
|
else:
|
|
self._log(self._t("build_first"), "error")
|
|
|
|
def _new_scene(self):
|
|
scene = {"entities": [], "camera": {"pos": [0, 5, -10], "target": [0, 0, 0]}}
|
|
path = filedialog.asksaveasfilename(initialdir=ENGINE_DIR, defaultextension=".json", filetypes=[("Scene", "*.json")])
|
|
if path:
|
|
with open(path, "w") as f: json.dump(scene, f, indent=2)
|
|
self.current_file = path
|
|
self._open_file_at(path)
|
|
self._log(self._t("new_scene_ok"), "success")
|
|
|
|
def _open_scene(self):
|
|
p = filedialog.askopenfilename(initialdir=ENGINE_DIR, filetypes=[("Scene", "*.json *.txt")])
|
|
if p: self._open_file_at(p)
|
|
|
|
def _save_scene(self):
|
|
self._save_file()
|
|
|
|
def _open_docs(self):
|
|
d = os.path.join(ENGINE_DIR, "Engine", "Scripting", "lity.py")
|
|
if os.path.exists(d): self._open_file_at(d)
|
|
|
|
def _on_close(self):
|
|
if self.process: self.process.kill()
|
|
self.root.destroy()
|
|
|
|
def run(self):
|
|
self.root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
LityEditor().run()
|