using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using IronPython.Modules;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Input;
namespace HavenSoft.HexManiac.Core.ViewModels.Map {
///
/// Represents the entire map editor tab, with all visible controls, maps, edit boxes, etc
///
public class MapEditorViewModel : ViewModelCore, ITabContent {
private readonly IFileSystem fileSystem;
private readonly IEditableViewPort viewPort;
private readonly IDataModel model;
private readonly ChangeHistory history;
private readonly Singletons singletons;
private BlockMapViewModel primaryMap;
public BlockMapViewModel PrimaryMap {
get => primaryMap;
set {
if (primaryMap == value) return;
primaryMap = value;
NotifyPropertyChanged();
}
}
private IEventModel selectedEvent;
public IEventModel SelectedEvent {
get => selectedEvent;
set {
selectedEvent = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(ShowEventPanel));
}
}
public bool ShowEventPanel => selectedEvent != null;
public ObservableCollection VisibleMaps { get; } = new();
public ObservableCollection MapButtons { get; } = new();
#region Block Picker
public IPixelViewModel Blocks => primaryMap?.BlockPixels;
private int drawBlockIndex = -10;
public int DrawBlockIndex {
get => drawBlockIndex;
set {
Set(ref drawBlockIndex, value, old => {
NotifyPropertyChanged(nameof(HighlightBlockX));
NotifyPropertyChanged(nameof(HighlightBlockY));
NotifyPropertyChanged(nameof(HighlightBlockSize));
});
}
}
private int collisionIndex = -1;
public int CollisionIndex {
get => collisionIndex;
set {
Set(ref collisionIndex, value, old => {
primaryMap.CollisionHighlight = value;
});
}
}
public ObservableCollection CollisionOptions { get; } = new();
public double HighlightBlockX => (drawBlockIndex % BlockMapViewModel.BlocksPerRow) * Blocks.SpriteScale * 16;
public double HighlightBlockY => (drawBlockIndex / BlockMapViewModel.BlocksPerRow) * Blocks.SpriteScale * 16;
public double HighlightBlockSize => 16 * Blocks.SpriteScale + 2;
#endregion
#region ITabContent
private StubCommand close, undo, redo, backCommand, forwardCommand;
public string Name => (primaryMap?.Name ?? "Map") + (history.HasDataChange ? "*" : string.Empty);
public string FullFileName => viewPort.FullFileName;
public bool IsMetadataOnlyChange => false;
public ICommand Save => viewPort.Save;
public ICommand SaveAs => viewPort.SaveAs;
public ICommand ExportBackup => null;
public ICommand Undo => StubCommand(ref undo,
() => {
history.Undo.Execute();
Refresh();
},
() => history.Undo.CanExecute(default));
public ICommand Redo => StubCommand(ref redo, () => history.Redo.Execute(), () => history.Redo.CanExecute(default));
public ICommand Copy => null;
public ICommand DeepCopy => null;
public ICommand Clear => null;
public ICommand SelectAll => null;
public ICommand Goto => null;
public ICommand ResetAlignment => null;
public ICommand Back => StubCommand(ref backCommand, ExecuteBack, CanExecuteBack);
public ICommand Forward => StubCommand(ref forwardCommand, ExecuteForward, CanExecuteForward);
public ICommand Close => StubCommand(ref close, () => Closed.Raise(this));
public ICommand Diff => null;
public ICommand DiffLeft => null;
public ICommand DiffRight => null;
public bool CanDuplicate => false;
public event EventHandler OnError;
public event EventHandler OnMessage;
public event EventHandler ClearMessage;
public event EventHandler Closed;
public event EventHandler RequestTabChange;
public event EventHandler RequestDelayedWork;
public event EventHandler RequestMenuClose;
public event EventHandler RequestDiff;
public event EventHandler RequestCanDiff;
public event EventHandler RequestCanCreatePatch;
public event EventHandler RequestCreatePatch;
public void Duplicate() { }
public void Refresh() {
VisibleMaps.Clear();
primaryMap.ClearCaches();
UpdatePrimaryMap(primaryMap);
}
public bool TryImport(LoadedFile file, IFileSystem fileSystem) => false;
private readonly List forwardStack = new(), backStack = new();
private bool CanExecuteBack() => backStack.Count > 0;
private bool CanExecuteForward() => forwardStack.Count > 0;
private void ExecuteBack() {
if (backStack.Count == 0) return;
var previous = backStack[backStack.Count - 1];
backStack.RemoveAt(backStack.Count - 1);
if (primaryMap != null) forwardStack.Add(primaryMap.MapID);
if (forwardStack.Count == 1) forwardCommand.RaiseCanExecuteChanged();
if (backStack.Count == 0) backCommand.RaiseCanExecuteChanged();
NavigateTo(previous, trackChange: false);
}
private void ExecuteForward() {
if (forwardStack.Count == 0) return;
var next = forwardStack[forwardStack.Count - 1];
forwardStack.RemoveAt(forwardStack.Count - 1);
if (primaryMap != null) backStack.Add(primaryMap.MapID);
if (backStack.Count == 1) backCommand.RaiseCanExecuteChanged();
if (forwardStack.Count == 0) forwardCommand.RaiseCanExecuteChanged();
NavigateTo(next, trackChange: false);
}
private void NavigateTo(int mapID, bool trackChange = true) => NavigateTo(mapID / 1000, mapID % 1000, trackChange);
private void NavigateTo(int bank, int map, bool trackChange = true) {
if (trackChange && primaryMap != null) {
backStack.Add(primaryMap.MapID);
if (backStack.Count == 1) backCommand.RaiseCanExecuteChanged();
if (forwardStack.Count > 0) {
forwardStack.Clear();
forwardCommand.RaiseCanExecuteChanged();
}
}
UpdatePrimaryMap(new BlockMapViewModel(fileSystem, model, () => history.CurrentChange, bank, map) {
IncludeBorders = primaryMap?.IncludeBorders ?? true,
SpriteScale = primaryMap?.SpriteScale ?? .5,
});
}
#endregion
public MapEditorViewModel(IFileSystem fileSystem, IEditableViewPort viewPort, Singletons singletons) {
(this.viewPort, this.model, this.history, this.singletons, this.fileSystem) = (viewPort, viewPort.Model, viewPort.ChangeHistory, singletons, fileSystem);
history.Undo.CanExecuteChanged += (sender, e) => undo.RaiseCanExecuteChanged();
history.Redo.CanExecuteChanged += (sender, e) => redo.RaiseCanExecuteChanged();
history.Bind(nameof(history.HasDataChange), (sender, e) => NotifyPropertyChanged(nameof(Name)));
var map = new BlockMapViewModel(fileSystem, model, () => history.CurrentChange, 3, 19) { IncludeBorders = true, SpriteScale = .5 };
UpdatePrimaryMap(map);
for (int i = 0; i < 0x40; i++) CollisionOptions.Add(i.ToString("X2"));
}
private void UpdatePrimaryMap(BlockMapViewModel map) {
// update the primary map
if (primaryMap != map) {
if (primaryMap != null) {
primaryMap.NeighborsChanged -= PrimaryMapNeighborsChanged;
primaryMap.PropertyChanged -= PrimaryMapPropertyChanged;
primaryMap.CollisionHighlight = -1;
}
PrimaryMap = map;
if (primaryMap != null) {
primaryMap.NeighborsChanged += PrimaryMapNeighborsChanged;
primaryMap.PropertyChanged += PrimaryMapPropertyChanged;
primaryMap.CollisionHighlight = collisionIndex;
}
NotifyPropertyChanged(nameof(Blocks));
NotifyPropertyChanged(nameof(Name));
}
// update the neighbor maps
var oldMaps = VisibleMaps.ToList();
var newMaps = GetMapNeighbors(map, map.SpriteScale < .5 ? 2 : 1).ToList();
newMaps.Add(map);
var mapDict = new Dictionary();
newMaps.ForEach(m => mapDict[m.MapID] = m);
newMaps = mapDict.Values.ToList();
foreach (var oldM in oldMaps) if (!newMaps.Any(newM => oldM.MapID == newM.MapID)) VisibleMaps.Remove(oldM);
foreach (var newM in newMaps) {
bool match = false;
foreach (var existingM in VisibleMaps) {
if (existingM.MapID == newM.MapID) {
match = true;
existingM.IncludeBorders = newM.IncludeBorders;
existingM.SpriteScale = newM.SpriteScale;
existingM.LeftEdge = newM.LeftEdge;
existingM.TopEdge = newM.TopEdge;
break;
}
}
if (!match) VisibleMaps.Add(newM);
}
// refresh connection buttons
var newButtons = primaryMap.GetMapSliders().ToList();
for (int i = 0; i < MapButtons.Count && i < newButtons.Count; i++) {
if (!MapButtons[i].TryUpdate(newButtons[i])) MapButtons[i] = newButtons[i];
}
for (int i = MapButtons.Count; i < newButtons.Count; i++) MapButtons.Add(newButtons[i]);
while (MapButtons.Count > newButtons.Count) MapButtons.RemoveAt(MapButtons.Count - 1);
}
private void PrimaryMapNeighborsChanged(object? sender, EventArgs e) {
UpdatePrimaryMap(primaryMap);
}
private void PrimaryMapPropertyChanged(object sender, PropertyChangedEventArgs e) {
var map = (BlockMapViewModel)sender;
if (e.PropertyName == nameof(BlockMapViewModel.SelectedEvent)) {
if (map.SelectedEvent != selectedEvent) {
SelectedEvent = map.SelectedEvent;
}
}
}
private IEnumerable GetMapNeighbors(BlockMapViewModel map, int recursionLevel) {
if (recursionLevel < 1) yield break;
var directions = new List {
MapDirection.Up, MapDirection.Down, MapDirection.Left, MapDirection.Right
};
var newMaps = new List(directions.SelectMany(map.GetNeighbors));
foreach (var m in newMaps) {
yield return m;
if (recursionLevel > 1) {
foreach (var mm in GetMapNeighbors(m, recursionLevel - 1)) yield return mm;
}
}
}
#region Map Interaction
private double cursorX, cursorY, deltaX, deltaY;
public event EventHandler AutoscrollBlocks;
private double highlightCursorX, highlightCursorY, highlightCursorSize;
public double HighlightCursorX { get => highlightCursorX; set => Set(ref highlightCursorX, value); }
public double HighlightCursorY { get => highlightCursorY; set => Set(ref highlightCursorY, value); }
public double HighlightCursorSize { get => highlightCursorSize; set => Set(ref highlightCursorSize, value); }
public void Hover(double x, double y) {
var map = MapUnderCursor(x, y);
if (map == null) return;
var dx = (int)((x - map.LeftEdge) / 16 / map.SpriteScale) + .5;
var dy = (int)((y - map.TopEdge) / 16 / map.SpriteScale) + .5;
(HighlightCursorX, HighlightCursorY) = (map.LeftEdge + dx * 16 * map.SpriteScale, map.TopEdge + dy * 16 * map.SpriteScale);
HighlightCursorSize = 16 * map.SpriteScale + 4;
}
public void DragDown(double x, double y) {
(cursorX, cursorY) = (x, y);
(deltaX, deltaY) = (0, 0);
var map = MapUnderCursor(x, y);
if (map != null) UpdatePrimaryMap(map);
}
public void DragMove(double x, double y) {
deltaX += x - cursorX;
deltaY += y - cursorY;
var (intX, intY) = ((int)deltaX, (int)deltaY);
deltaX -= intX;
deltaY -= intY;
(cursorX, cursorY) = (x, y);
Pan(intX, intY);
Hover(x, y);
}
public void DragUp(double x, double y) { }
#region Primary Interaction (left-click)
private PrimaryInteractionType interactionType;
public void PrimaryDown(double x, double y, int clickCount) {
var map = MapUnderCursor(x, y);
if (map == null) {
interactionType = PrimaryInteractionType.None;
return;
}
var ev = map.EventUnderCursor(x, y);
if (ev != null) {
EventDown(x, y, ev, clickCount);
return;
} else {
primaryMap.DeselectEvent();
SelectedEvent = null;
}
DrawDown(x, y);
}
public void PrimaryMove(double x, double y) {
if (interactionType == PrimaryInteractionType.Draw) DrawMove(x, y);
if (interactionType == PrimaryInteractionType.Event) EventMove(x, y);
}
public void PrimaryUp(double x, double y) {
if (interactionType == PrimaryInteractionType.Draw) DrawUp(x, y);
if (interactionType == PrimaryInteractionType.Event) EventUp(x, y);
interactionType = PrimaryInteractionType.None;
}
private void DrawDown(double x, double y) {
interactionType = PrimaryInteractionType.Draw;
DrawMove(x, y);
}
private void DrawMove(double x, double y) {
var map = MapUnderCursor(x, y);
if (map != null) map.DrawBlock(history.CurrentChange, drawBlockIndex, collisionIndex, x, y);
Hover(x, y);
}
private void DrawUp(double x, double y) {
history.ChangeCompleted();
interactionType = PrimaryInteractionType.None;
}
private void EventDown(double x, double y, IEventModel ev, int clickCount) {
SelectedEvent = ev;
if (SelectedEvent is WarpEventModel warp && clickCount == 2) {
NavigateTo(warp.Bank, warp.Map);
} else {
interactionType = PrimaryInteractionType.Event;
DrawBlockIndex = -1;
CollisionIndex = -1;
}
}
private void EventMove(double x, double y) {
var map = MapUnderCursor(x, y);
if (map != null) map.UpdateEventLocation(selectedEvent, x, y);
Hover(x, y);
}
private void EventUp(double x, double y) => history.ChangeCompleted();
#endregion
public void SelectDown(double x, double y) {
var map = MapUnderCursor(x, y);
if (map == null) return;
var (blockIndex, collisionIndex) = map.GetBlock(x, y);
if (blockIndex >= 0) {
DrawBlockIndex = blockIndex;
AutoscrollBlocks.Raise(this);
}
if (collisionIndex >= 0) CollisionIndex = collisionIndex;
}
public void SelectMove(double x, double y) { }
public void SelectUp(double x, double y) {
interactionType = PrimaryInteractionType.None;
}
public void Zoom(double x, double y, bool enlarge) {
var map = MapUnderCursor(x, y);
if (map == null) map = primaryMap;
map.Scale(x, y, enlarge);
map.IncludeBorders = map.SpriteScale <= 1;
UpdatePrimaryMap(map);
}
public void SelectBlock(int x, int y) {
DrawBlockIndex = y * BlockMapViewModel.BlocksPerRow + x;
}
private StubCommand panCommand, zoomCommand, deleteCommand;
public ICommand PanCommand => StubCommand(ref panCommand, Pan);
public ICommand ZoomCommand => StubCommand(ref zoomCommand, Zoom);
public ICommand DeleteCommand => StubCommand(ref deleteCommand, Delete);
public void Pan(MapDirection direction) {
int intX = 0, intY = 0;
if (direction == MapDirection.Left) intX = -1;
if (direction == MapDirection.Right) intX = 1;
if (direction == MapDirection.Up) intY = -1;
if (direction == MapDirection.Down) intY = 1;
Pan(intX * -32, intY * -32);
var map = MapUnderCursor(0, 0);
if (map != null) UpdatePrimaryMap(map);
}
public void Zoom(ZoomDirection direction) => Zoom(0, 0, direction == ZoomDirection.Enlarge);
public void Delete() {
if (selectedEvent == null) return;
selectedEvent.Delete();
selectedEvent = null;
primaryMap.DeselectEvent();
primaryMap.RedrawEvents();
}
private void Pan(int intX, int intY) {
foreach (var map in VisibleMaps) {
map.LeftEdge += intX;
map.TopEdge += intY;
}
foreach (var button in MapButtons) {
button.AnchorPositionX += button.AnchorLeftEdge ? intX : -intX;
button.AnchorPositionY += button.AnchorTopEdge ? intY : -intY;
}
}
private BlockMapViewModel MapUnderCursor(double x, double y) {
foreach (var map in VisibleMaps) {
if (map.LeftEdge < x && x < map.LeftEdge + map.PixelWidth * map.SpriteScale) {
if (map.TopEdge < y && y < map.TopEdge + map.PixelHeight * map.SpriteScale) {
return map;
}
}
}
return null;
}
#endregion
#region ShiftInteraction
private MapSlider shiftButton;
public void ShiftDown(double x, double y) {
(cursorX, cursorY) = (x, y);
(deltaX, deltaY) = (0, 0);
shiftButton = ButtonUnderCursor(x, y);
HighlightCursorSize = 0;
}
public void ShiftMove(double x, double y) {
if (shiftButton == null) return;
deltaX += x - cursorX;
deltaY += y - cursorY;
var blockSize = (int)(16 * primaryMap.SpriteScale);
var (intX, intY) = ((int)deltaX / blockSize, (int)deltaY / blockSize);
deltaX -= intX * blockSize;
deltaY -= intY * blockSize;
(cursorX, cursorY) = (x, y);
shiftButton.Move(intX, intY);
}
public void ShiftUp(double x, double y) => history.ChangeCompleted();
private MapSlider ButtonUnderCursor(double x, double y) {
int left, right, top, bottom;
foreach (var button in MapButtons) {
if (button.AnchorLeftEdge) {
left = button.AnchorPositionX;
right = left + MapSlider.SliderSize;
} else {
right = -button.AnchorPositionX;
left = right - MapSlider.SliderSize;
}
if (button.AnchorTopEdge) {
top = button.AnchorPositionY;
bottom = top + MapSlider.SliderSize;
} else {
bottom = -button.AnchorPositionY;
top = bottom - MapSlider.SliderSize;
}
if (left <= x && x < right && top <= y && y < bottom) return button;
}
return null;
}
#endregion
}
public enum PrimaryInteractionType { None, Draw, Event }
}