layer_selector.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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, Callable, Iterable, Optional, Union
  17. from PyQt5.QtCore import QItemSelection, QModelIndex
  18. from PyQt5.QtGui import (QStandardItemModel, QStandardItem, QIcon)
  19. from PyQt5.QtWidgets import (QWidget, QToolBar, QTreeView,
  20. QVBoxLayout)
  21. from flexlay.commands.layer_delete_command import LayerDeleteCommand
  22. from flexlay.gui.editor_map_component import EditorMapComponent
  23. from flexlay.objmap_tilemap_object import ObjMapTilemapObject
  24. from flexlay.tilemap_layer import TilemapLayer
  25. from flexlay.tool_context import ToolContext
  26. from flexlay.util.signal import Signal
  27. from flexlay.layer import Layer
  28. from flexlay.object_layer import ObjectLayer
  29. from flexlay.editor_map import EditorMap
  30. class LayerTreeView(QTreeView):
  31. """A QTreeView for layers"""
  32. def __init__(self, layer_selector: 'LayerSelector', parent: Optional[QWidget] = None,
  33. selection_callback: Optional[Callable[[int, int], None]] = None) -> None:
  34. """Initialises a LayerTreeView
  35. Only difference from QTreeView is that a Signal is created
  36. to handle whenever the selection changes.
  37. :param: selection_callback: A callback to be connected immediately
  38. """
  39. super().__init__(parent)
  40. # Signal called when selection changes
  41. self.selection_signal = Signal()
  42. if selection_callback:
  43. self.selection_signal.connect(selection_callback)
  44. # The layers selector object which should have created this LayerTreeView
  45. self.layer_selector = layer_selector
  46. def selectionChanged(self, selected: QItemSelection, deselected: QItemSelection) -> None:
  47. """Called when user clicks on a different Layer
  48. QTreeView function, not to be called by anything else.
  49. :param selected - QItemSelection previously selected
  50. :param deselected - QItemSelection previously deselected
  51. Calls all callbacks in selection_signal
  52. With parameters as indexes in tree view (row which is selected, top is 0):
  53. selected : What's now selected. None if nothing is selected.
  54. deselected What used to be selected. None if nothing was selected
  55. """
  56. super().selectionChanged(selected, deselected)
  57. selected_index: Optional[int] = None
  58. deselected_index: Optional[int] = None
  59. if len(selected.indexes()) > 0:
  60. selected_index = selected.indexes()[0].row()
  61. if len(deselected.indexes()) > 0:
  62. deselected_index = deselected.indexes()[0].row()
  63. self.selection_signal(selected_index, deselected_index)
  64. def dataChanged(self,
  65. top_left: QModelIndex,
  66. bottom_right: QModelIndex,
  67. roles: Iterable[int] = []) -> None:
  68. """Overrides in QTreeView. Ensures name of actual tilemap is set"""
  69. super().dataChanged(top_left, bottom_right, roles)
  70. if top_left == bottom_right:
  71. index = top_left.row()
  72. data = str(top_left.data())
  73. self.layer_selector.get_layers()[index].name = data
  74. class LayerSelector:
  75. """Show layers in a Tree View to be selected
  76. Also handles selected layer. Although more than one may be selected,
  77. only caters for a single one being selected. (most likely first
  78. one selected, but don't rely on that.
  79. **NOTE** When hiding layers, use set_hidden or toggle_hidden here
  80. When getting (a) layer(s) use get_layers and get_layer to
  81. return a TilemapLayer
  82. """
  83. def __init__(self, generate_tilemap_obj: Callable[[], ObjMapTilemapObject]) -> None:
  84. """A way to view layers
  85. :param metadata_from_size: A method/function which returns metadata
  86. for an ObjMapTilemapObject with arguments
  87. width, height
  88. """
  89. self.model = QStandardItemModel()
  90. # QStandardItems in the model (to set font etc.)
  91. # items are added to list by self.set_map()
  92. self.items: list[QStandardItem] = []
  93. # Preferably use get_layers and get_layer
  94. self.tilemap_layers: list[TilemapLayer] = []
  95. # self.model.setHorizontalHeaderItem(0, QStandardItem("Visible"))
  96. self.model.setHorizontalHeaderItem(1, QStandardItem("Layer"))
  97. self.vbox = QWidget()
  98. # Use QTreeWidget instead!?
  99. self.tree_view = LayerTreeView(self, selection_callback=self.selection_changed)
  100. self.tree_view.setModel(self.model)
  101. self.toolbar = QToolBar()
  102. self.toolbar.addAction("Hide All", self.hide_all)
  103. self.toolbar.addAction("Show All", self.show_all)
  104. # Eye icons:
  105. self.eye_open_icon = QIcon("data/images/supertux/stock-eye-12.png")
  106. self.eye_closed_icon = QIcon("data/images/supertux/stock-eye-half-12.png")
  107. # Button to toggle selected layer hidden/shown.
  108. self.current_hidden = self.toolbar.addAction(self.eye_open_icon,
  109. "Toggle Visibility",
  110. self.toggle_action)
  111. # Stays pressed when clicked. Pressed = hidden, else shown
  112. self.current_hidden.setCheckable(True)
  113. # Buttons to add/remove layers.
  114. self.toolbar.addAction(QIcon("data/images/supertux/plus.png"),
  115. "New Layer",
  116. self.add_layer)
  117. self.toolbar.addAction(QIcon("data/images/supertux/minus.png"),
  118. "Delete This Layer",
  119. self.remove_current_layer)
  120. self.layout = QVBoxLayout(self.vbox)
  121. self.layout.setContentsMargins(0, 0, 0, 0)
  122. self.layout.addWidget(self.tree_view)
  123. self.layout.addWidget(self.toolbar)
  124. # Currently selected index, -1 if nothing selected
  125. self.selected_index: int = -1
  126. # Show only selected layer if true
  127. self.show_only_selected: bool = False
  128. self.generate_tilemap_obj = generate_tilemap_obj
  129. # To get the tilemap_layers
  130. self.editormap: Optional[EditorMap] = None
  131. def toggle_hidden(self, index: int) -> None:
  132. """Run set hidden on selected tilemap to toggle visibility"""
  133. if self.selected_index >= 0:
  134. self.set_hidden(self.selected_index, not self.is_hidden(self.selected_index))
  135. def set_hidden(self, index: int, hidden: bool, repaint: bool = True) -> None:
  136. """Set tilemap_layer to hidden
  137. :param index is the index of the tilemap in the treeview
  138. :param hidden is a boolean to set it to True = not visible
  139. :param repaint: Whether to repaint the screen immediately after (for efficiency purposes)
  140. """
  141. assert EditorMapComponent.current is not None
  142. if len(self.items) > index:
  143. # Get font of relevant item
  144. font = self.items[index].font()
  145. font.setBold(not hidden)
  146. self.items[index].setFont(font)
  147. if len(self.get_layers()) > index:
  148. layer = self.get_layer(index)
  149. assert layer is not None
  150. layer.hidden = hidden
  151. if repaint:
  152. EditorMapComponent.current.editormap_widget.repaint()
  153. def is_hidden(self, index: int) -> bool:
  154. """Returns True if tilemap_layer at index
  155. in hidden is hidden else False
  156. """
  157. # Check for None and False.
  158. tilemap_layer = self.get_layer(index)
  159. if not tilemap_layer:
  160. return False
  161. return tilemap_layer.hidden
  162. def set_map(self, editormap: EditorMap) -> None:
  163. """Refresh, showing new layers in tree view"""
  164. self.editormap = editormap
  165. self.model.clear()
  166. self.items = []
  167. # When done this way, we can expect that the position in the
  168. # TreeView corresponds to poistion in list.
  169. unnamed_count = 1
  170. for layer in self.get_layers():
  171. if layer.metadata.name == "":
  172. standard_item = QStandardItem("No name (" + str(unnamed_count) + ")")
  173. unnamed_count += 1
  174. else:
  175. standard_item = QStandardItem(layer.metadata.name)
  176. self.model.appendRow([standard_item])
  177. self.items.append(standard_item)
  178. # Ensure all are visible and set to bold.
  179. self.show_all()
  180. def hide_all(self) -> None:
  181. """Hide all layers in this editormap"""
  182. if not self.editormap:
  183. return
  184. # Get TilemapLayer, and set hidden
  185. for i in range(len(self.get_layers())):
  186. self.set_hidden(i, True, repaint=False)
  187. # Repaint so that changes are visible
  188. assert EditorMapComponent.current is not None
  189. EditorMapComponent.current.editormap_widget.repaint()
  190. def show_all(self) -> None:
  191. """Unhide all layers in this editormap"""
  192. if not self.editormap:
  193. return
  194. # Get TilemapLayer, and set hidden
  195. for i in range(len(self.get_layers())):
  196. self.set_hidden(i, False, repaint=False)
  197. # Repaint so that changes are visible
  198. assert EditorMapComponent.current is not None
  199. EditorMapComponent.current.editormap_widget.repaint()
  200. def hide_all_layers(self) -> None:
  201. if not self.editormap:
  202. return
  203. # Get TilemapLayer, and set hidden
  204. assert isinstance(self.editormap.layers[0], ObjectLayer)
  205. object_layer = cast(ObjectLayer, self.editormap.layers[0])
  206. for object in object_layer.objects:
  207. if isinstance(object, ObjMapTilemapObject):
  208. layer = object.tilemap_layer
  209. if isinstance(layer, TilemapLayer):
  210. layer.hidden = True
  211. # Repaint so that changes are visible
  212. assert EditorMapComponent.current is not None
  213. EditorMapComponent.current.editormap_widget.repaint()
  214. def show_all_layers(self) -> None:
  215. if not self.editormap:
  216. return
  217. # Get TilemapLayer, and set hidden
  218. layer = self.editormap.layers[0]
  219. assert isinstance(layer, ObjectLayer)
  220. object_layer = cast(ObjectLayer, layer)
  221. for object in object_layer.objects:
  222. if isinstance(object, ObjMapTilemapObject):
  223. layer = object.tilemap_layer
  224. if isinstance(layer, TilemapLayer):
  225. layer.hidden = False
  226. # Repaint so that changes are visible
  227. assert EditorMapComponent.current is not None
  228. EditorMapComponent.current.editormap_widget.repaint()
  229. def get_widget(self) -> QWidget:
  230. return self.vbox
  231. def get_layers(self) -> list[TilemapLayer]:
  232. """Returns all the tilemap_layers associated with current editormap"""
  233. assert self.editormap is not None
  234. return self.editormap.get_tilemap_layers()
  235. def get_layer(self, index: int) -> Optional[TilemapLayer]:
  236. """Gets tilemap from the list of tilemaps
  237. :param index: Which tilemap to get
  238. :return: TilemapLayer from tilemaps. None if invalid index
  239. """
  240. if index is not None:
  241. try:
  242. return self.get_layers()[index]
  243. except IndexError:
  244. pass
  245. return None
  246. def toggle_current(self) -> None:
  247. """Toggle the currently selected layer visibility"""
  248. self.toggle_hidden(self.selected_index)
  249. def toggle_show_only_selected(self) -> None:
  250. self.show_only_selected = not self.show_only_selected
  251. def toggle_action(self) -> None:
  252. """Toggle current layer and change eye icon to open/close"""
  253. self.toggle_current()
  254. if self.is_hidden(self.selected_index):
  255. self.current_hidden.setIcon(self.eye_closed_icon)
  256. else:
  257. self.current_hidden.setIcon(self.eye_open_icon)
  258. def get_selected(self) -> Optional[Layer]:
  259. """Returns TilemapLayer which is currently selected, if any"""
  260. return self.get_layer(self.selected_index)
  261. def selection_changed(self, selected: int, deselected: int) -> None:
  262. """Connected to LayerTreeView selectionChanged"""
  263. self.selected_index = selected
  264. layer = self.get_selected()
  265. if layer is not None:
  266. assert isinstance(layer, TilemapLayer)
  267. tilemap_layer: TilemapLayer = cast(TilemapLayer, layer)
  268. assert ToolContext.current is not None
  269. ToolContext.current.tilemap_layer = tilemap_layer
  270. if self.show_only_selected and selected:
  271. self.hide_all()
  272. self.set_hidden(selected, True)
  273. # Set toggle button
  274. self.current_hidden.setChecked(tilemap_layer.hidden)
  275. if tilemap_layer.hidden:
  276. self.current_hidden.setIcon(self.eye_closed_icon)
  277. else:
  278. self.current_hidden.setIcon(self.eye_open_icon)
  279. def add_layer(self, tilemap_object: Optional[ObjMapTilemapObject] = None) -> None:
  280. """Creates a new layer"""
  281. if tilemap_object is None:
  282. if self.generate_tilemap_obj is None:
  283. raise RuntimeError("Layer Selector cannot create tilemaps without metadata")
  284. tilemap_object = self.generate_tilemap_obj()
  285. # Add object to editormap
  286. assert self.editormap is not None
  287. assert isinstance(self.editormap.layers[0], ObjectLayer)
  288. cast(ObjectLayer, self.editormap.layers[0]).add_object(tilemap_object)
  289. # Create item
  290. item = QStandardItem(tilemap_object.tilemap_layer.name)
  291. # Set bold if required
  292. font = item.font()
  293. font.setBold(not tilemap_object.tilemap_layer.hidden)
  294. item.setFont(font)
  295. self.items.append(item)
  296. self.model.appendRow(item)
  297. def remove_current_layer(self) -> None:
  298. self.remove_layer(self.selected_index)
  299. def remove_layer(self, layer: int) -> None:
  300. """Deletes this layer safely, adding to the editormap's undo stack
  301. :param layer: Either a TilemapLayer, an ObjMapTilemapObject or an int (the layer to remove)
  302. """
  303. command = LayerDeleteCommand(self, layer)
  304. assert self.editormap is not None
  305. self.editormap.execute(command)
  306. def unsafe_remove_layer(self, layer: Union[int, TilemapLayer, ObjMapTilemapObject]) -> ObjMapTilemapObject:
  307. """Remove a layer without adding to undo_stack
  308. :param layer: Either a TilemapLayer, an ObjMapTilemapObject or an int (the layer to remove)
  309. """
  310. tilemap_layer: TilemapLayer
  311. if isinstance(layer, int):
  312. maybe_layer = self.get_layer(layer)
  313. assert maybe_layer is not None
  314. tilemap_layer = maybe_layer
  315. index = layer
  316. elif isinstance(layer, TilemapLayer):
  317. tilemap_layer = layer
  318. index = self.get_layers().index(tilemap_layer)
  319. elif isinstance(layer, ObjMapTilemapObject):
  320. tilemap_layer = layer.tilemap_layer
  321. index = self.get_layers().index(tilemap_layer)
  322. else:
  323. raise RuntimeError("Layer Selector: Cannot pass " + str(type(layer)) + " to _remove_layer\n" +
  324. "Try instead: ObjMapTilemapObject, TilemapLayer or int")
  325. assert self.editormap is not None
  326. tilemap_object = self.editormap.remove_tilemap_layer(tilemap_layer)
  327. self.model.removeRow(index)
  328. # Stop errors
  329. if self.selected_index == index:
  330. self.selected_index = -1
  331. self.tree_view.clearSelection()
  332. # Update EditorMap
  333. assert EditorMapComponent.current is not None
  334. EditorMapComponent.current.editormap_widget.repaint()
  335. return tilemap_object
  336. # EOF #