using System; using System.Collections.Generic; using System.Linq; using UnityEditor.EditorTools; using UnityEditor.ShortcutManagement; using UnityEngine; using UnityEngine.Tilemaps; using Event = UnityEngine.Event; using Object = UnityEngine.Object; using UnityEditor.Experimental.SceneManagement; using UnityEditor.SceneManagement; namespace UnityEditor.Tilemaps { internal class GridPaintPaletteWindow : EditorWindow { internal enum TilemapFocusMode { None = 0, Tilemap = 1, Grid = 2 } private static readonly string k_TilemapFocusModeEditorPref = "TilemapFocusMode"; private TilemapFocusMode focusMode { get { return (TilemapFocusMode)EditorPrefs.GetInt(k_TilemapFocusModeEditorPref, (int)TilemapFocusMode.None); } set { EditorPrefs.SetInt(k_TilemapFocusModeEditorPref, (int)value); } } private static readonly string k_TilemapLastPaletteEditorPref = "TilemapLastPalette"; private string lastTilemapPalette { get { return EditorPrefs.GetString(k_TilemapLastPaletteEditorPref, ""); } set { EditorPrefs.SetString(k_TilemapLastPaletteEditorPref, value); } } private static class MouseStyles { // The following paths match the enums in OperatingSystemFamily public static readonly string[] mouseCursorOSPath = { "", // Other OS "Cursors/macOS", "Cursors/Windows", "Cursors/Linux", }; // The following paths match the enums in OperatingSystemFamily public static readonly Vector2[] mouseCursorOSHotspot = { Vector2.zero, // Other OS new Vector2(6f, 4f), new Vector2(6f, 4f), new Vector2(6f, 4f), }; // The following paths match the enums in sceneViewEditModes above public static readonly string[] mouseCursorTexturePaths = { "", "Grid.MoveTool.png", "Grid.PaintTool.png", "Grid.BoxTool.png", "Grid.PickingTool.png", "Grid.EraserTool.png", "Grid.FillTool.png", }; public static readonly Texture2D[] mouseCursorTextures; static MouseStyles() { mouseCursorTextures = new Texture2D[mouseCursorTexturePaths.Length]; int osIndex = (int)SystemInfo.operatingSystemFamily; for (int i = 0; i < mouseCursorTexturePaths.Length; ++i) { if ((mouseCursorOSPath[osIndex] != null && mouseCursorOSPath[osIndex].Length > 0) && (mouseCursorTexturePaths[i] != null && mouseCursorTexturePaths[i].Length > 0)) { string cursorPath = Utils.Paths.Combine(mouseCursorOSPath[osIndex], mouseCursorTexturePaths[i]); mouseCursorTextures[i] = EditorGUIUtility.LoadRequired(cursorPath) as Texture2D; } else mouseCursorTextures[i] = null; } } } private static class Styles { public static readonly GUIContent emptyProjectInfo = EditorGUIUtility.TrTextContent("Create a new palette in the dropdown above."); public static readonly GUIContent emptyPaletteInfo = EditorGUIUtility.TrTextContent("Drag Tile, Sprite or Sprite Texture assets here."); public static readonly GUIContent invalidPaletteInfo = EditorGUIUtility.TrTextContent("This is an invalid palette. Did you delete the palette asset?"); public static readonly GUIContent invalidGridInfo = EditorGUIUtility.TrTextContent("The palette has an invalid Grid. Did you add a Grid to the palette asset?"); public static readonly GUIContent selectPaintTarget = EditorGUIUtility.TrTextContent("Select Paint Target"); public static readonly GUIContent selectPalettePrefab = EditorGUIUtility.TrTextContent("Select Palette Prefab"); public static readonly GUIContent selectTileAsset = EditorGUIUtility.TrTextContent("Select Tile Asset"); public static readonly GUIContent unlockPaletteEditing = EditorGUIUtility.TrTextContent("Unlock Palette Editing"); public static readonly GUIContent lockPaletteEditing = EditorGUIUtility.TrTextContent("Lock Palette Editing"); public static readonly GUIContent openTilePalettePreferences = EditorGUIUtility.TrTextContent("Open Tile Palette Preferences"); public static readonly GUIContent createNewPalette = EditorGUIUtility.TrTextContent("Create New Palette"); public static readonly GUIContent focusLabel = EditorGUIUtility.TrTextContent("Focus On"); public static readonly GUIContent rendererOverlayTitleLabel = EditorGUIUtility.TrTextContent("Tilemap"); public static readonly GUIContent activeTargetLabel = EditorGUIUtility.TrTextContent("Active Tilemap", "Specifies the currently active Tilemap used for painting in the Scene View."); public static readonly GUIContent prefabWarningIcon = EditorGUIUtility.TrIconContent("console.warnicon.sml", "Editing Tilemaps in Prefabs will have better performance if edited in Prefab Mode."); public static readonly GUIContent tilePalette = EditorGUIUtility.TrTextContent("Tile Palette"); public static readonly GUIContent edit = EditorGUIUtility.TrTextContent("Edit", "Toggle to edit current Tile Palette"); public static readonly GUIContent editModified = EditorGUIUtility.TrTextContent("Edit*", "Toggle to save edits for current Tile Palette"); public static readonly GUIContent gizmos = EditorGUIUtility.TrTextContent("Gizmos", "Toggle visibility of Gizmos in the Tile Palette"); public static readonly GUIContent lockZPosition = EditorGUIUtility.TrTextContent("Lock Z Position", "Toggle editing of Z position"); public static readonly GUIContent zPosition = EditorGUIUtility.TrTextContent("Z Position", "Set a Z position for the active Brush for painting"); public static readonly GUIContent resetZPosition = EditorGUIUtility.TrTextContent("Reset", "Reset Z position for the active Brush"); public static readonly GUIStyle ToolbarTitleStyle = "Toolbar"; public static readonly GUIStyle dragHandle = "RL DragHandle"; public static readonly float dragPadding = 3f; public static readonly GUILayoutOption[] dropdownOptions = { GUILayout.Width(k_DropdownWidth) }; } private class TilePaletteSaveScope : IDisposable { private GameObject m_GameObject; public TilePaletteSaveScope(GameObject paletteInstance) { m_GameObject = paletteInstance; if (m_GameObject != null) { GridPaintingState.savingPalette = true; SetHideFlagsRecursively(paletteInstance, HideFlags.HideInHierarchy); foreach (var renderer in paletteInstance.GetComponentsInChildren()) renderer.gameObject.layer = 0; } } public void Dispose() { if (m_GameObject != null) { SetHideFlagsRecursively(m_GameObject, HideFlags.HideAndDontSave); GridPaintingState.savingPalette = false; } } private void SetHideFlagsRecursively(GameObject root, HideFlags flags) { root.hideFlags = flags; for (int i = 0; i < root.transform.childCount; i++) SetHideFlagsRecursively(root.transform.GetChild(i).gameObject, flags); } } internal class TilePaletteProperties { public enum PrefabEditModeSettings { EnableDialog = 0, EditInPrefabMode = 1, EditInScene = 2 } public static readonly string targetEditModeDialogTitle = L10n.Tr("Open in Prefab Mode"); public static readonly string targetEditModeDialogMessage = L10n.Tr("Editing Tilemaps in Prefabs will have better performance if edited in Prefab Mode. Do you want to open it in Prefab Mode or edit it in the Scene?"); public static readonly string targetEditModeDialogYes = L10n.Tr("Prefab Mode"); public static readonly string targetEditModeDialogChange = L10n.Tr("Preferences"); public static readonly string targetEditModeDialogNo = L10n.Tr("Scene"); public static readonly string targetEditModeEditorPref = "TilePalette.TargetEditMode"; public static readonly string targetEditModeLookup = "Target Edit Mode"; public static readonly string tilePalettePreferencesLookup = "Tile Palette"; public static readonly GUIContent targetEditModeDialogLabel = EditorGUIUtility.TrTextContent(targetEditModeLookup, "Controls the behaviour of editing a Prefab Instance when one is selected as the Active Target in the Tile Palette"); } private static readonly GridBrushBase.Tool[] k_SceneViewEditModes = { GridBrushBase.Tool.Select, GridBrushBase.Tool.Move, GridBrushBase.Tool.Paint, GridBrushBase.Tool.Box, GridBrushBase.Tool.Pick, GridBrushBase.Tool.Erase, GridBrushBase.Tool.FloodFill }; private const float k_DropdownWidth = 200f; private const float k_ActiveTargetLabelWidth = 90f; private const float k_ActiveTargetDropdownWidth = 130f; private const float k_ActiveTargetWarningSize = 20f; private const float k_TopAreaHeight = 104f; private const float k_MinBrushInspectorHeight = 50f; private const float k_MinClipboardHeight = 200f; private const float k_ToolbarHeight = 17f; private const float k_ResizerDragRectPadding = 10f; private static readonly Vector2 k_MinWindowSize = new Vector2(k_ActiveTargetLabelWidth + k_ActiveTargetDropdownWidth + k_ActiveTargetWarningSize, 200f); private PaintableSceneViewGrid m_PaintableSceneViewGrid; class ShortcutContext : IShortcutToolContext { public bool active { get; set; } } readonly ShortcutContext m_ShortcutContext = new ShortcutContext { active = true }; [FormerlyPrefKeyAs("Grid Painting/Select", "s")] [Shortcut("Grid Painting/Select", typeof(ShortcutContext), KeyCode.S)] static void GridSelectKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(SelectTool)); } [FormerlyPrefKeyAs("Grid Painting/Move", "m")] [Shortcut("Grid Painting/Move", typeof(ShortcutContext), KeyCode.M)] static void GridMoveKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(MoveTool)); } [FormerlyPrefKeyAs("Grid Painting/Brush", "b")] [Shortcut("Grid Painting/Brush", typeof(ShortcutContext), KeyCode.B)] static void GridBrushKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(PaintTool)); } [FormerlyPrefKeyAs("Grid Painting/Rectangle", "u")] [Shortcut("Grid Painting/Rectangle", typeof(ShortcutContext), KeyCode.U)] static void GridRectangleKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(BoxTool)); } [FormerlyPrefKeyAs("Grid Painting/Picker", "i")] [Shortcut("Grid Painting/Picker", typeof(ShortcutContext), KeyCode.I)] static void GridPickerKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(PickingTool)); } [FormerlyPrefKeyAs("Grid Painting/Erase", "d")] [Shortcut("Grid Painting/Erase", typeof(ShortcutContext), KeyCode.D)] static void GridEraseKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(EraseTool)); } [FormerlyPrefKeyAs("Grid Painting/Fill", "g")] [Shortcut("Grid Painting/Fill", typeof(ShortcutContext), KeyCode.G)] static void GridFillKey() { TilemapEditorTool.ToggleActiveEditorTool(typeof(FillTool)); } static void RotateBrush(GridBrushBase.RotationDirection direction) { GridPaintingState.gridBrush.Rotate(direction, GridPaintingState.activeGrid.cellLayout); GridPaintingState.activeGrid.Repaint(); } [FormerlyPrefKeyAs("Grid Painting/Rotate Clockwise", "[")] [Shortcut("Grid Painting/Rotate Clockwise", typeof(ShortcutContext), KeyCode.LeftBracket)] static void RotateBrushClockwise() { if (GridPaintingState.gridBrush != null && GridPaintingState.activeGrid != null) RotateBrush(GridBrushBase.RotationDirection.Clockwise); } [FormerlyPrefKeyAs("Grid Painting/Rotate Anti-Clockwise", "]")] [Shortcut("Grid Painting/Rotate Anti-Clockwise", typeof(ShortcutContext), KeyCode.RightBracket)] static void RotateBrushAntiClockwise() { if (GridPaintingState.gridBrush != null && GridPaintingState.activeGrid != null) RotateBrush(GridBrushBase.RotationDirection.CounterClockwise); } static void FlipBrush(GridBrushBase.FlipAxis axis) { GridPaintingState.gridBrush.Flip(axis, GridPaintingState.activeGrid.cellLayout); GridPaintingState.activeGrid.Repaint(); } [FormerlyPrefKeyAs("Grid Painting/Flip X", "#[")] [Shortcut("Grid Painting/Flip X", typeof(ShortcutContext), KeyCode.LeftBracket, ShortcutModifiers.Shift)] static void FlipBrushX() { if (GridPaintingState.gridBrush != null && GridPaintingState.activeGrid != null) FlipBrush(GridBrushBase.FlipAxis.X); } [FormerlyPrefKeyAs("Grid Painting/Flip Y", "#]")] [Shortcut("Grid Painting/Flip Y", typeof(ShortcutContext), KeyCode.RightBracket, ShortcutModifiers.Shift)] static void FlipBrushY() { if (GridPaintingState.gridBrush != null && GridPaintingState.activeGrid != null) FlipBrush(GridBrushBase.FlipAxis.Y); } static void ChangeBrushZ(int change) { GridPaintingState.gridBrush.ChangeZPosition(change); GridPaintingState.activeGrid.ChangeZPosition(change); GridPaintingState.activeGrid.Repaint(); foreach (var window in GridPaintPaletteWindow.instances) { window.Repaint(); } } [Shortcut("Grid Painting/Increase Z", typeof(ShortcutContext), KeyCode.Minus)] static void IncreaseBrushZ() { if (GridPaintingState.gridBrush != null && GridPaintingState.activeGrid != null && GridPaintingState.activeBrushEditor != null && GridPaintingState.activeBrushEditor.canChangeZPosition) ChangeBrushZ(1); } [Shortcut("Grid Painting/Decrease Z", typeof(ShortcutContext), KeyCode.Equals)] static void DecreaseBrushZ() { if (GridPaintingState.gridBrush != null && GridPaintingState.activeGrid != null && GridPaintingState.activeBrushEditor != null && GridPaintingState.activeBrushEditor.canChangeZPosition) ChangeBrushZ(-1); } internal static void PreferencesGUI() { using (new SettingsWindow.GUIScope()) { EditorGUI.BeginChangeCheck(); var val = (TilePaletteProperties.PrefabEditModeSettings)EditorGUILayout.EnumPopup(TilePaletteProperties.targetEditModeDialogLabel, (TilePaletteProperties.PrefabEditModeSettings)EditorPrefs.GetInt(TilePaletteProperties.targetEditModeEditorPref, 0)); if (EditorGUI.EndChangeCheck()) { EditorPrefs.SetInt(TilePaletteProperties.targetEditModeEditorPref, (int)val); } } } private static List s_Instances; public static List instances { get { if (s_Instances == null) s_Instances = new List(); return s_Instances; } } public static bool isActive { get { return s_Instances != null && s_Instances.Count > 0; } } [SerializeField] private PreviewResizer m_PreviewResizer; private GridPalettesDropdown m_PaletteDropdown; [SerializeField] private GameObject m_Palette; [SerializeField] private bool m_DrawGizmos; internal bool drawGizmos { get { return m_DrawGizmos; } } public GameObject palette { get { return m_Palette; } set { if (m_Palette != value) { clipboardView.OnBeforePaletteSelectionChanged(); m_Palette = value; clipboardView.OnAfterPaletteSelectionChanged(); lastTilemapPalette = AssetDatabase.GetAssetPath(m_Palette); GridPaintingState.OnPaletteChanged(m_Palette); Repaint(); } } } private GameObject m_PaletteInstance; public GameObject paletteInstance { get { return m_PaletteInstance; } } private bool m_DelayedResetPaletteInstance; private bool m_Enabled; public GridPaintPaletteClipboard clipboardView { get; private set; } private Vector2 m_BrushScroll; private GridBrushEditorBase m_PreviousToolActivatedEditor; private GridBrushBase.Tool m_PreviousToolActivated; private PreviewRenderUtility m_PreviewUtility; public PreviewRenderUtility previewUtility { get { if (m_Enabled && m_PreviewUtility == null) InitPreviewUtility(); return m_PreviewUtility; } } private void OnSelectionChange() { // Update active palette if user has selected a palette prefab var selectedObject = Selection.activeGameObject; if (selectedObject != null) { bool isPrefab = EditorUtility.IsPersistent(selectedObject) || (selectedObject.hideFlags & HideFlags.NotEditable) != 0; if (isPrefab) { var assetPath = AssetDatabase.GetAssetPath(selectedObject); var allAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath); foreach (var asset in allAssets) { if (asset != null && asset.GetType() == typeof(GridPalette)) { var targetPalette = (GameObject)AssetDatabase.LoadMainAssetAtPath(assetPath); if (targetPalette != palette) { palette = targetPalette; Repaint(); } break; } } } } } private void OnGUI() { HandleContextMenu(); EditorGUILayout.BeginVertical(); GUILayout.Space(10f); EditorGUILayout.BeginHorizontal(); float leftMargin = (Screen.width / EditorGUIUtility.pixelsPerPoint - TilemapEditorTool.tilemapEditorToolsToolbarSize) * 0.5f; GUILayout.Space(leftMargin); DoTilemapToolbar(); GUILayout.Space(leftMargin); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Space(leftMargin); DoActiveTargetsGUI(); GUILayout.Space(leftMargin); EditorGUILayout.EndHorizontal(); GUILayout.Space(6f); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(); Rect clipboardToolbarRect = EditorGUILayout.BeginHorizontal(GUIContent.none, Styles.ToolbarTitleStyle); DoClipboardHeader(); EditorGUILayout.EndHorizontal(); ConvertGridPrefabToPalette(clipboardToolbarRect); Rect dragRect = new Rect(k_DropdownWidth + k_ResizerDragRectPadding, 0, position.width - k_DropdownWidth - k_ResizerDragRectPadding, k_ToolbarHeight); float brushInspectorSize = m_PreviewResizer.ResizeHandle(position, k_MinBrushInspectorHeight, k_MinClipboardHeight, k_ToolbarHeight, dragRect); float clipboardHeight = position.height - brushInspectorSize - k_TopAreaHeight; Rect clipboardRect = new Rect(0f, clipboardToolbarRect.yMax, position.width, clipboardHeight); OnClipboardGUI(clipboardRect); EditorGUILayout.EndVertical(); GUILayout.Space(clipboardRect.height); EditorGUILayout.BeginVertical(); EditorGUILayout.BeginHorizontal(GUIContent.none, Styles.ToolbarTitleStyle); DoBrushesDropdownToolbar(); EditorGUILayout.EndHorizontal(); m_BrushScroll = GUILayout.BeginScrollView(m_BrushScroll, false, false); GUILayout.Space(4f); OnBrushInspectorGUI(); GUILayout.EndScrollView(); EditorGUILayout.EndVertical(); Color oldColor = Handles.color; Handles.color = Color.black; Handles.DrawLine(new Vector3(0, clipboardRect.yMax + 0.5f, 0), new Vector3(Screen.width, clipboardRect.yMax + 0.5f, 0)); Handles.color = Color.black.AlphaMultiplied(0.33f); Handles.DrawLine(new Vector3(0, GUILayoutUtility.GetLastRect().yMax + 0.5f, 0), new Vector3(Screen.width, GUILayoutUtility.GetLastRect().yMax + 0.5f, 0)); Handles.color = oldColor; EditorGUILayout.BeginVertical(); GUILayout.Space(2f); EditorGUILayout.EndVertical(); // Keep repainting until all previews are loaded if (AssetPreview.IsLoadingAssetPreviews(GetInstanceID())) Repaint(); // Release keyboard focus on click to empty space if (Event.current.type == EventType.MouseDown) GUIUtility.keyboardControl = 0; } static void DoTilemapToolbar() { EditorTool active = EditorToolManager.activeTool; EditorTool selected; if (EditorGUILayout.EditorToolbar(active, TilemapEditorTool.tilemapEditorTools, out selected)) { if (active == selected) ToolManager.SetActiveTool(EditorToolManager.GetLastTool(x => !TilemapEditorTool.tilemapEditorTools.Contains(x))); else ToolManager.SetActiveTool(selected); } } public void DelayedResetPreviewInstance() { m_DelayedResetPaletteInstance = true; } public void ResetPreviewInstance() { if (m_PreviewUtility == null) InitPreviewUtility(); m_DelayedResetPaletteInstance = false; DestroyPreviewInstance(); if (palette != null) { m_PaletteInstance = previewUtility.InstantiatePrefabInScene(palette); // Disconnecting prefabs is no longer possible. // If performance of overrides on palette palette instance turns out to be a problem. // unpack the prefab instance here, and overwrite the prefab later instead of reconnecting. PrefabUtility.UnpackPrefabInstance(m_PaletteInstance, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); EditorUtility.InitInstantiatedPreviewRecursive(m_PaletteInstance); m_PaletteInstance.transform.position = new Vector3(0, 0, 0); m_PaletteInstance.transform.rotation = Quaternion.identity; m_PaletteInstance.transform.localScale = Vector3.one; GridPalette paletteAsset = GridPaletteUtility.GetGridPaletteFromPaletteAsset(palette); if (paletteAsset != null) { if (paletteAsset.cellSizing == GridPalette.CellSizing.Automatic) { Grid grid = m_PaletteInstance.GetComponent(); if (grid != null) { grid.cellSize = GridPaletteUtility.CalculateAutoCellSize(grid, grid.cellSize); } else { Debug.LogWarning("Grid component not found from: " + palette.name); } } previewUtility.camera.transparencySortMode = paletteAsset.transparencySortMode; previewUtility.camera.transparencySortAxis = paletteAsset.transparencySortAxis; } else { Debug.LogWarning("GridPalette subasset not found from: " + palette.name); previewUtility.camera.transparencySortMode = TransparencySortMode.Default; previewUtility.camera.transparencySortAxis = new Vector3(0f, 0f, 1f); } foreach (var transform in m_PaletteInstance.GetComponentsInChildren()) transform.gameObject.hideFlags = HideFlags.HideAndDontSave; // Show all renderers from Palettes from previous versions PreviewRenderUtility.SetEnabledRecursive(m_PaletteInstance, true); clipboardView.ResetPreviewMesh(); } } public void DestroyPreviewInstance() { if (m_PaletteInstance != null) { Undo.ClearUndo(m_PaletteInstance); DestroyImmediate(m_PaletteInstance); } } public void InitPreviewUtility() { int previewCullingLayer = Camera.PreviewCullingLayer; m_PreviewUtility = new PreviewRenderUtility(true, true); m_PreviewUtility.camera.cullingMask = 1 << previewCullingLayer; m_PreviewUtility.camera.gameObject.layer = previewCullingLayer; m_PreviewUtility.lights[0].gameObject.layer = previewCullingLayer; m_PreviewUtility.camera.orthographic = true; m_PreviewUtility.camera.orthographicSize = 5f; m_PreviewUtility.camera.transform.position = new Vector3(0f, 0f, -10f); m_PreviewUtility.ambientColor = new Color(1f, 1f, 1f, 0); ResetPreviewInstance(); clipboardView.SetupPreviewCameraOnInit(); } private void HandleContextMenu() { if (Event.current.type == EventType.ContextClick) { DoContextMenu(); Event.current.Use(); } } public void SavePalette() { if (paletteInstance != null && palette != null) { using (new TilePaletteSaveScope(paletteInstance)) { string path = AssetDatabase.GetAssetPath(palette); PrefabUtility.SaveAsPrefabAssetAndConnect(paletteInstance, path, InteractionMode.AutomatedAction); } ResetPreviewInstance(); Repaint(); } } private void DoContextMenu() { GenericMenu pm = new GenericMenu(); if (GridPaintingState.scenePaintTarget != null) pm.AddItem(Styles.selectPaintTarget, false, SelectPaintTarget); else pm.AddDisabledItem(Styles.selectPaintTarget); if (palette != null) pm.AddItem(Styles.selectPalettePrefab, false, SelectPaletteAsset); else pm.AddDisabledItem(Styles.selectPalettePrefab); if (clipboardView.activeTile != null) pm.AddItem(Styles.selectTileAsset, false, SelectTileAsset); else pm.AddDisabledItem(Styles.selectTileAsset); pm.AddSeparator(""); if (clipboardView.unlocked) pm.AddItem(Styles.lockPaletteEditing, false, FlipLocked); else pm.AddItem(Styles.unlockPaletteEditing, false, FlipLocked); pm.AddItem(Styles.openTilePalettePreferences, false, OpenTilePalettePreferences); pm.ShowAsContext(); } private void OpenTilePalettePreferences() { var settingsWindow = SettingsWindow.Show(SettingsScope.User); settingsWindow.FilterProviders(TilePaletteProperties.tilePalettePreferencesLookup); } private void FlipLocked() { clipboardView.unlocked = !clipboardView.unlocked; } private void SelectPaintTarget() { Selection.activeObject = GridPaintingState.scenePaintTarget; } private void SelectPaletteAsset() { Selection.activeObject = palette; } private void SelectTileAsset() { Selection.activeObject = clipboardView.activeTile; } private bool NotOverridingColor(GridBrush defaultGridBrush) { foreach (var cell in defaultGridBrush.cells) { TileBase tile = cell.tile; if (tile is Tile && ((tile as Tile).flags & TileFlags.LockColor) == 0) { return true; } } return false; } private void DoBrushesDropdownToolbar() { GUIContent content = GUIContent.Temp(GridPaintingState.gridBrush.name); if (EditorGUILayout.DropdownButton(content, FocusType.Passive, EditorStyles.toolbarPopup, Styles.dropdownOptions)) { var menuData = new GridBrushesDropdown.MenuItemProvider(); var flexibleMenu = new GridBrushesDropdown(menuData, GridPaletteBrushes.brushes.IndexOf(GridPaintingState.gridBrush), null, SelectBrush, k_DropdownWidth); PopupWindow.Show(GUILayoutUtility.topLevel.GetLast(), flexibleMenu); } if (Event.current.type == EventType.Repaint) { var dragRect = GUILayoutUtility.GetLastRect(); var dragIconRect = new Rect(); dragIconRect.x = dragRect.x + dragRect.width + Styles.dragPadding; dragIconRect.y = dragRect.y + (dragRect.height - Styles.dragHandle.fixedHeight) / 2 + 1; dragIconRect.width = position.width - (dragIconRect.x) - Styles.dragPadding; dragIconRect.height = Styles.dragHandle.fixedHeight; Styles.dragHandle.Draw(dragIconRect, GUIContent.none, false, false, false, false); } GUILayout.FlexibleSpace(); } private void SelectBrush(int i, object o) { GridPaintingState.gridBrush = GridPaletteBrushes.brushes[i]; } public void OnEnable() { m_Enabled = true; instances.Add(this); if (clipboardView == null) { clipboardView = CreateInstance(); clipboardView.owner = this; clipboardView.hideFlags = HideFlags.HideAndDontSave; clipboardView.unlocked = false; } if (m_PaintableSceneViewGrid == null) { m_PaintableSceneViewGrid = CreateInstance(); m_PaintableSceneViewGrid.hideFlags = HideFlags.HideAndDontSave; } GridPaletteBrushes.FlushCache(); ShortcutIntegration.instance.profileManager.shortcutBindingChanged += UpdateTooltips; GridSelection.gridSelectionChanged += OnGridSelectionChanged; GridPaintingState.RegisterPainterInterest(this); GridPaintingState.scenePaintTargetChanged += OnScenePaintTargetChanged; GridPaintingState.brushChanged += OnBrushChanged; SceneView.duringSceneGui += OnSceneViewGUI; PrefabUtility.prefabInstanceUpdated += PrefabInstanceUpdated; EditorApplication.projectWasLoaded += OnProjectLoaded; AssetPreview.SetPreviewTextureCacheSize(256, GetInstanceID()); wantsMouseMove = true; wantsMouseEnterLeaveWindow = true; if (m_PreviewResizer == null) { m_PreviewResizer = new PreviewResizer(); m_PreviewResizer.Init("TilemapBrushInspector"); } minSize = k_MinWindowSize; if (palette == null && !String.IsNullOrEmpty(lastTilemapPalette)) { palette = GridPalettes.palettes .Where((palette, index) => (AssetDatabase.GetAssetPath(palette) == lastTilemapPalette)) .FirstOrDefault(); } if (palette == null && GridPalettes.palettes.Count > 0) { palette = GridPalettes.palettes[0]; } ToolManager.activeToolChanged += ActiveToolChanged; ToolManager.activeToolChanging += ActiveToolChanging; ShortcutIntegration.instance.contextManager.RegisterToolContext(m_ShortcutContext); } private static void UpdateTooltips(IShortcutProfileManager obj, Identifier identifier, ShortcutBinding oldBinding, ShortcutBinding newBinding) { TilemapEditorTool.UpdateTooltips(); } private void PrefabInstanceUpdated(GameObject updatedPrefab) { // case 947462: Reset the palette instance after its prefab has been updated as it could have been changed if (m_PaletteInstance != null && PrefabUtility.GetCorrespondingObjectFromSource(updatedPrefab) == m_Palette && !GridPaintingState.savingPalette) { ResetPreviewInstance(); Repaint(); } } private void OnProjectLoaded() { // ShortcutIntegration instance is recreated after LoadLayout which wipes the OnEnable registration ShortcutIntegration.instance.contextManager.RegisterToolContext(m_ShortcutContext); } private void OnBrushChanged(GridBrushBase brush) { DisableFocus(); if (brush is GridBrush) EnableFocus(); SceneView.RepaintAll(); } private void OnGridSelectionChanged() { Repaint(); } public void OnDisable() { m_Enabled = false; DisableFocus(); focusMode = TilemapFocusMode.None; CallOnToolDeactivated(); instances.Remove(this); if (instances.Count <= 1) GridPaintingState.gridBrush = null; DestroyPreviewInstance(); DestroyImmediate(clipboardView); DestroyImmediate(m_PaintableSceneViewGrid); if (m_PreviewUtility != null) m_PreviewUtility.Cleanup(); m_PreviewUtility = null; if (PaintableGrid.InGridEditMode()) { // Set Editor Tool to an always available Tool, as Tile Palette Tools are not available any more ToolManager.SetActiveTool(); } ShortcutIntegration.instance.profileManager.shortcutBindingChanged -= UpdateTooltips; ToolManager.activeToolChanged -= ActiveToolChanged; ToolManager.activeToolChanging -= ActiveToolChanging; GridSelection.gridSelectionChanged -= OnGridSelectionChanged; SceneView.duringSceneGui -= OnSceneViewGUI; GridPaintingState.scenePaintTargetChanged -= OnScenePaintTargetChanged; GridPaintingState.brushChanged -= OnBrushChanged; GridPaintingState.UnregisterPainterInterest(this); PrefabUtility.prefabInstanceUpdated -= PrefabInstanceUpdated; EditorApplication.projectWasLoaded -= OnProjectLoaded; ShortcutIntegration.instance.contextManager.DeregisterToolContext(m_ShortcutContext); } private void OnScenePaintTargetChanged(GameObject scenePaintTarget) { DisableFocus(); EnableFocus(); Repaint(); } private void ActiveToolChanged() { if (GridPaintingState.gridBrush != null && PaintableGrid.InGridEditMode() && GridPaintingState.activeBrushEditor != null) { GridBrushBase.Tool tool = PaintableGrid.EditTypeToBrushTool(ToolManager.activeToolType); GridPaintingState.activeBrushEditor.OnToolActivated(tool); m_PreviousToolActivatedEditor = GridPaintingState.activeBrushEditor; m_PreviousToolActivated = tool; for (int i = 0; i < k_SceneViewEditModes.Length; ++i) { if (k_SceneViewEditModes[i] == tool) { Cursor.SetCursor(MouseStyles.mouseCursorTextures[i], MouseStyles.mouseCursorTextures[i] != null ? MouseStyles.mouseCursorOSHotspot[(int)SystemInfo.operatingSystemFamily] : Vector2.zero, CursorMode.Auto); break; } } } else { Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto); } Repaint(); } private void ActiveToolChanging() { if (!TilemapEditorTool.IsActive(typeof(MoveTool)) && !TilemapEditorTool.IsActive(typeof(SelectTool))) { GridSelection.Clear(); } CallOnToolDeactivated(); } private void CallOnToolDeactivated() { if (GridPaintingState.gridBrush != null && m_PreviousToolActivatedEditor != null) { m_PreviousToolActivatedEditor.OnToolDeactivated(m_PreviousToolActivated); m_PreviousToolActivatedEditor = null; if (!PaintableGrid.InGridEditMode()) Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto); } } internal void ResetZPosition() { GridPaintingState.gridBrush.ResetZPosition(); GridPaintingState.lastActiveGrid.ResetZPosition(); } private void OnBrushInspectorGUI() { if (GridPaintingState.gridBrush == null) return; // Brush Inspector GUI EditorGUI.BeginChangeCheck(); if (GridPaintingState.activeBrushEditor != null) GridPaintingState.activeBrushEditor.OnPaintInspectorGUI(); else if (GridPaintingState.fallbackEditor != null) GridPaintingState.fallbackEditor.OnInspectorGUI(); if (EditorGUI.EndChangeCheck()) { GridPaletteBrushes.ActiveGridBrushAssetChanged(); } // Z Position Inspector var hasLastActiveGrid = GridPaintingState.lastActiveGrid != null; using (new EditorGUI.DisabledScope(!hasLastActiveGrid)) { var lockZPosition = false; if (GridPaintingState.activeBrushEditor != null) { EditorGUI.BeginChangeCheck(); lockZPosition = EditorGUILayout.Toggle(Styles.lockZPosition, !GridPaintingState.activeBrushEditor.canChangeZPosition); if (EditorGUI.EndChangeCheck()) GridPaintingState.activeBrushEditor.canChangeZPosition = !lockZPosition; } using (new EditorGUI.DisabledScope(lockZPosition)) { EditorGUILayout.BeginHorizontal(); EditorGUI.BeginChangeCheck(); var zPosition = EditorGUILayout.DelayedIntField(Styles.zPosition, hasLastActiveGrid ? GridPaintingState.lastActiveGrid.zPosition : 0); if (EditorGUI.EndChangeCheck()) { GridPaintingState.gridBrush.ChangeZPosition(zPosition - GridPaintingState.lastActiveGrid.zPosition); GridPaintingState.lastActiveGrid.zPosition = zPosition; } if (GUILayout.Button(Styles.resetZPosition)) { ResetZPosition(); } EditorGUILayout.EndHorizontal(); } } } private bool IsObjectPrefabInstance(Object target) { return target != null && PrefabUtility.IsPartOfRegularPrefab(target); } private GameObject FindPrefabInstanceEquivalent(GameObject prefabInstance, GameObject prefabTarget) { var prefabRoot = prefabTarget.transform.root.gameObject; var currentTransform = prefabTarget.transform; var reverseTransformOrder = new Stack(); while (currentTransform != prefabRoot.transform && currentTransform.parent != null) { var parentTransform = currentTransform.parent; for (int i = 0; i < parentTransform.childCount; ++i) { if (currentTransform == parentTransform.GetChild(i)) { reverseTransformOrder.Push(i); break; } } currentTransform = currentTransform.parent; } currentTransform = prefabInstance.transform; while (reverseTransformOrder.Count > 0) { var childIndex = reverseTransformOrder.Pop(); if (childIndex >= currentTransform.childCount) return null; currentTransform = currentTransform.GetChild(childIndex); } return currentTransform.gameObject; } private void GoToPrefabMode(GameObject target) { var prefabObject = PrefabUtility.GetCorrespondingObjectFromSource(target); var assetPath = AssetDatabase.GetAssetPath(prefabObject); var stage = PrefabStageUtility.OpenPrefab(assetPath); var prefabInstance = stage.prefabContentsRoot; var prefabTarget = FindPrefabInstanceEquivalent(prefabInstance, prefabObject); if (prefabTarget != null) { GridPaintingState.scenePaintTarget = prefabTarget; } } private void DoActiveTargetsGUI() { using (new EditorGUI.DisabledScope(GridPaintingState.validTargets == null || GridPaintingState.scenePaintTarget == null)) { bool hasPaintTarget = GridPaintingState.scenePaintTarget != null; bool needWarning = IsObjectPrefabInstance(GridPaintingState.scenePaintTarget); GUILayout.Label(Styles.activeTargetLabel, GUILayout.Width(k_ActiveTargetLabelWidth), GUILayout.Height(k_ActiveTargetWarningSize)); GUIContent content = GUIContent.Temp(hasPaintTarget ? GridPaintingState.scenePaintTarget.name : "Nothing"); if (EditorGUILayout.DropdownButton(content, FocusType.Passive, EditorStyles.popup, GUILayout.Width(k_ActiveTargetDropdownWidth - (needWarning ? k_ActiveTargetWarningSize : 0f)), GUILayout.Height(k_ActiveTargetWarningSize))) { int index = hasPaintTarget ? Array.IndexOf(GridPaintingState.validTargets, GridPaintingState.scenePaintTarget) : 0; var menuData = new GridPaintTargetsDropdown.MenuItemProvider(); var flexibleMenu = new GridPaintTargetsDropdown(menuData, index, null, SelectTarget, k_ActiveTargetDropdownWidth); PopupWindow.Show(GUILayoutUtility.topLevel.GetLast(), flexibleMenu); } if (needWarning) GUILayout.Label(Styles.prefabWarningIcon, GUILayout.Width(k_ActiveTargetWarningSize), GUILayout.Height(k_ActiveTargetWarningSize)); } } private void SelectTarget(int i, object o) { var obj = o as GameObject; var isPrefabInstance = IsObjectPrefabInstance(obj); if (isPrefabInstance) { var editMode = (TilePaletteProperties.PrefabEditModeSettings)EditorPrefs.GetInt(TilePaletteProperties.targetEditModeEditorPref, 0); switch (editMode) { case TilePaletteProperties.PrefabEditModeSettings.EnableDialog: { var option = EditorUtility.DisplayDialogComplex(TilePaletteProperties.targetEditModeDialogTitle , TilePaletteProperties.targetEditModeDialogMessage , TilePaletteProperties.targetEditModeDialogYes , TilePaletteProperties.targetEditModeDialogNo , TilePaletteProperties.targetEditModeDialogChange); switch (option) { case 0: GoToPrefabMode(obj); return; case 1: // Do nothing here for "No" break; case 2: var settingsWindow = SettingsWindow.Show(SettingsScope.User); settingsWindow.FilterProviders(TilePaletteProperties.targetEditModeLookup); break; } } break; case TilePaletteProperties.PrefabEditModeSettings.EditInPrefabMode: GoToPrefabMode(obj); return; case TilePaletteProperties.PrefabEditModeSettings.EditInScene: default: break; } } GridPaintingState.scenePaintTarget = obj; if (GridPaintingState.scenePaintTarget != null) EditorGUIUtility.PingObject(GridPaintingState.scenePaintTarget); } private void DoClipboardHeader() { if (!GridPalettes.palettes.Contains(palette) || palette == null) // Palette not in list means it was deleted { GridPalettes.CleanCache(); if (GridPalettes.palettes.Count > 0) { palette = GridPalettes.palettes.LastOrDefault(); } } EditorGUILayout.BeginHorizontal(); DoPalettesDropdown(); using (new EditorGUI.DisabledScope(palette == null)) { clipboardView.unlocked = GUILayout.Toggle(clipboardView.unlocked, clipboardView.isModified ? Styles.editModified : Styles.edit, EditorStyles.toolbarButton); } GUILayout.FlexibleSpace(); using (new EditorGUI.DisabledScope(palette == null)) { EditorGUI.BeginChangeCheck(); m_DrawGizmos = GUILayout.Toggle(m_DrawGizmos, Styles.gizmos, EditorStyles.toolbarButton); if (EditorGUI.EndChangeCheck()) { if (m_DrawGizmos) { clipboardView.SavePaletteIfNecessary(); ResetPreviewInstance(); } Repaint(); } } EditorGUILayout.EndHorizontal(); } private void DoPalettesDropdown() { string name = palette != null ? palette.name : Styles.createNewPalette.text; Rect rect = GUILayoutUtility.GetRect(GUIContent.Temp(name), EditorStyles.toolbarDropDown, Styles.dropdownOptions); if (GridPalettes.palettes.Count == 0) { if (EditorGUI.DropdownButton(rect, GUIContent.Temp(name), FocusType.Passive, EditorStyles.toolbarDropDown)) { OpenAddPalettePopup(rect); } } else { GUIContent content = GUIContent.Temp(GridPalettes.palettes.Count > 0 && palette != null ? palette.name : Styles.createNewPalette.text); if (EditorGUI.DropdownButton(rect, content, FocusType.Passive, EditorStyles.toolbarPopup)) { var menuData = new GridPalettesDropdown.MenuItemProvider(); m_PaletteDropdown = new GridPalettesDropdown(menuData, GridPalettes.palettes.IndexOf(palette), null, SelectPalette, k_DropdownWidth); PopupWindow.Show(GUILayoutUtility.topLevel.GetLast(), m_PaletteDropdown); } } } private void SelectPalette(int i, object o) { if (i < GridPalettes.palettes.Count) { palette = GridPalettes.palettes[i]; } else { m_PaletteDropdown.editorWindow.Close(); OpenAddPalettePopup(new Rect(0, 0, 0, 0)); } } private void OpenAddPalettePopup(Rect rect) { bool popupOpened = GridPaletteAddPopup.ShowAtPosition(rect, this); if (popupOpened) GUIUtility.ExitGUI(); } private void DisplayClipboardText(GUIContent clipboardText, Rect position) { Color old = GUI.color; GUI.color = Color.gray; var infoSize = GUI.skin.label.CalcSize(clipboardText); Rect rect = new Rect(position.center.x - infoSize.x * .5f, position.center.y - infoSize.y, 500, 100); GUI.Label(rect, clipboardText); GUI.color = old; } private void OnClipboardGUI(Rect position) { if (Event.current.type != EventType.Layout && position.Contains(Event.current.mousePosition) && GridPaintingState.activeGrid != clipboardView && clipboardView.unlocked) { GridPaintingState.activeGrid = clipboardView; SceneView.RepaintAll(); } // Validate palette (case 1017965) GUIContent paletteError = null; if (palette == null) { if (GridPalettes.palettes.Count == 0) paletteError = Styles.emptyProjectInfo; else paletteError = Styles.invalidPaletteInfo; } else if (palette.GetComponent() == null) { paletteError = Styles.invalidGridInfo; } if (paletteError != null) { DisplayClipboardText(paletteError, position); return; } bool oldEnabled = GUI.enabled; GUI.enabled = !clipboardView.showNewEmptyClipboardInfo || DragAndDrop.objectReferences.Length > 0; if (Event.current.type == EventType.Repaint) clipboardView.guiRect = position; if (m_DelayedResetPaletteInstance) ResetPreviewInstance(); EditorGUI.BeginChangeCheck(); clipboardView.OnGUI(); if (EditorGUI.EndChangeCheck()) Repaint(); GUI.enabled = oldEnabled; if (clipboardView.showNewEmptyClipboardInfo) { DisplayClipboardText(Styles.emptyPaletteInfo, position); } } private void ConvertGridPrefabToPalette(Rect targetPosition) { if (!targetPosition.Contains(Event.current.mousePosition) || (Event.current.type != EventType.DragPerform && Event.current.type != EventType.DragUpdated) || DragAndDrop.objectReferences.Length != 1) return; var draggedObject = DragAndDrop.objectReferences[0]; if (!PrefabUtility.IsPartOfRegularPrefab(draggedObject)) return; switch (Event.current.type) { case EventType.DragUpdated: { DragAndDrop.visualMode = DragAndDropVisualMode.Generic; Event.current.Use(); GUI.changed = true; } break; case EventType.DragPerform: { var path = AssetDatabase.GetAssetPath(draggedObject); var assets = AssetDatabase.LoadAllAssetsAtPath(path); bool hasNewPaletteAsset = false; Grid gridPrefab = null; foreach (var asset in assets) { var gridPalette = asset as GridPalette; hasNewPaletteAsset |= gridPalette != null; GameObject go = asset as GameObject; if (go != null) { var grid = go.GetComponent(); if (grid != null) gridPrefab = grid; } } if (!hasNewPaletteAsset && gridPrefab != null) { var cellLayout = gridPrefab.cellLayout; var cellSizing = (cellLayout == GridLayout.CellLayout.Rectangle || cellLayout == GridLayout.CellLayout.Hexagon) ? GridPalette.CellSizing.Automatic : GridPalette.CellSizing.Manual; var newPalette = GridPaletteUtility.CreateGridPalette(cellSizing); AssetDatabase.AddObjectToAsset(newPalette, path); AssetDatabase.ForceReserializeAssets(new string[] {path}); AssetDatabase.SaveAssets(); Event.current.Use(); GUIUtility.ExitGUI(); } } break; } } private void OnSceneViewGUI(SceneView sceneView) { if (GridPaintingState.defaultBrush != null && GridPaintingState.scenePaintTarget != null) SceneViewOverlay.Window(Styles.rendererOverlayTitleLabel, DisplayFocusMode, (int)SceneViewOverlay.Ordering.TilemapRenderer, SceneViewOverlay.WindowDisplayOption.OneWindowPerTitle); else if (focusMode != TilemapFocusMode.None) { // case 946284: Disable Focus if focus mode is set but there is nothing to focus on DisableFocus(); focusMode = TilemapFocusMode.None; } } internal void SetFocusMode(TilemapFocusMode tilemapFocusMode) { if (tilemapFocusMode != focusMode) { DisableFocus(); focusMode = tilemapFocusMode; EnableFocus(); } } private void DisplayFocusMode(Object displayTarget, SceneView sceneView) { var labelWidth = EditorGUIUtility.labelWidth; var fieldWidth = EditorGUIUtility.fieldWidth; EditorGUIUtility.labelWidth = EditorGUIUtility.fieldWidth = 0.5f * (EditorGUIUtility.labelWidth + EditorGUIUtility.fieldWidth); var newFocus = (TilemapFocusMode)EditorGUILayout.EnumPopup(Styles.focusLabel, focusMode); SetFocusMode(newFocus); EditorGUIUtility.labelWidth = labelWidth; EditorGUIUtility.fieldWidth = fieldWidth; } private void FilterSingleSceneObjectInScene(int instanceID) { if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.SetSceneViewFiltering(true); StageHandle currentStageHandle = StageUtility.GetCurrentStageHandle(); if (currentStageHandle.IsValid() && !currentStageHandle.isMainStage) { HierarchyProperty.FilterSingleSceneObjectInScene(instanceID , false , new UnityEngine.SceneManagement.Scene[] { currentStageHandle.customScene }); } else { HierarchyProperty.FilterSingleSceneObject(instanceID, false); } if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.Repaint(); } private void EnableFocus() { if (GridPaintingState.scenePaintTarget == null) return; switch (focusMode) { case TilemapFocusMode.Tilemap: { FilterSingleSceneObjectInScene(GridPaintingState.scenePaintTarget.GetInstanceID()); break; } case TilemapFocusMode.Grid: { Tilemap tilemap = GridPaintingState.scenePaintTarget.GetComponent(); if (tilemap != null && tilemap.layoutGrid != null) { FilterSingleSceneObjectInScene(tilemap.layoutGrid.gameObject.GetInstanceID()); } break; } default: { break; } } } private void DisableFocus() { if (focusMode == TilemapFocusMode.None) return; StageHandle currentStageHandle = StageUtility.GetCurrentStageHandle(); if (currentStageHandle.IsValid() && !currentStageHandle.isMainStage) { HierarchyProperty.ClearSceneObjectsFilterInScene(new UnityEngine.SceneManagement.Scene[] { currentStageHandle.customScene }); } else { HierarchyProperty.ClearSceneObjectsFilter(); } if (SceneView.lastActiveSceneView != null) { SceneView.lastActiveSceneView.SetSceneViewFiltering(false); SceneView.lastActiveSceneView.Repaint(); } } [MenuItem("Window/2D/Tile Palette", false, 2)] public static void OpenTilemapPalette() { GridPaintPaletteWindow w = GetWindow(); w.titleContent = Styles.tilePalette; } // TODO: Better way of clearing caches than AssetPostprocessor public class AssetProcessor : AssetPostprocessor { public override int GetPostprocessOrder() { return int.MaxValue; } private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromPath) { if (GridPaintingState.savingPalette) return; foreach (var window in instances) { window.DelayedResetPreviewInstance(); } } } public class PaletteAssetModificationProcessor : AssetModificationProcessor { static void OnWillCreateAsset(string assetName) { SavePalettesIfRequired(null); } static string[] OnWillSaveAssets(string[] paths) { SavePalettesIfRequired(paths); return paths; } static void SavePalettesIfRequired(string[] paths) { if (GridPaintingState.savingPalette) return; foreach (var window in instances) { if (window.clipboardView.isModified) { window.clipboardView.CheckRevertIfChanged(paths); window.clipboardView.SavePaletteIfNecessary(); window.Repaint(); } } } } } }