ClassWizard.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. r'''
  8. ClassWizard.py
  9. GUI Mode Examples:
  10. Windows:
  11. PS C:\o3de> C:\o3de\python\python.cmd '.\Tools\ClassCreationWizard\ClassWizard.py' `
  12. --engine-path C:\o3de `
  13. --project-path C:\o3de\user\myproject
  14. Linux:
  15. $ ~/o3de$ ./python/python.sh Tools/ClassCreationWizard/ClassWizard.py \
  16. --engine-path /home/yourusername/o3de \
  17. --project-path /home/yourusername/o3de/user/myproject
  18. Example GUI Input:
  19. Component Details:
  20. Component Name: Image
  21. Component Type: Default
  22. Namespace: myproject
  23. Project Directory: C:\o3de\user\myproject\Gem
  24. Settings:
  25. [X] Add to project
  26. [X] Default License
  27. Non-GUI Mode Examples:
  28. Activated using the flags: --component-name
  29. Windows:
  30. PS C:\o3de> C:\o3de\python\python.cmd '.\Tools\ClassCreationWizard\ClassWizard.py' `
  31. --engine-path C:\o3de `
  32. --project-path C:\o3de\user\myproject `
  33. --component-name Image `
  34. --component-type Default `
  35. --namespace myproject `
  36. --add-to-project `
  37. --default-license
  38. Linux:
  39. $ ~/o3de$ ./python/python.sh Tools/ClassCreationWizard/ClassWizard.py \
  40. --engine-path $HOME/o3de/ \
  41. --project-path $HOME/o3de/user/myproject \
  42. --component-name Image \
  43. --component-type Default \
  44. --namespace myproject \
  45. --add-to-project \
  46. --default-license
  47. Required Arguments:
  48. --engine-path PATH Path to O3DE engine root
  49. Optional Arguments:
  50. --project-path PATH Path to O3DE project (required for non-GUI)
  51. --component-name NAME Component name (required for non-GUI)
  52. --component-type TYPE Component type: Default or Editor (required for non-GUI)
  53. --namespace NAME Component namespace (required for non-GUI)
  54. --add-to-project Automatically add to project's Gem folder
  55. --default-license Include default license
  56. '''
  57. import argparse
  58. import os
  59. import subprocess
  60. import sys
  61. import traceback
  62. from pathlib import Path
  63. import tkinter as tk
  64. import tkinter.font as tkFont
  65. from tkinter import filedialog, ttk
  66. LOCK_FILE = Path(__file__).parent / ".lock"
  67. def check_instance() -> bool:
  68. """Check if another instance is running"""
  69. if LOCK_FILE.exists():
  70. print("Another instance may already be running.")
  71. return False
  72. LOCK_FILE.touch()
  73. return True
  74. def remove_lock():
  75. """Remove the lock file on exit"""
  76. try:
  77. if LOCK_FILE.exists():
  78. LOCK_FILE.unlink()
  79. except:
  80. pass
  81. def validate_path(path: str) -> Path:
  82. """Validates path"""
  83. try:
  84. _path = Path(os.path.expanduser(path)).resolve()
  85. if not _path.exists():
  86. raise argparse.ArgumentTypeError(f"Path does not exist: {_path.name}")
  87. if not _path.is_dir():
  88. raise argparse.ArgumentTypeError(f"Not a directory: {_path.name}")
  89. return _path
  90. except Exception as e:
  91. raise argparse.ArgumentTypeError(f"Invalid path: {str(e)}")
  92. def validate_engine_path(path: str) -> Path:
  93. """Validates an O3DE engine path"""
  94. engine_path = validate_path(path)
  95. if not (engine_path / "engine.json").exists():
  96. raise argparse.ArgumentTypeError(
  97. f" Not a valid O3DE engine directory: {engine_path}\n"
  98. " Hint: engine.json file not found.\n"
  99. " Make sure you're pointing to the root of an O3DE engine directory.\n"
  100. " Example: --engine-path C:\\o3de"
  101. )
  102. return engine_path
  103. def validate_component_name(component_name, log=None) -> bool:
  104. """Validate the component name"""
  105. KEYWORDS = {
  106. 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto',
  107. 'bitand', 'bitor', 'bool', 'break', 'case', 'catch',
  108. 'char', 'char8_t', 'char16_t', 'char32_t', 'class', 'compl',
  109. 'concept', 'const', 'consteval', 'constexpr', 'const_cast', 'continue',
  110. 'co_await', 'co_return', 'co_yield', 'decltype', 'default', 'delete',
  111. 'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit',
  112. 'export', 'extern', 'false', 'float', 'for', 'friend',
  113. 'goto', 'if', 'inline', 'int', 'long', 'mutable',
  114. 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr',
  115. 'operator', 'or', 'or_eq', 'private', 'protected', 'public',
  116. 'register', 'reinterpret_cast','requires', 'return', 'short', 'signed',
  117. 'sizeof', 'static', 'static_assert','static_cast', 'struct', 'switch',
  118. 'template', 'this', 'thread_local', 'throw', 'true', 'try',
  119. 'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using',
  120. 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor',
  121. 'xor_eq'
  122. }
  123. if not component_name:
  124. if log:
  125. log("Error: The name cannot be empty.")
  126. return False
  127. if (invalid := next((c for c in '*?+-,;=&%$`"\'/\\[]{}~#|<>!^@()#: \t\n\r\f\v' if c in component_name), None)):
  128. log and log(f"The name contains invalid character: {invalid}");
  129. return False
  130. if (
  131. not (component_name[0].isalpha() or component_name[0] == '_')
  132. or component_name.startswith('__')
  133. or (component_name.startswith('_') and len(component_name) > 1 and component_name[1].isupper())
  134. ):
  135. if log:
  136. log("Error: The name must start with a letter or single underscore.")
  137. return False
  138. if component_name in KEYWORDS:
  139. if log:
  140. log(f"Error: '{component_name}' is a C++ keyword. Please choose a different name.")
  141. return False
  142. return True
  143. def add_component_to_project(component_path: Path, component_name: str, namespace: str, log=None) -> bool:
  144. """Automatically integrates the component into the project's Gem folder."""
  145. def log_message(message):
  146. if log:
  147. log(message)
  148. else:
  149. print(message)
  150. try:
  151. log_message(f"Adding component to the project...")
  152. # Update {namespace}Module.cpp
  153. module_path = component_path / "Source" / f"{namespace}Module.cpp"
  154. if not module_path.exists():
  155. log_message(f"Error: Module file not found at {module_path}")
  156. log_message(" Hint: Make sure the Project Directory points to a valid Gem directory, not the root of a project. Usually it's where *_files.cmake resides.")
  157. return False
  158. with open(module_path, 'r', encoding='utf-8') as f:
  159. lines = f.read().splitlines()
  160. include_line = f'#include "{component_name}Component.h"'
  161. descriptor_line = f'{component_name}Component::CreateDescriptor()'
  162. # Insert include if not present
  163. if not any(include_line in line for line in lines):
  164. last_include_idx = max(i for i, line in enumerate(lines) if line.strip().startswith('#include'))
  165. lines.insert(last_include_idx + 1, include_line)
  166. # Insert descriptor if not present
  167. descriptor_inserted = any(descriptor_line in line for line in lines)
  168. if not descriptor_inserted:
  169. for i, line in enumerate(lines):
  170. if 'm_descriptors.insert' in line:
  171. insert_start = i
  172. break
  173. else:
  174. insert_start = -1
  175. if insert_start != -1:
  176. # Find the line with closing "});"
  177. for j in range(insert_start, len(lines)):
  178. if '});' in lines[j]:
  179. descriptor_end = j
  180. break
  181. else:
  182. descriptor_end = -1
  183. if descriptor_end != -1:
  184. # Determine indentation
  185. for k in range(insert_start, descriptor_end):
  186. if 'CreateDescriptor()' in lines[k]:
  187. indent = lines[k][:len(lines[k]) - len(lines[k].lstrip())]
  188. break
  189. else:
  190. indent = ' ' * 16
  191. # Insert new descriptor before closing
  192. prev_line_idx = descriptor_end - 1
  193. if not lines[prev_line_idx].strip().endswith(','):
  194. lines[prev_line_idx] = lines[prev_line_idx].rstrip() + ','
  195. # Insert new descriptor line
  196. lines.insert(descriptor_end, f'{indent}{descriptor_line},')
  197. # Write the generated content to the module_path with UTF-8 encoding
  198. with open(module_path, 'w', encoding='utf-8', newline='\n') as f:
  199. f.write('\n'.join(lines) + '\n')
  200. # Update {namespace}_files.cmake
  201. project_files_path = component_path / f"{namespace.lower()}_files.cmake"
  202. if not project_files_path.exists():
  203. log_message(f"Error: Could not find {project_files_path}")
  204. return False
  205. with open(project_files_path, 'r', encoding='utf-8') as f:
  206. content = f.read()
  207. # Add .h/.cpp files if not present
  208. new_files = [
  209. f' Source/{component_name}Component.cpp\n',
  210. f' Source/{component_name}Component.h\n'
  211. ]
  212. files_section_start = content.find('set(FILES')
  213. if files_section_start != -1:
  214. files_section_end = content.find(')', files_section_start)
  215. if files_section_end != -1:
  216. for new_file in new_files:
  217. if new_file not in content:
  218. content = content[:files_section_end] + new_file + content[files_section_end:]
  219. # Write the generated content to the project_files_path with UTF-8 encoding
  220. with open(project_files_path, 'w', encoding='utf-8') as f:
  221. f.write(content)
  222. return True
  223. except Exception as e:
  224. log_message(f"Error adding component: {str(e)}")
  225. return False
  226. def create_default_component(engine_path, project_dir, namespace, component_name,
  227. add_to_project=False, default_license=False, log=None) -> bool:
  228. """Creates a new default component with the specified parameters."""
  229. def log_message(message):
  230. if log:
  231. log(message)
  232. else:
  233. print(message)
  234. try:
  235. script_name = "o3de.bat" if sys.platform == "win32" else "o3de.sh"
  236. o3de_script = Path(engine_path) / "scripts" / script_name
  237. cmd = [
  238. str(o3de_script),
  239. "create-from-template",
  240. "-dp", str(project_dir),
  241. "-dn", component_name,
  242. "-tn", "DefaultComponent",
  243. "-r", "${GemName}", namespace
  244. ]
  245. if default_license:
  246. cmd.append("--keep-license-text")
  247. cmd.append("--force")
  248. log(f"Creating component: {component_name}...")
  249. result = subprocess.run(
  250. cmd,
  251. cwd=engine_path,
  252. stdout=subprocess.PIPE,
  253. stderr=subprocess.PIPE,
  254. text=True,
  255. check=True
  256. )
  257. if result.stdout:
  258. for line in result.stderr.splitlines():
  259. if not line or line.replace('.', '').isdigit():
  260. continue
  261. if '[INFO]' not in line and '[WARNING]' not in line:
  262. log_message("" + line)
  263. if result.stderr:
  264. for line in result.stderr.splitlines():
  265. if '[INFO]' not in line and '[WARNING]' not in line:
  266. log_message("" + line)
  267. log_message(f"Successfully created component: {component_name}")
  268. if add_to_project:
  269. success = add_component_to_project(Path(project_dir), component_name, namespace, log)
  270. if not success:
  271. log_message("Warning: Failed to automatically add the component to the project.")
  272. else:
  273. log_message("Successfully added component. The project may need to be rebuilt.")
  274. return True
  275. except subprocess.CalledProcessError as e:
  276. log_message(f"Failed to create component (exit code {e.returncode})")
  277. if e.stderr:
  278. log_message("" + e.stderr)
  279. return False
  280. except Exception as e:
  281. log_message(f"Error: {str(e)}")
  282. return False
  283. def create_editor_component(engine_path, project_dir, namespace, component_name,
  284. add_to_project=False, default_license=False, log=None) -> bool:
  285. """Creates an editor component with the specified parameters."""
  286. if log:
  287. log("Error: Editor component is not yet implemented. Please use 'Default' component type.")
  288. return False
  289. def create_component(engine_path, project_dir, namespace, component_name,
  290. component_type="Default", add_to_project=False, default_license=False, log=None)-> bool:
  291. """Creates a new O3DE component of the specified type."""
  292. if component_type == "Default":
  293. status = create_default_component(
  294. engine_path=engine_path,
  295. project_dir=project_dir,
  296. namespace=namespace,
  297. component_name=component_name,
  298. add_to_project=add_to_project,
  299. default_license=default_license,
  300. log=print
  301. )
  302. elif component_type == "Editor":
  303. status = create_editor_component(
  304. engine_path=engine_path,
  305. project_dir=project_path,
  306. namespace=namespace,
  307. component_name=component_name,
  308. add_to_project=add_to_project,
  309. default_license=default_license,
  310. log=print
  311. )
  312. return status
  313. class Tooltip:
  314. """Tooltip class for displaying text when hovering over a widget."""
  315. def __init__(self, widget, text, delay=515):
  316. self.widget = widget
  317. self.text = text
  318. self.tooltip = None
  319. self.delay = delay
  320. self.id = None
  321. self.widget.bind("<Enter>", self.start_timer)
  322. self.widget.bind("<Leave>", self.hide)
  323. def show(self, event=None):
  324. """Display the tooltip near the mouse pointer when hovering over the widget."""
  325. if self.tooltip:
  326. return
  327. x, y, _, _ = self.widget.bbox("insert")
  328. x += self.widget.winfo_rootx() + 25
  329. y += self.widget.winfo_rooty() + 25
  330. self.tooltip = tk.Toplevel(self.widget)
  331. self.tooltip.wm_overrideredirect(True)
  332. self.tooltip.wm_geometry(f"+{x}+{y}")
  333. label = ttk.Label(
  334. self.tooltip,
  335. text=self.text,
  336. background="#ffffe0",
  337. foreground="black",
  338. relief="solid",
  339. borderwidth=0,
  340. padding=5,
  341. wraplength=380)
  342. label.pack()
  343. def start_timer(self, event=None):
  344. self.id = self.widget.after(self.delay, self.show)
  345. def hide(self, event=None):
  346. """Hide the tooltip when the mouse leaves the widget."""
  347. if self.id:
  348. self.widget.after_cancel(self.id)
  349. self.id = None
  350. if self.tooltip:
  351. self.tooltip.destroy()
  352. self.tooltip = None
  353. class NewComponentWindow:
  354. """GUI window for creating a new component in an O3DE project.
  355. This class allows users to define settings such as the component name, type,
  356. namespace, and project location. It supports automatic integration with a project's Gem folder.
  357. """
  358. def __init__(self, root, engine_path, project_path):
  359. self.root = root
  360. self.engine_path = engine_path
  361. self.project_path = project_path
  362. self.namespace = tk.StringVar(value=project_path.parent.stem if project_path else "")
  363. self.root.title("Add C++ Component")
  364. self.root.minsize(300, 480) if sys.platform == "win32" else self.root.minsize(300, 500)
  365. self.root.geometry("500x480") if sys.platform == "win32" else self.root.geometry("500x500")
  366. self.root.protocol("WM_DELETE_WINDOW", self.close_window)
  367. self.root.columnconfigure(1, weight=1)
  368. # default style to all ttk widgets
  369. style = ttk.Style()
  370. style.theme_use('clam')
  371. self.root.configure(bg="#444444")
  372. self.root.option_add("*TEntry.Font", ("TkDefaultFont", 10))
  373. self.root.option_add("*TCombobox.Font", ("TkDefaultFont", 10))
  374. default_font = tkFont.nametofont("TkDefaultFont")
  375. default_font.configure(family="Sans Serif", size=10)
  376. style.configure('.', background='#444444', foreground='#8C8C8C')
  377. style.configure("C.TLabelframe", background="#444444", bordercolor="#4E4E4E", borderwidth=1, relief="solid")
  378. style.configure("C.TButton", background="#444444", bordercolor="#4E4E4E", borderwidth=1, relief="solid")
  379. # Main container
  380. main_frame = ttk.Frame(root, padding="10")
  381. main_frame.pack(fill=tk.BOTH, expand=True)
  382. # Component Details
  383. details_frame = ttk.LabelFrame(main_frame, text=" Component Details ", padding="10", style="C.TLabelframe")
  384. details_frame.pack(fill=tk.X, pady=5)
  385. # Configure grid for alignment
  386. for i in range(3):
  387. details_frame.columnconfigure(i, weight=1 if i == 1 else 0)
  388. # Row 0: Component Name
  389. ttk.Label(details_frame, text="Component Name:").grid(
  390. row=0, column=0, sticky="e", padx=5, pady=5)
  391. self.component_name = ttk.Entry(details_frame)
  392. self.component_name.grid(row=0, column=1, columnspan=1, sticky="ew", padx=5, pady=5)
  393. Tooltip(self.component_name, text="Enter the base name of your C++ component. \nThe template appends the word 'Component'.")
  394. # Row 1: Component Type
  395. ttk.Label(details_frame, text="Component Type:").grid(
  396. row=1, column=0, sticky="e", padx=5, pady=5)
  397. def on_component_select(event):
  398. """Component type selection"""
  399. if self.component_type.get() == "Editor":
  400. self.component_type.set("Default")
  401. self.clear_log()
  402. self.log_message("Info: Editor type is not yet implemented.")
  403. self.component_type = ttk.Combobox(
  404. details_frame,
  405. values=["Default", ("Editor")],
  406. state="readonly",
  407. width=18)
  408. self.component_type.current(0)
  409. self.component_type.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
  410. self.component_type.bind("<<ComboboxSelected>>", on_component_select)
  411. Tooltip(self.component_type, "Select component type: 'Default' for runtime, 'Editor' for editor-specific functionality.")
  412. # Row 2: Namespace
  413. ttk.Label(details_frame, text="Namespace:").grid(
  414. row=2, column=0, sticky="e", padx=5, pady=5)
  415. self.namespace_entry = ttk.Entry(
  416. details_frame,
  417. textvariable=self.namespace)
  418. self.namespace_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=5)
  419. Tooltip(self.namespace_entry, "Enter the C++ namespace for your component.\nThis is usually your project name.")
  420. # Empty cell for alignment
  421. ttk.Frame(details_frame, width=10).grid(row=2, column=2)
  422. # Row 3: Project Directory
  423. ttk.Label(details_frame, text="Project Directory:").grid(
  424. row=3, column=0, sticky="e", padx=5, pady=5)
  425. self.project_dir_var = tk.StringVar(value=str(project_path))
  426. self.project_dir_entry = ttk.Entry(
  427. details_frame,
  428. textvariable=self.project_dir_var)
  429. self.project_dir_entry.grid(row=3, column=1, sticky="ew", padx=5, pady=5)
  430. Tooltip(
  431. self.project_dir_entry,
  432. "Specifies the destination directory where the component will be created. "
  433. "To automatically add the component to the project, this must point to a valid Gem folder within the project. "
  434. "In that case, 'Add to project' must be checked.")
  435. # Browse Button
  436. self.browse_btn = ttk.Button(
  437. details_frame,
  438. text="...",
  439. width=3,
  440. command=self.browse_project_dir,
  441. style="C.TButton")
  442. self.browse_btn.grid(row=3, column=2, sticky="e", padx=5, pady=5)
  443. Tooltip(self.browse_btn, "Browse for a different project's Gem folder or destination directory.")
  444. # Settings Section
  445. settings_frame = ttk.LabelFrame(main_frame, text=" Settings ", padding="10", style="C.TLabelframe")
  446. settings_frame.pack(fill=tk.X, pady=5)
  447. # Checkboxes
  448. self.add_to_project = tk.BooleanVar(value=False)
  449. cmake_cb = ttk.Checkbutton(
  450. settings_frame,
  451. text="Add to project",
  452. variable=self.add_to_project,
  453. onvalue=True,
  454. offvalue=False)
  455. cmake_cb.pack(anchor="w", pady=2)
  456. Tooltip(cmake_cb, "Automatically add this component to the Gem's private CMake source files.")
  457. self.default_license = tk.BooleanVar(value=False)
  458. license_cb = ttk.Checkbutton(
  459. settings_frame,
  460. text="Default License",
  461. variable=self.default_license,
  462. onvalue=True,
  463. offvalue=False)
  464. license_cb.pack(anchor="w", pady=2)
  465. Tooltip(license_cb, "Include the default license header in the source files.")
  466. # Log Section
  467. log_frame = ttk.LabelFrame(main_frame, text=" Log ", padding="10", style="C.TLabelframe")
  468. log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
  469. self.log_text = tk.Text(log_frame, height=3, state="disabled", bg="#444444", fg="#c4c4c4", relief="flat", bd=0,
  470. highlightthickness=0, highlightbackground="#444444", highlightcolor="#444444")
  471. self.log_text.pack(fill=tk.BOTH, expand=True)
  472. # Button Frame
  473. button_frame = ttk.Frame(main_frame)
  474. button_frame.pack(fill=tk.X, pady=5)
  475. ok_btn = ttk.Button(
  476. button_frame,
  477. text="Create",
  478. command=self.on_ok,
  479. style="C.TButton")
  480. ok_btn.pack(side="right", padx=5)
  481. Tooltip(ok_btn, "Create the component using the specified settings.")
  482. cancel_btn = ttk.Button(
  483. button_frame,
  484. text="Cancel",
  485. command=self.on_cancel,
  486. style="C.TButton")
  487. cancel_btn.pack(side="right")
  488. Tooltip(cancel_btn, "Close this window without creating a component.")
  489. def close_window(self):
  490. """Centralized for all close operations"""
  491. remove_lock()
  492. self.root.destroy()
  493. def log_message(self, message):
  494. """ Append a message to the log frame"""
  495. self.log_text.config(state="normal")
  496. self.log_text.insert("end", message + "\n")
  497. self.log_text.see("end")
  498. self.log_text.config(state="disabled")
  499. def clear_log(self):
  500. """Clear all content from the log"""
  501. self.log_text.config(state="normal")
  502. self.log_text.delete("1.0", "end")
  503. self.log_text.config(state="disabled")
  504. def browse_project_dir(self):
  505. """Open directory dialog to select project path"""
  506. selected_path = filedialog.askdirectory(
  507. title="Select Project Directory",
  508. initialdir=self.project_dir_var.get())
  509. if selected_path:
  510. self.project_dir_var.set(selected_path)
  511. self.clear_log()
  512. self.log_message(f"Project directory: {selected_path}")
  513. def on_cancel(self):
  514. """Close the window"""
  515. self.close_window()
  516. def on_ok(self):
  517. """Create the component using the specified settings"""
  518. self.clear_log()
  519. component_name = self.component_name.get().strip()
  520. if not component_name:
  521. self.log_message("Error: Component name is required!")
  522. return
  523. if not validate_component_name(component_name, log=self.log_message):
  524. return
  525. namespace = self.namespace.get().strip()
  526. if not namespace:
  527. self.log_message("Error: Namespace is required!")
  528. return
  529. if not validate_component_name(namespace, log=self.log_message):
  530. return
  531. component_type = self.component_type.get()
  532. project_dir = self.project_dir_var.get().strip()
  533. if not os.path.isdir(project_dir):
  534. self.log_message(f"Error: Project directory {project_dir} does not exist.")
  535. return
  536. add_to_project=self.add_to_project.get()
  537. self.log_message("Please wait...")
  538. self.root.update_idletasks()
  539. if component_type == "Default":
  540. create_default_component(engine_path=self.engine_path, project_dir=project_dir, component_name=component_name,
  541. namespace=namespace, add_to_project=add_to_project, default_license=self.default_license.get(), log=self.log_message)
  542. elif component_type == "Editor":
  543. create_editor_component(engine_path=self.engine_path, project_dir=project_dir, component_name=component_name,
  544. namespace=namespace, add_to_project=add_to_project, default_license=self.default_license.get(), log=self.log_message)
  545. def main():
  546. """
  547. Supports both GUI and command-line modes.
  548. Parses command line arguments, validates paths, and initiates either:
  549. GUI mode: Interactive Tkinter interface for creating components
  550. Non-GUI mode: Automated component creation using command-line arguments
  551. """
  552. # Check if an instance of the application is already running
  553. if not check_instance():
  554. sys.exit(1)
  555. try:
  556. # Command line arguments for the script
  557. # GUI mode
  558. parser = argparse.ArgumentParser()
  559. parser.add_argument("--engine-path", required=True, type=validate_engine_path, help="Path to O3DE engine")
  560. parser.add_argument("--project-path", nargs='?', default=None, type=validate_path, help="Path to O3DE project")
  561. # Non-GUI mode
  562. parser.add_argument("--component-name", help="Component name")
  563. parser.add_argument("--component-type", choices=["Default", "Editor"], help="Default or Editor")
  564. parser.add_argument("--namespace", help="Namespace")
  565. parser.add_argument("--default-license", action="store_true", help="Include default license")
  566. parser.add_argument("--add-to-project", action="store_true", help="Add to project's Gem folder")
  567. args, unknown = parser.parse_known_args()
  568. if unknown:
  569. print(f"Please check your input for typos or unquoted special characters!")
  570. sys.exit(1)
  571. engine_path = args.engine_path
  572. project_path = (args.project_path / "Gem") if (args.project_path and "Gem" not in args.project_path.parts) else args.project_path
  573. if args.component_name:
  574. if not args.project_path:
  575. print("Error: --project-path is required in non-GUI mode.")
  576. sys.exit(1)
  577. if not args.component_type:
  578. print("Error: --component-type is required in non-GUI mode.")
  579. sys.exit(1)
  580. if not validate_component_name(args.component_name, log=print):
  581. print(f"Error: --component-name is required in non-GUI mode. Please provide valid --component-name argument.")
  582. sys.exit(1)
  583. if not validate_component_name(args.namespace, log=print):
  584. print("Error: --namespace is required in non-GUI mode. Please provide valid --namespace argument.")
  585. sys.exit(1)
  586. success = create_component(
  587. engine_path=engine_path,
  588. project_dir=project_path,
  589. namespace=args.namespace,
  590. component_name=args.component_name,
  591. component_type=args.component_type,
  592. add_to_project=args.add_to_project,
  593. default_license=args.default_license,
  594. log=print
  595. )
  596. sys.exit(0 if success else 1)
  597. else:
  598. # Initialize the main Tkinter window
  599. root = tk.Tk()
  600. # Window icon setup (PNG format)
  601. icon_path = Path(engine_path).joinpath("Assets", "Editor", "UI", "Icons", "Editor Settings Manager.png")
  602. if not icon_path.exists():
  603. print(f"Icon not found at: {icon_path}")
  604. else:
  605. img = tk.PhotoImage(file=icon_path)
  606. root.iconphoto(True, img)
  607. # Create and run the main application window
  608. app = NewComponentWindow(root, engine_path, project_path)
  609. root.mainloop()
  610. except Exception:
  611. traceback.print_exc()
  612. sys.exit(1)
  613. finally:
  614. # Remove lock file before exiting
  615. remove_lock()
  616. if __name__ == "__main__":
  617. main()