gui.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. # Flexlay - A Generic 2D Game Editor
  2. # Copyright (C) 2014 Ingo Ruhnke <grumbel@gmail.com>
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. from typing import cast, Any, Optional, Callable, TYPE_CHECKING
  17. import logging
  18. import os
  19. import subprocess
  20. import tempfile
  21. import threading
  22. from PyQt5.QtGui import QIcon
  23. from PyQt5.QtWidgets import QMessageBox, QFileDialog
  24. from flexlay.color import Color
  25. from flexlay.commands.object_add_command import ObjectAddCommand
  26. from flexlay.gui.file_dialog import OpenFileDialog, SaveFileDialog
  27. from flexlay.input_event import InputEvent
  28. from flexlay.math import Point, Size, Rectf, Pointf, Sizef
  29. from flexlay.objmap_path_node import ObjMapPathNode
  30. from flexlay.objmap_rect_object import ObjMapRectObject
  31. from flexlay.tool_context import ToolContext
  32. from flexlay.tools.objmap_select_tool import ObjMapSelectTool
  33. from flexlay.tools.tile_brush_create_tool import TileBrushCreateTool
  34. from flexlay.tools.tile_fill_tool import TileFillTool
  35. from flexlay.tools.tile_paint_tool import TilePaintTool
  36. from flexlay.tools.tile_replace_tool import TileReplaceTool
  37. from flexlay.tools.tilemap_select_tool import TileMapSelectTool
  38. from flexlay.tools.workspace_move_tool import WorkspaceMoveTool
  39. from flexlay.tools.zoom_out_tool import ZoomOutTool
  40. from flexlay.tools.zoom_tool import ZoomTool
  41. from flexlay.util.config import Config
  42. from flexlay.workspace import Workspace
  43. from flexlay.gui.layer_selector import LayerSelector
  44. from flexlay.object_brush import ObjectBrush
  45. from flexlay.objmap_tilemap_object import ObjMapTilemapObject
  46. from supertux.addon import Addon
  47. from supertux.addon_dialog import SaveAddonDialog
  48. from supertux.button_panel import SuperTuxButtonPanel
  49. from supertux.gameobj_factor import supertux_gameobj_factory
  50. from supertux.gameobjs import PathNode
  51. from supertux.level import Level
  52. from supertux.level_file_dialog import OpenLevelFileDialog, SaveLevelFileDialog
  53. from supertux.menubar import SuperTuxMenuBar
  54. from supertux.new_addon import NewAddonWizard
  55. from supertux.new_level import NewLevelWizard
  56. from supertux.sector import Sector
  57. from supertux.supertux_arguments import SuperTuxArguments
  58. from supertux.tilemap import SuperTuxTileMap
  59. from supertux.tileset import SuperTuxTileset
  60. from supertux.toolbox import SuperTuxToolbox
  61. if TYPE_CHECKING:
  62. from flexlay.flexlay import Flexlay
  63. from flexlay.gui_manager import GUIManager
  64. class SuperTuxGUI:
  65. current: Optional['SuperTuxGUI'] = None
  66. def __init__(self, flexlay: 'Flexlay') -> None:
  67. SuperTuxGUI.current = self
  68. self.use_worldmap: bool = False
  69. self.tool_context = ToolContext()
  70. self.level: Optional[Level] = None
  71. self.sector: Optional[Sector] = None
  72. self.gui: GUIManager = flexlay.create_gui_manager("SuperTux Editor")
  73. self.gui.window.setWindowIcon(QIcon("data/images/supertux/supertux-editor.png"))
  74. self.gui.window.set_on_close(self.on_window_close)
  75. self.button_panel = SuperTuxButtonPanel(self.gui, self)
  76. self.toolbox = SuperTuxToolbox(self.gui, self)
  77. self.editor_map = self.gui.create_editor_map_component()
  78. self.statusbar = self.gui.create_statusbar()
  79. self.workspace = self.editor_map.get_workspace()
  80. # Tools
  81. self.workspace.set_tool(InputEvent.MOUSE_MIDDLE, WorkspaceMoveTool())
  82. self.minimap = self.gui.create_minimap(self.editor_map)
  83. self.objectselector = self.gui.create_object_selector(42, 42)
  84. self.properties_widget = self.gui.create_properties_view()
  85. self.editor_map.sig_drop.connect(self.on_object_drop)
  86. for object_brush in supertux_gameobj_factory.create_object_brushes():
  87. self.objectselector.add_brush(object_brush)
  88. self.tileselector = self.gui.create_tile_selector()
  89. assert SuperTuxTileset.current is not None
  90. self.gui_set_tileset(SuperTuxTileset.current)
  91. self.layer_selector: LayerSelector = self.gui.create_layer_selector(self.generate_tilemap_obj)
  92. # self.worldmapobjectselector = self.gui.create_object_selector(42, 42)
  93. # if False:
  94. # self.worldmapobjectselector.sig_drop.connect(self.on_worldmap_object_drop)
  95. # for obj in worldmap_objects:
  96. # self.objectselector.add_brush(ObjectBrush(Sprite.from_file(os.path.join(Config.current.datadir, obj[1])),
  97. # obj[0]))
  98. # Loading Dialogs
  99. assert Config.current is not None
  100. self.load_dialog = OpenLevelFileDialog("Load SuperTux Level")
  101. self.load_dialog.set_directory(Config.current.datadir, "levels")
  102. self.save_dialog = SaveLevelFileDialog("Save SuperTux Level As...")
  103. self.save_dialog.set_directory(Config.current.datadir, "levels")
  104. self.addon_save_dialog = SaveAddonDialog("Save SuperTux Add-on As...")
  105. self.addon_save_dialog.set_directory(Config.current.datadir, "addons")
  106. self.register_keyboard_shortcuts()
  107. # Popup menu
  108. # objmap_select_tool.sig_on_right_click().connect(proc{ | x, y |
  109. # print("Launching Menu at %s, %s" % (x, y))
  110. # menu=Menu(Point(x, y))
  111. # menu.add_item(mysprite, "Delete Object(s)", proc{
  112. # print("Trying to delete
  113. # {self.workspace.get_map().metadata}
  114. # {self.workspace.get_map().metadata.objects}")
  115. # cmd=ObjectDeleteCommand(self.workspace.get_map().metadata.objects)
  116. # for i in objmap_select_tool.get_selection():
  117. # cmd.add_object(i)
  118. # self.workspace.get_map().execute(cmd)
  119. # objmap_select_tool.clear_selection()
  120. # })
  121. # menu.add_item(mysprite, "Edit Properties", proc{
  122. # for i in objmap_select_tool.get_selection():
  123. # i.metadata.property_dialog()
  124. # }
  125. # })
  126. # menu.run()
  127. # })
  128. # setting initial state
  129. level = Level.from_size(100, 50)
  130. self.set_level(level, "main")
  131. self.set_tilemap_paint_tool()
  132. # Must be after LayerSelector initialised
  133. self.menubar = SuperTuxMenuBar(self.gui, self)
  134. # Command line arguments, when game is run
  135. self.arguments = SuperTuxArguments()
  136. def register_keyboard_shortcuts(self) -> None:
  137. self.editor_map.sig_on_key("f1").connect(lambda x, y: self.gui_toggle_minimap())
  138. self.editor_map.sig_on_key("m").connect(lambda x, y: self.gui_toggle_minimap())
  139. self.editor_map.sig_on_key("g").connect(lambda x, y: self.gui_toggle_grid())
  140. self.editor_map.sig_on_key("+").connect(lambda x, y: self.editor_map.zoom_in(Pointf(x, y)))
  141. self.editor_map.sig_on_key("-").connect(lambda x, y: self.editor_map.zoom_out(Pointf(x, y)))
  142. self.editor_map.sig_on_key("Enter").connect(lambda x, y: self.gui_set_zoom(1.0))
  143. self.editor_map.sig_on_key("i").connect(lambda x, y: self.insert_path_node(x, y))
  144. self.editor_map.sig_on_key("c").connect(lambda x, y: self.connect_path_nodes())
  145. self.editor_map.sig_on_key("7").connect(
  146. cast(Callable[[int, int], None],
  147. lambda x, y: self.workspace.get_map().metadata.parent.activate_sector("main",
  148. self.workspace)))
  149. self.editor_map.sig_on_key("8").connect(
  150. cast(Callable[[int, int], None],
  151. lambda x, y: self.workspace.get_map().metadata.parent.activate_sector("another_world",
  152. self.workspace)))
  153. self.editor_map.sig_on_key("p").connect(lambda x, y: self.gui_show_object_properties())
  154. def on_a_key(x: int, y: int) -> None:
  155. pos = self.editor_map.screen2world(Pointf(x, y))
  156. rectobj = ObjMapRectObject(Rectf.from_ps(pos,
  157. Sizef(128, 64)),
  158. Color(0, 255, 255, 155),
  159. None)
  160. self.workspace.get_map().metadata.objects.add_object(rectobj)
  161. self.editor_map.sig_on_key("a").connect(on_a_key)
  162. def on_window_close(self, *args: Any) -> bool:
  163. """Called when window x button is clicked
  164. Ask whether to save, continue, or just quit.
  165. :return: boolean whether to close or not. If not boolean, will close.
  166. """
  167. assert Workspace.current is not None
  168. editor_map = Workspace.current.get_map()
  169. # If the most recent save was the same as the save_pointer index,
  170. # we can safely quit
  171. if editor_map.save_pointer == len(editor_map.undo_stack):
  172. return True
  173. else:
  174. choice = QMessageBox.warning(self.gui.window, "Unsaved Changes to Level",
  175. "The level has been changed since "
  176. "the last save.",
  177. QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard,
  178. QMessageBox.Save)
  179. if choice == QMessageBox.Save:
  180. dialog_is_cancelled = False
  181. def after_save(i: int) -> None:
  182. dialog_is_cancelled = (i == 0) # noqa: F841
  183. self.save_dialog.file_dialog.finished.connect(after_save)
  184. self.gui_level_save()
  185. # If saved, show confirmation dialog to reassure user.
  186. if not dialog_is_cancelled:
  187. QMessageBox.information(self.gui.window, "Saved Successfully", "Editor will now quit")
  188. # If dialog is cancelled, don't quit, as that would lose changes
  189. return not dialog_is_cancelled
  190. elif choice == QMessageBox.Cancel:
  191. return False
  192. elif choice == QMessageBox.Discard:
  193. return True
  194. else:
  195. assert False, f"unhandled QMessageBox choice value: {choice}"
  196. def on_object_drop(self, brush: ObjectBrush, pos: Pointf) -> None:
  197. obj = supertux_gameobj_factory.create_gameobj_at(brush.metadata, pos)
  198. if obj is None:
  199. logging.error("Unknown object type dropped: %r" % brush.metadata)
  200. else:
  201. cmd = ObjectAddCommand(self.workspace.get_map().metadata.object_layer)
  202. assert obj.objmap_object is not None
  203. cmd.add_object(obj.objmap_object)
  204. self.workspace.get_map().execute(cmd)
  205. def run(self) -> None:
  206. self.gui.run()
  207. def show_objects(self) -> None:
  208. if False: # GRUMBEL
  209. self.tileselector.show(False) # type: ignore[unreachable]
  210. if self.use_worldmap:
  211. self.objectselector.show(False)
  212. else:
  213. self.objectselector.show(True)
  214. def show_tiles(self) -> None:
  215. if False: # GRUMBEL
  216. self.tileselector.show(True) # type: ignore[unreachable]
  217. self.objectselector.show(False)
  218. def gui_toggle_minimap(self) -> None:
  219. if self.minimap.get_widget().isVisible():
  220. self.minimap.get_widget().hide()
  221. self.button_panel.minimap_icon.set_up()
  222. else:
  223. self.minimap.get_widget().show()
  224. self.button_panel.minimap_icon.set_down()
  225. def gui_toggle_grid(self) -> None:
  226. self.workspace.get_map().draw_grid = not self.workspace.get_map().draw_grid
  227. if not self.workspace.get_map().draw_grid:
  228. self.button_panel.grid_icon.set_down()
  229. else:
  230. self.button_panel.grid_icon.set_up()
  231. self.editor_map.editormap_widget.repaint()
  232. def gui_set_tileset(self, tileset: SuperTuxTileset) -> None:
  233. self.tileselector.set_tileset(tileset)
  234. self.tileselector.clear_tilegroups()
  235. self.tileselector.add_tilegroup("All Tiles", tileset.get_tiles())
  236. for tilegroup in tileset.tilegroups:
  237. self.tileselector.add_tilegroup(tilegroup.name, tilegroup.tiles)
  238. if self.sector is not None:
  239. for tilemap_layer in [tilemap.tilemap_layer for tilemap in self.sector.tilemaps]:
  240. assert tilemap_layer is not None
  241. tilemap_layer.tileset = tileset
  242. self.editor_map.editormap_widget.repaint()
  243. if self.level is not None:
  244. self.level.tileset_path = tileset.filename
  245. def set_tileset(self, filename: str) -> None:
  246. """Set tileset from (.strf) filename"""
  247. if not filename:
  248. QMessageBox.warning(None, "No Tileset Selected", "No tileset was selected, aborting...")
  249. tileset = SuperTuxTileset(32)
  250. tileset.load(filename)
  251. self.gui_set_tileset(tileset)
  252. # def gui_change_tileset(self):
  253. # tileset_dialog = OpenFileDialog("Select Tileset To Open", ("SuperTux Tilesets (*.strf)", "All Files (*)"))
  254. # tileset_dialog.set_directory(Config.current.datadir, "images")
  255. # tileset_dialog.run(self.set_tileset)
  256. def gui_change_tileset(self) -> bool:
  257. assert Config.current is not None
  258. filename, _filter = QFileDialog.getOpenFileName(None, "Select Tileset To Open", Config.current.datadir)
  259. if not filename:
  260. QMessageBox.warning(None, "No Tileset Selected", "No tileset was selected, aborting...")
  261. return False
  262. tileset = SuperTuxTileset(32)
  263. tileset.load(filename)
  264. self.gui_set_tileset(tileset)
  265. return True
  266. def gui_run_level(self) -> None:
  267. logging.info("Run this level...")
  268. level = self.workspace.get_map().metadata.get_level()
  269. if level.is_worldmap:
  270. suffix = ".stwm"
  271. else:
  272. suffix = ".stl"
  273. tmpfile = tempfile.mkstemp(suffix=suffix, prefix="flexlay-")
  274. self.save_level(tmpfile[1], False, True)
  275. self.arguments.run_level = tmpfile[1]
  276. # for obj in self.sector.object_layer.get_objects():
  277. # if isinstance(obj.metadata, Tux):
  278. # self.arguments.spawn_at = obj.pos
  279. try:
  280. thread = threading.Thread(target=self.gui_run_level_thread,
  281. args=(self.gui_run_level_cleanup,
  282. self.arguments.get_popen_arg(),
  283. tmpfile))
  284. thread.start()
  285. except FileNotFoundError:
  286. QMessageBox.warning(None, "No Supertux Binary Found",
  287. "Press OK to select your Supertux binary")
  288. assert Config.current is not None
  289. Config.current.binary = OpenFileDialog("Open Supertux Binary").filename
  290. if not Config.current.binary:
  291. raise RuntimeError("binary path missing, use --binary BIN")
  292. # self.arguments.spawn_at = None
  293. def gui_run_level_thread(self, postexit_fn: Callable[[str], None], popen_args: list[str], tmpfile: str) -> None:
  294. subproc = subprocess.Popen(popen_args)
  295. subproc.wait()
  296. postexit_fn(tmpfile)
  297. def gui_run_level_cleanup(self, tmpfile: tuple[int, str]) -> None:
  298. # Safely get rid of temporary file
  299. os.close(tmpfile[0])
  300. os.remove(tmpfile[1])
  301. def gui_record_level(self) -> None:
  302. dialog = SaveFileDialog("Choose Record Target File")
  303. dialog.run(lambda _: None)
  304. self.arguments.record_demo_file = dialog.get_filename()
  305. self.gui_run_level()
  306. self.arguments.record_demo_file = None
  307. def gui_play_demo(self) -> None:
  308. QMessageBox.information(None,
  309. "Select a level file",
  310. "You must now select a level file - the level of the demo")
  311. # level = OpenLevelFileDialog("Select The Level")
  312. QMessageBox.information(None,
  313. "Select a demo file",
  314. "You must now select a demo file to play")
  315. demo = OpenFileDialog("Select The Demo File To Play").filename
  316. self.arguments.play_demo_file = demo
  317. subprocess.Popen(self.arguments.get_popen_arg())
  318. self.arguments.play_demo_file = None
  319. def gui_watch_example(self) -> None:
  320. assert Config.current is not None
  321. level = os.path.join(Config.current.datadir, "levels", "world1", "01 - Welcome to Antarctica.stl")
  322. demo = os.path.join("data", "supertux", "demos", "karkus476_plays_level_1")
  323. subprocess.Popen([Config.current.binary, level, "--play-demo", demo])
  324. def gui_resize_sector(self) -> None:
  325. sector = self.workspace.get_map().metadata
  326. assert isinstance(sector, Sector)
  327. dialog = self.gui.create_generic_dialog("Resize Sector")
  328. dialog.add_int("Width: ", sector.width)
  329. dialog.add_int("Height: ", sector.height)
  330. dialog.add_int("X: ", 0)
  331. dialog.add_int("Y: ", 0)
  332. def on_callback(w: int, h: int, x: int, y: int) -> None:
  333. logging.info("Resize Callback")
  334. sector.resize(Size(w, h), Point(x, y))
  335. dialog.add_callback(on_callback)
  336. def gui_smooth_level_struct(self) -> None:
  337. logging.info("Smoothing level structure")
  338. tilemap = self.tool_context.tilemap_layer
  339. assert tilemap is not None
  340. data = tilemap.get_data()
  341. # width = tilemap.width
  342. #
  343. # GRUMBEL
  344. # def get(x, y):
  345. # return data[y * width + x]
  346. #
  347. # def set(x, y, val):
  348. # data[y * width + x] = val
  349. #
  350. # def smooth(x, y):
  351. # pass # GRUMBEL
  352. # for ary in itile_conditions:
  353. # if ((solid_itiles.index(get[x - 1, y - 1]) ? 1: 0) == ary[0]
  354. # and (solid_itiles.index(get[x, y - 1]) ? 1: 0) == ary[1]
  355. # and (solid_itiles.index(get[x + 1, y - 1]) ? 1: 0) == ary[2]
  356. # and (solid_itiles.index(get[x - 1, y]) ? 1: 0) == ary[3]
  357. # and (solid_itiles.index(get[x, y]) ? 1: 0) == ary[4]
  358. # and (solid_itiles.index(get[x + 1, y]) ? 1: 0) == ary[5]
  359. # and (solid_itiles.index(get[x - 1, y + 1]) ? 1: 0) == ary[6]
  360. # and (solid_itiles.index(get[x, y + 1]) ? 1: 0) == ary[7]
  361. # and (solid_itiles.index(get[x + 1, y + 1]) ? 1: 0) == ary[8]):
  362. # set[x, y, ary[9]]
  363. #
  364. # rect = self.tilemap_select_tool.get_selection_rect()
  365. #
  366. # start_x = rect.left
  367. # end_x = rect.right
  368. # start_y = rect.top
  369. # end_y = rect.bottom
  370. #
  371. # GRUMBEL
  372. # for y in range(start_y, end_y):
  373. # for x in range(start_x, end_x):
  374. # smooth(x, y)
  375. tilemap.set_data(data)
  376. def gui_resize_sector_to_selection(self) -> None:
  377. if self.tool_context.tile_selection is not None:
  378. level = self.workspace.get_map().metadata
  379. rect = self.tool_context.tile_selection.get_rect()
  380. if (rect.width > 2 and rect.height > 2):
  381. level.resize(rect.size, Point(-rect.left, -rect.top))
  382. def gui_edit_level(self) -> None:
  383. level = self.workspace.get_map().metadata.get_level()
  384. dialog = self.gui.create_generic_dialog("Edit Level")
  385. dialog.add_string("Name:", level.name)
  386. dialog.add_string("Author:", level.author)
  387. dialog.add_string("Contact:", level.contact)
  388. dialog.add_int("Target Time:", level.target_time)
  389. def on_callback(name: str, author: str, contact: str, target_time: float) -> None:
  390. level.name = name
  391. level.author = author
  392. level.contact = contact
  393. level.target_time = target_time
  394. dialog.add_callback(on_callback)
  395. def gui_edit_sector(self) -> None:
  396. level = self.workspace.get_map().metadata.get_level()
  397. dialog = self.gui.create_generic_dialog("Edit Sector")
  398. assert Config.current is not None
  399. dialog.add_string("Name: ", level.current_sector.name)
  400. dialog.add_file("Music: ",
  401. level.current_sector.music,
  402. ret_rel_to=Config.current.datadir,
  403. show_rel_to=os.path.join(Config.current.datadir, "music"),
  404. open_in=os.path.join(Config.current.datadir, "music"))
  405. dialog.add_float("Gravity: ", level.current_sector.gravity)
  406. def on_callback(*args: Any) -> None:
  407. level.current_sector.name = args[0]
  408. level.current_sector.music = args[1]
  409. level.current_sector.gravity = args[2]
  410. dialog.add_callback(on_callback)
  411. def gui_zoom_in(self) -> None:
  412. factor = 2.0
  413. gc = self.editor_map.get_gc_state()
  414. zoom = gc.get_zoom()
  415. self.gui_set_zoom(zoom / pow(1.25, -factor))
  416. def gui_zoom_out(self) -> None:
  417. factor = 2.0
  418. gc = self.editor_map.get_gc_state()
  419. zoom = gc.get_zoom()
  420. self.gui_set_zoom(zoom * pow(1.25, -factor))
  421. def gui_zoom_fit(self) -> None:
  422. rect = self.workspace.get_map().get_bounding_rect()
  423. zoom = min(self.editor_map.editormap_widget.width() / rect.width,
  424. self.editor_map.editormap_widget.height() / rect.height)
  425. self.gui_set_zoom(zoom, Pointf(rect.width / 2, rect.height / 2))
  426. def gui_set_zoom(self, zoom: float, pos: Optional[Pointf] = None) -> None:
  427. gc = self.editor_map.get_gc_state()
  428. pos = pos or gc.get_pos()
  429. gc.set_zoom(zoom)
  430. gc.set_pos(pos)
  431. self.editor_map.editormap_widget.repaint()
  432. def gui_remove_sector(self) -> None:
  433. sector = self.workspace.get_map().metadata
  434. sector.get_level().remove_sector(sector.name)
  435. def gui_add_sector(self) -> None:
  436. level = self.workspace.get_map().metadata.get_level()
  437. name = "sector"
  438. uniq_name = name
  439. i = 2
  440. while level.get_sectors().index(uniq_name):
  441. uniq_name = name + "<%d>" % i
  442. i += 1
  443. sector = Sector(level)
  444. sector.new_from_size(uniq_name, 30, 20)
  445. level.add_sector(sector)
  446. self.set_level(level, uniq_name)
  447. self.gui_edit_sector()
  448. def gui_show_object_properties(self) -> None:
  449. if self.tool_context.object_selection:
  450. selection = self.tool_context.object_selection
  451. if len(selection) > 1:
  452. logging.warning("Selection too large")
  453. elif len(selection) == 1:
  454. obj = selection[0].metadata
  455. obj.property_dialog(self.gui.window)
  456. else:
  457. logging.warning("Selection is empty")
  458. def undo(self) -> None:
  459. self.workspace.get_map().undo()
  460. def redo(self) -> None:
  461. self.workspace.get_map().redo()
  462. def on_map_change(self) -> None:
  463. self.editor_map.editormap_widget.repaint()
  464. if self.workspace.get_map().undo_stack_size() > 0:
  465. self.button_panel.undo_icon.enable()
  466. else:
  467. self.button_panel.undo_icon.disable()
  468. if self.workspace.get_map().redo_stack_size() > 0:
  469. self.button_panel.redo_icon.enable()
  470. else:
  471. self.button_panel.redo_icon.disable()
  472. def gui_level_save_as(self) -> None:
  473. path = self.save_dialog.get_filename()
  474. if os.path.isdir(path):
  475. self.save_dialog.set_directory(path)
  476. else:
  477. self.save_dialog.set_directory(os.path.dirname(path) + "/")
  478. self.save_dialog.run(self.save_level)
  479. def gui_level_save(self) -> None:
  480. if self.use_worldmap:
  481. filename = self.workspace.get_map().metadata.filename
  482. else:
  483. filename = self.workspace.get_map().metadata.parent.filename
  484. logging.info("Save Filename: " + filename)
  485. if filename:
  486. self.save_level(filename)
  487. else:
  488. filename = self.save_dialog.get_filename()
  489. if filename[-1] == "/"[0]:
  490. self.save_dialog.set_directory(filename)
  491. else:
  492. self.save_dialog.set_directory(os.path.dirname(filename) + "/")
  493. self.save_dialog.run(self.save_level)
  494. def gui_level_new(self) -> None:
  495. if False:
  496. dialog = NewLevelWizard(self.gui.window) # type: ignore[unreachable]
  497. dialog.exec_()
  498. if dialog.level:
  499. self.set_level(dialog.level, "main")
  500. if dialog.level is not None:
  501. def save_path_chosen(save_path):
  502. dialog.level.save(save_path)
  503. self.load_level(save_path)
  504. self.save_dialog.run(save_path_chosen)
  505. else:
  506. level = Level.from_size(100, 25)
  507. self.set_level(level, "main")
  508. def gui_addon_new(self) -> None:
  509. dialog = NewAddonWizard(self.gui.window)
  510. dialog.exec_()
  511. if dialog.addon is not None:
  512. def save_path_chosen(save_path: str) -> None:
  513. assert dialog.addon is not None
  514. dialog.addon.save(save_path)
  515. self.load_addon(dialog.addon, save_path)
  516. self.addon_save_dialog.run(save_path_chosen)
  517. pass
  518. def gui_level_load(self) -> None:
  519. self.load_dialog.run(self.load_level)
  520. def insert_path_node(self, x: int, y: int) -> None:
  521. logging.info("Insert path Node")
  522. m = self.workspace.get_map().metadata
  523. pathnode = ObjMapPathNode(self.editor_map.screen2world(Pointf(x, y)),
  524. "PathNode")
  525. pathnode.metadata = PathNode(pathnode)
  526. m.objects.add_object(pathnode)
  527. def connect_path_nodes(self) -> None:
  528. logging.info("Connecting path nodes")
  529. pathnodes: list[ObjMapPathNode] = []
  530. for i in self.tool_context.object_selection:
  531. obj = i.metadata
  532. if isinstance(obj, PathNode):
  533. pathnodes.append(obj.node)
  534. last: Optional[ObjMapPathNode] = None
  535. for i in pathnodes:
  536. if last is not None:
  537. last.connect(i)
  538. last = i
  539. def gui_set_datadir(self) -> None:
  540. assert Config.current is not None
  541. if os.path.isdir(Config.current.datadir):
  542. dialog = self.gui.create_generic_dialog("Specify the SuperTux data directory and restart")
  543. dialog.add_label("You need to specify the datadir where SuperTux is located")
  544. dialog.add_string("SuperTux datadir:", Config.current.datadir)
  545. def on_callback(datadir: str) -> None:
  546. assert Config.current is not None
  547. Config.current.datadir = datadir
  548. dialog.add_callback(on_callback)
  549. def load_level(self, filename: str, set_title: bool = True) -> None:
  550. logging.info("Loading: " + filename)
  551. # Clear object selection, it's a new level!
  552. self.tool_context.object_selection.clear()
  553. assert self.gui.properties_widget is not None
  554. self.gui.properties_widget.clear_properties()
  555. # Set title if desired
  556. if set_title:
  557. self.gui.window.setWindowTitle("SuperTux Editor: [" + filename + "]")
  558. # if filename[-5:] == ".stwm":
  559. # QMessageBox.warning(None, "Opening Worldmap File",
  560. # "[WARNING] Opening supertux worldmap file:\n'"+filename+"'\n" +
  561. # "Worldmaps usually use different tilesets to levels.\n"+
  562. # "Please select a different tileset to use (look for .strf files).")
  563. # if not self.gui_change_tileset():
  564. # return
  565. # print("Loading worldmap")
  566. level = Level.from_file(filename)
  567. assert SuperTuxTileset.current is not None
  568. assert Config.current is not None
  569. if level.tileset_path != SuperTuxTileset.current.filename:
  570. tileset = SuperTuxTileset(32)
  571. tileset.load(os.path.join(Config.current.datadir, level.tileset_path))
  572. # tileset.load(level.tileset_path)
  573. self.gui_set_tileset(tileset)
  574. # Tileset has changed, reload level:
  575. level = Level.from_file(filename)
  576. self.set_level(level, "main")
  577. Config.current.add_recent_file(filename)
  578. self.menubar.update_recent_files()
  579. self.minimap.update_minimap()
  580. # TODO: We don't yet support multiple sectors, so we set the first sector's name.
  581. self.editor_map.set_sector_tab_label(0, level.sectors[0].name)
  582. def load_worldmap(self, filename: str) -> None:
  583. print("Loading Worldmap: {}".format(filename))
  584. # worldmap = WorldMap(filename)
  585. # worldmap.activate(self.workspace)
  586. def save_level(self, filename: str, set_title: bool = True, is_tmp: bool = False) -> None:
  587. if set_title:
  588. self.gui.window.setWindowTitle("SuperTux Editor: [" + filename + "]")
  589. assert Workspace.current is not None
  590. editor_map = Workspace.current.get_map()
  591. editor_map.save_pointer = len(editor_map.undo_stack)
  592. level = self.workspace.get_map().metadata.parent
  593. assert Config.current is not None
  594. Config.current.add_recent_file(filename)
  595. self.menubar.update_recent_files()
  596. # Do backup save if the file exists and is going to be saved permanently.
  597. if os.path.isfile(filename) and not is_tmp:
  598. os.rename(filename, filename + "~")
  599. level.save(filename)
  600. level.filename = filename
  601. def load_addon(self, addon: Addon, dirname: str) -> None:
  602. print("Add-on dirname is: " + dirname)
  603. self.gui.project_widget.set_addon(addon)
  604. self.gui.project_widget.set_project_directory(dirname)
  605. def load_addon_zip(self, filename: str) -> None:
  606. print("Add-on zip path is: {}".format(filename))
  607. def raise_selection(self) -> None:
  608. for obj in self.tool_context.object_selection:
  609. self.workspace.get_map().metadata.objects.raise_object(obj)
  610. self.editor_map.editormap_widget.repaint()
  611. def lower_selection(self) -> None:
  612. for obj in self.tool_context.object_selection:
  613. self.workspace.get_map().metadata.objects.lower_object(obj)
  614. self.editor_map.editormap_widget.repaint()
  615. def raise_selection_to_top(self) -> None:
  616. selection = self.tool_context.object_selection
  617. self.workspace.get_map().metadata.objects.raise_objects_to_top(selection)
  618. self.editor_map.editormap_widget.repaint()
  619. def lower_selection_to_bottom(self) -> None:
  620. selection = self.tool_context.object_selection
  621. self.workspace.get_map().metadata.objects.lower_objects_to_bottom(selection)
  622. self.editor_map.editormap_widget.repaint()
  623. def set_tilemap_paint_tool(self) -> None:
  624. self.workspace.set_tool(InputEvent.MOUSE_LEFT, TilePaintTool())
  625. self.workspace.set_tool(InputEvent.MOUSE_RIGHT, TileBrushCreateTool())
  626. self.toolbox.set_down(self.toolbox.paint_icon)
  627. def set_tilemap_replace_tool(self) -> None:
  628. self.workspace.set_tool(InputEvent.MOUSE_LEFT, TileReplaceTool())
  629. self.workspace.set_tool(InputEvent.MOUSE_RIGHT, TileBrushCreateTool())
  630. self.toolbox.set_down(self.toolbox.replace_icon)
  631. def set_tilemap_fill_tool(self) -> None:
  632. self.workspace.set_tool(InputEvent.MOUSE_LEFT, TileFillTool())
  633. self.workspace.set_tool(InputEvent.MOUSE_RIGHT, TileBrushCreateTool())
  634. self.toolbox.set_down(self.toolbox.fill_icon)
  635. def set_tilemap_select_tool(self) -> None:
  636. self.workspace.set_tool(InputEvent.MOUSE_LEFT, TileMapSelectTool())
  637. self.workspace.set_tool(InputEvent.MOUSE_RIGHT, None)
  638. self.toolbox.set_down(self.toolbox.select_icon)
  639. def set_zoom_tool(self) -> None:
  640. self.workspace.set_tool(InputEvent.MOUSE_LEFT, ZoomTool())
  641. self.workspace.set_tool(InputEvent.MOUSE_RIGHT, ZoomOutTool())
  642. self.toolbox.set_down(self.toolbox.zoom_icon)
  643. def set_objmap_select_tool(self) -> None:
  644. self.workspace.set_tool(InputEvent.MOUSE_LEFT, ObjMapSelectTool(self.gui))
  645. self.workspace.set_tool(InputEvent.MOUSE_RIGHT, ObjMapSelectTool(self.gui))
  646. self.toolbox.set_down(self.toolbox.object_icon)
  647. def set_level(self, level: Level, sectorname: str) -> None:
  648. self.level = level
  649. for sec in self.level.sectors:
  650. if sec.name == sectorname:
  651. self.set_sector(sec)
  652. break
  653. def set_sector(self, sector: Sector) -> None:
  654. assert sector is not None
  655. self.sector = sector
  656. self.workspace.current_sector = sector
  657. assert self.sector.editormap is not None
  658. self.workspace.set_map(self.sector.editormap)
  659. self.layer_selector.set_map(self.sector.editormap)
  660. # TODO: We don't yet support multiple sectors, so we set the first sector's name.
  661. self.editor_map.set_sector_tab_label(0, sector.name)
  662. assert ToolContext.current is not None
  663. ToolContext.current.tilemap_layer = self.sector.get_some_solid_tilemap().tilemap_layer
  664. ToolContext.current.object_layer = self.sector.object_layer
  665. assert SuperTuxGUI.current is not None
  666. assert self.sector.editormap is not None
  667. self.sector.editormap.sig_change.connect(SuperTuxGUI.current.on_map_change)
  668. def generate_tilemap_obj(self) -> ObjMapTilemapObject:
  669. """Generate a basic ObjMapTilemapObject with basic parameters
  670. May later open a dialog.
  671. :return: ObjMapTilemapObject
  672. """
  673. assert self.sector is not None
  674. tilemap = SuperTuxTileMap.from_size(self.sector.width,
  675. self.sector.height,
  676. "<no name>",
  677. 0, True)
  678. assert tilemap.tilemap_layer is not None
  679. tilemap_object = ObjMapTilemapObject(tilemap.tilemap_layer, tilemap)
  680. return tilemap_object
  681. def camera_properties(self) -> None:
  682. assert self.sector is not None
  683. assert self.sector.camera is not None
  684. self.sector.camera.property_dialog(self.gui.window)
  685. # EOF #