0ad/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/TransformObject.cpp
Ralph Sennhauser fa9584fdc0
Fix some Atlas header includes
iwyu 0.26 now want's full declaration of TYPE in case of
std::vector<TYPE>, which is good as it allows the header to be
self standing.

There are some other changes suggested in part of the headers not being
updated when they should have been and some due to changes in iwyu
itself.

Ref: #8086
Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2026-06-12 21:22:36 +02:00

627 lines
16 KiB
C++

/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "tools/atlas/AtlasUI/General/AtlasWindowCommandProc.h"
#include "tools/atlas/AtlasUI/General/Observable.h"
#include "tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.h"
#include "tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/MiscState.h"
#include "tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.h"
#include "tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/Tools.h"
#include "tools/atlas/GameInterface/MessagePasser.h"
#include "tools/atlas/GameInterface/Messages.h"
#include "tools/atlas/GameInterface/Shareable.h"
#include "tools/atlas/GameInterface/SharedTypes.h"
#include <algorithm>
#include <cstddef>
#include <limits>
#include <string>
#include <vector>
#include <wx/chartype.h>
#include <wx/clipbrd.h>
#include <wx/dataobj.h>
#include <wx/debug.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/object.h>
#include <wx/sstream.h>
#include <wx/string.h>
#include <wx/utils.h>
#include <wx/xml/xml.h>
class wxInputStream;
using AtlasMessage::Position;
class TransformObject : public StateDrivenTool<TransformObject>
{
DECLARE_DYNAMIC_CLASS(TransformObject);
int m_dx, m_dy;
AtlasMessage::ObjectID m_lastSelected;
wxPoint m_startPoint;
Position m_entPosition;
// TODO: If we don't plan to change hotkeys, just replace with evt.ShiftDown(), etc.
static const wxKeyCode SELECTION_ADD_HOTKEY = WXK_SHIFT;
static const wxKeyCode SELECTION_REMOVE_HOTKEY = WXK_CONTROL; // COMMAND on Macs
static const wxKeyCode SELECTION_ACTORS_HOTKEY = WXK_ALT;
public:
TransformObject() : m_lastSelected(0)
{
SetState(&Waiting);
}
virtual void OnCommand(const wxString& command, void* WXUNUSED(userData))
{
if (command == _T("copy"))
OnCopy();
else if (command == _T("paste"))
OnPasteStart();
}
void OnDisable()
{
g_SelectedObjects.clear();
g_SelectedObjects.NotifyObservers();
POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects));
}
bool OnCopy() const
{
if (g_SelectedObjects.empty())
return false;
AtlasMessage::qGetObjectMapSettings info(g_SelectedObjects);
info.Post();
if (wxTheClipboard->Open())
{
wxString text(info.xmldata.c_str());
wxTheClipboard->SetData(new wxTextDataObject(text));
wxTheClipboard->Close();
}
return true;
}
void OnPasteStart()
{
wxString entities;
if (wxTheClipboard->Open())
{
if (wxTheClipboard->IsSupported(wxDF_TEXT))
{
wxTextDataObject data;
wxTheClipboard->GetData(data);
entities = data.GetText();
}
wxTheClipboard->Close();
}
// First do we need to check if it is a correct xml string
wxInputStream* is = new wxStringInputStream(entities);
wxXmlDocument doc;
if (!doc.Load(*is))
return;
// Entities, Entity(1.*)
wxXmlNode* root = doc.GetRoot();
if (root->GetName() != wxT("Entities"))
return;
// Template, position,orientation
const wxXmlNode* child = root->GetChildren();
while (child)
{
if (child->GetName() != wxT("Entity"))
return;
child = child->GetNext();
}
g_SelectedObjects.clear();
POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects));
// is the source code get here now you can add the objects to scene(preview)
// store id to move
child = root->GetChildren();
while (child)
{
wxString templateName;
Position entityPos;
long playerId = 0;
double orientation = 0;
unsigned int actorSeed = 0;
const wxXmlNode* xmlData = child->GetChildren();
while (xmlData)
{
if (xmlData->GetName() == wxT("Template"))
templateName = xmlData->GetNodeContent();
else if (xmlData->GetName() == wxT("Position"))
{
wxString x, z;
xmlData->GetAttribute(wxT("x"), &x);
xmlData->GetAttribute(wxT("z"), &z);
double aux, aux2;
x.ToDouble(&aux);
z.ToDouble(&aux2);
entityPos = Position(aux, 0, aux2);
}
else if (xmlData->GetName() == wxT("Orientation"))
{
wxString y;
xmlData->GetAttribute(wxT("y"), &y);
y.ToDouble(&orientation);
}
else if (xmlData->GetName() == wxT("Player"))
{
wxString x(xmlData->GetNodeContent());
x.ToLong(&playerId);
}
else if (xmlData->GetName() == wxT("ActorSeed"))
{
wxString x(xmlData->GetNodeContent());
unsigned long xTmp = 0;
x.ToULong(&xTmp);
wxASSERT(xTmp <= (unsigned long)std::numeric_limits<unsigned int>::max());
actorSeed = xTmp;
}
xmlData = xmlData->GetNext();
}
//Update current Ownership
this->GetScenarioEditor().GetObjectSettings().SetPlayerID(playerId);
this->GetScenarioEditor().GetObjectSettings().NotifyObservers();
POST_MESSAGE(ObjectPreview, ((std::wstring)templateName.c_str(), GetScenarioEditor().GetObjectSettings().GetSettings(), entityPos, false, Position(), orientation, actorSeed, false));
child = child->GetNext();
}
//Set state paste for preview the new objects
this->SetState(&Pasting);
//Update the objects to current mouse position
OnMovingPaste();
}
void OnMovingPaste()
{
//Move the preview(s) object(s)
POST_MESSAGE(MoveObjectPreview, ((m_entPosition)));
}
void OnPasteEnd(bool canceled)
{
if (canceled)
//delete previews objects
POST_MESSAGE(ObjectPreview, (_T(""), GetScenarioEditor().GetObjectSettings().GetSettings(), Position(), false, Position(), 0, 0, true));
else
{
ScenarioEditor::GetCommandProc().FinaliseLastCommand();
//Create new Objects and delete preview objects
POST_MESSAGE(ObjectPreviewToEntity, ());
AtlasMessage::qGetCurrentSelection currentSelection;
currentSelection.Post();
g_SelectedObjects = *currentSelection.ids;
}
//when all is done set default state
this->SetState(&Waiting);
}
// TODO: keys to rotate/move object?
struct sWaiting : public State
{
bool OnMouse(TransformObject* obj, wxMouseEvent& evt)
{
if (evt.LeftDClick() && AtlasMessage::ObjectIDIsValid(obj->m_lastSelected))
{
SET_STATE(SelectSimilar);
return true;
}
else if (evt.LeftDown())
{
bool selectionAdd = wxGetKeyState(SELECTION_ADD_HOTKEY);
bool selectionRemove = wxGetKeyState(SELECTION_REMOVE_HOTKEY);
bool selectionActors = wxGetKeyState(SELECTION_ACTORS_HOTKEY);
// New selection - never merge with movements of other objects
ScenarioEditor::GetCommandProc().FinaliseLastCommand();
// Select the object clicked on:
AtlasMessage::qPickObject qry(Position(evt.GetPosition()), selectionActors);
qry.Post();
// Check they actually clicked on a valid object
if (AtlasMessage::ObjectIDIsValid(qry.id))
{
std::vector<AtlasMessage::ObjectID>::iterator it = std::find(g_SelectedObjects.begin(), g_SelectedObjects.end(), qry.id);
bool objectIsSelected = (it != g_SelectedObjects.end());
if (selectionRemove)
{
// Remove from selection
if (objectIsSelected)
g_SelectedObjects.erase(it);
}
else if (!objectIsSelected)
{
// Add to selection
if (!selectionAdd)
g_SelectedObjects.clear();
g_SelectedObjects.push_back(qry.id);
}
obj->m_lastSelected = qry.id;
// If we're selecting the whole group
if (!selectionAdd && !selectionRemove && !g_SelectedObjects.empty())
{
// Remember the screen-space offset of the mouse from the
// object's centre, so we can add that back when moving it
// (instead of just moving the object's centre to directly
// beneath the mouse)
obj->m_dx = qry.offsetx;
obj->m_dy = qry.offsety;
SET_STATE(Dragging);
}
g_SelectedObjects.NotifyObservers();
POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects));
}
else
{
// Bandboxing
obj->m_lastSelected = 0;
obj->m_startPoint = evt.GetPosition();
SET_STATE(Bandboxing);
}
return true;
}
else if (!g_SelectedObjects.empty() && ((evt.Dragging() && evt.RightIsDown()) || evt.RightDown()))
{
SET_STATE(Rotating);
return true;
}
else if (evt.Moving())
{
//Save position for smooth paste position
obj->m_entPosition = Position(evt.GetPosition());
// Prevent certain events from reaching game UI in this mode
// to prevent selection ring confusion
return true;
}
else
return false;
}
bool OnKey(TransformObject* obj, wxKeyEvent& evt, KeyEventType type)
{
if (type == KEY_CHAR && evt.GetKeyCode() == WXK_DELETE)
{
POST_COMMAND(DeleteObjects, (g_SelectedObjects));
g_SelectedObjects.clear();
g_SelectedObjects.NotifyObservers();
POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects));
return true;
}
else if (type == KEY_CHAR && (evt.GetKeyCode() >= '0' && evt.GetKeyCode() <= '8'))
{
const int maxPlayerID = obj->GetScenarioEditor().GetMapSettings()["PlayerData"]["item"].count();
const int playerID = evt.GetKeyCode() - '0';
if (playerID <= maxPlayerID)
{
obj->GetScenarioEditor().GetObjectSettings().SetPlayerID(playerID);
obj->GetScenarioEditor().GetObjectSettings().NotifyObservers();
}
return true;
}
else if (evt.GetModifiers() == wxMOD_CONTROL)
{
if (evt.GetKeyCode() == 'C')
return obj->OnCopy();
else if (evt.GetKeyCode() == 'V')
{
obj->OnPasteStart();
return true;
}
}
return false;
}
}
Waiting;
struct sDragging : public State
{
bool OnMouse(TransformObject* obj, wxMouseEvent& evt)
{
if (evt.LeftUp())
{
POST_MESSAGE(ResetSelectionColor, ());
SET_STATE(Waiting);
return true;
}
else if (evt.Dragging())
{
Position pos(evt.GetPosition() + wxPoint(obj->m_dx, obj->m_dy));
POST_COMMAND(MoveObjects, (g_SelectedObjects, obj->m_lastSelected, pos));
return true;
}
else
return false;
}
bool OnKey(TransformObject* obj, wxKeyEvent& evt, KeyEventType type)
{
if (type == KEY_UP && evt.GetKeyCode() == WXK_ESCAPE)
{
// Cancel move action
ScenarioEditor::GetCommandProc().FinaliseLastCommand();
ScenarioEditor::GetCommandProc().Undo();
SET_STATE(Waiting);
return true;
}
else
return false;
}
}
Dragging;
struct sBandboxing : public State
{
bool OnMouse(TransformObject* obj, wxMouseEvent& evt)
{
if (evt.LeftIsDown() && evt.Dragging())
{
// Update bandbox overlay
POST_MESSAGE(SetBandbox, (true, obj->m_startPoint.x, obj->m_startPoint.y, evt.GetPosition().x, evt.GetPosition().y));
return true;
}
else if (evt.LeftUp())
{
bool selectionAdd = wxGetKeyState(SELECTION_ADD_HOTKEY);
bool selectionRemove = wxGetKeyState(SELECTION_REMOVE_HOTKEY);
bool selectionActors = wxGetKeyState(SELECTION_ACTORS_HOTKEY);
// Now we have both corners of the box, pick objects
AtlasMessage::qPickObjectsInRect qry(Position(obj->m_startPoint), Position(evt.GetPosition()), selectionActors);
qry.Post();
std::vector<AtlasMessage::ObjectID> ids = *qry.ids;
if (!selectionAdd && !selectionRemove)
{
// Just copy new selections (clears list if no selections)
g_SelectedObjects = ids;
}
else
{
for (size_t i = 0; i < ids.size(); ++i)
{
std::vector<AtlasMessage::ObjectID>::iterator it = std::find(g_SelectedObjects.begin(), g_SelectedObjects.end(), ids[i]);
bool objectIsSelected = (it != g_SelectedObjects.end());
if (selectionRemove)
{
// Remove from selection
if (objectIsSelected)
g_SelectedObjects.erase(it);
}
else if (!objectIsSelected)
{
// Add to selection
g_SelectedObjects.push_back(ids[i]);
}
}
}
POST_MESSAGE(SetBandbox, (false, 0, 0, 0, 0));
g_SelectedObjects.NotifyObservers();
POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects));
SET_STATE(Waiting);
return true;
}
else
return false;
}
bool OnKey(TransformObject* obj, wxKeyEvent& evt, KeyEventType type)
{
if (type == KEY_UP && evt.GetKeyCode() == WXK_ESCAPE)
{
// Clear bandbox and return to waiting state
POST_MESSAGE(SetBandbox, (false, 0, 0, 0, 0));
SET_STATE(Waiting);
return true;
}
else
return false;
}
}
Bandboxing;
struct sSelectSimilar : public State
{
bool OnMouse(TransformObject* obj, wxMouseEvent& evt)
{
if (evt.LeftUp())
{
bool selectionAdd = wxGetKeyState(SELECTION_ADD_HOTKEY);
bool selectionRemove = wxGetKeyState(SELECTION_REMOVE_HOTKEY);
// Select similar objects
AtlasMessage::qPickSimilarObjects qry(obj->m_lastSelected);
qry.Post();
std::vector<AtlasMessage::ObjectID> ids = *qry.ids;
if (!selectionAdd && !selectionRemove)
{
// Just copy new selections (clears list if no selections)
g_SelectedObjects = ids;
}
else
{
for (size_t i = 0; i < ids.size(); ++i)
{
std::vector<AtlasMessage::ObjectID>::iterator it = std::find(g_SelectedObjects.begin(), g_SelectedObjects.end(), ids[i]);
bool objectIsSelected = (it != g_SelectedObjects.end());
if (selectionRemove)
{
// Remove from selection
if (objectIsSelected)
g_SelectedObjects.erase(it);
}
else if (!objectIsSelected)
{
// Add to selection
g_SelectedObjects.push_back(ids[i]);
}
}
}
g_SelectedObjects.NotifyObservers();
POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects));
SET_STATE(Waiting);
return true;
}
else
return false;
}
}
SelectSimilar;
struct sPasting : public State
{
bool OnMouse(TransformObject* obj, wxMouseEvent& evt)
{
if (evt.Moving())
{
//Move the object
obj->m_entPosition = Position(evt.GetPosition());
obj->OnMovingPaste();
return true;
}
else if (evt.LeftDown())
{
//Place the object and update
obj->OnPasteEnd(false);
return true;
}
else
return false;
}
bool OnKey(TransformObject* obj, wxKeyEvent& evt, KeyEventType type)
{
if (type == KEY_CHAR && evt.GetKeyCode() == WXK_ESCAPE)
{
obj->OnPasteEnd(true);
return true;
}
else
return false;
}
}
Pasting;
struct sRotating : public State
{
bool fromCenterPoint;
void OnEnter(TransformObject* WXUNUSED(obj))
{
fromCenterPoint = true;
}
bool OnMouse(TransformObject* obj, wxMouseEvent& evt)
{
if (evt.RightUp())
{
POST_MESSAGE(ResetSelectionColor, ());
SET_STATE(Waiting);
return true;
}
else if (evt.Dragging())
{
bool fromGlobalAndIndividualCenterPoints = !evt.ControlDown() && !evt.ShiftDown();
bool newFromCenterPoint = evt.ShiftDown() || fromGlobalAndIndividualCenterPoints;
if (newFromCenterPoint != fromCenterPoint)
{
ScenarioEditor::GetCommandProc().FinaliseLastCommand();
fromCenterPoint = newFromCenterPoint;
}
Position pos(evt.GetPosition());
if (fromCenterPoint)
POST_COMMAND(RotateObjectsFromCenterPoint, (g_SelectedObjects, pos, fromGlobalAndIndividualCenterPoints));
else
POST_COMMAND(RotateObject, (g_SelectedObjects, pos));
return true;
}
return false;
}
bool OnKey(TransformObject* obj, wxKeyEvent& evt, KeyEventType type)
{
if (type == KEY_UP && evt.GetKeyCode() == WXK_ESCAPE)
{
// Cancel move action
ScenarioEditor::GetCommandProc().FinaliseLastCommand();
ScenarioEditor::GetCommandProc().Undo();
SET_STATE(Waiting);
return true;
}
return false;
}
}
Rotating;
};
IMPLEMENT_DYNAMIC_CLASS(TransformObject, StateDrivenTool<TransformObject>);