mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
NOTE: requires update-workspaces and may require correction of some modded actors/scenarios This was SVN commit r16438.
1081 lines
31 KiB
C++
1081 lines
31 KiB
C++
/* Copyright (C) 2015 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 "ScenarioEditor.h"
|
|
|
|
#include "wx/busyinfo.h"
|
|
#include "wx/clipbrd.h"
|
|
#include "wx/config.h"
|
|
#include "wx/dir.h"
|
|
#include "wx/evtloop.h"
|
|
#include "wx/ffile.h"
|
|
#include "wx/filename.h"
|
|
#include "wx/image.h"
|
|
#include "wx/sstream.h"
|
|
#include "wx/sysopt.h"
|
|
#include "wx/tooltip.h"
|
|
#include "wx/xml/xml.h"
|
|
|
|
#include "General/AtlasEventLoop.h"
|
|
#include "General/Datafile.h"
|
|
|
|
#include "CustomControls/Buttons/ToolButton.h"
|
|
#include "CustomControls/Canvas/Canvas.h"
|
|
#include "CustomControls/HighResTimer/HighResTimer.h"
|
|
#include "CustomControls/MapDialog/MapDialog.h"
|
|
|
|
#include "GameInterface/MessagePasser.h"
|
|
#include "GameInterface/Messages.h"
|
|
|
|
#include "Misc/KeyMap.h"
|
|
|
|
#include "Tools/Common/Tools.h"
|
|
#include "Tools/Common/Brushes.h"
|
|
#include "Tools/Common/MiscState.h"
|
|
|
|
static HighResTimer g_Timer;
|
|
|
|
using namespace AtlasMessage;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
// GL functions exported from DLL, and called by game (in a separate
|
|
// thread to the standard wx one)
|
|
ATLASDLLIMPEXP void Atlas_GLSetCurrent(void* canvas)
|
|
{
|
|
static_cast<Canvas*>(canvas)->SetCurrent();
|
|
}
|
|
|
|
ATLASDLLIMPEXP void Atlas_GLSwapBuffers(void* canvas)
|
|
{
|
|
static_cast<Canvas*>(canvas)->SwapBuffers();
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
class GameCanvas : public Canvas
|
|
{
|
|
public:
|
|
GameCanvas(ScenarioEditor& scenarioEditor, wxWindow* parent, int* attribList)
|
|
: Canvas(parent, attribList, wxWANTS_CHARS),
|
|
m_ScenarioEditor(scenarioEditor), m_MouseState(NONE), m_LastMouseState(NONE)
|
|
{
|
|
}
|
|
|
|
private:
|
|
|
|
bool KeyScroll(wxKeyEvent& evt, bool enable)
|
|
{
|
|
int dir;
|
|
switch (evt.GetKeyCode())
|
|
{
|
|
case 'A': case WXK_LEFT: dir = eScrollConstantDir::LEFT; break;
|
|
case 'D': case WXK_RIGHT: dir = eScrollConstantDir::RIGHT; break;
|
|
case 'W': case WXK_UP: dir = eScrollConstantDir::FORWARDS; break;
|
|
case 'S': case WXK_DOWN: dir = eScrollConstantDir::BACKWARDS; break;
|
|
case 'Q': case '[': dir = eScrollConstantDir::CLOCKWISE; break;
|
|
case 'E': case ']': dir = eScrollConstantDir::ANTICLOCKWISE; break;
|
|
case WXK_SHIFT: case WXK_CONTROL: dir = -1; break;
|
|
default: return false;
|
|
}
|
|
|
|
float speed = 120.f * ScenarioEditor::GetSpeedModifier();
|
|
|
|
if (dir == -1) // changed modifier keys - update all currently-scrolling directions
|
|
{
|
|
if (wxGetKeyState(WXK_LEFT)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::LEFT, speed));
|
|
if (wxGetKeyState(WXK_RIGHT)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::RIGHT, speed));
|
|
if (wxGetKeyState(WXK_UP)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::FORWARDS, speed));
|
|
if (wxGetKeyState(WXK_DOWN)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::BACKWARDS, speed));
|
|
if (wxGetKeyState((wxKeyCode)'[')) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::CLOCKWISE, speed));
|
|
if (wxGetKeyState((wxKeyCode)']')) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::ANTICLOCKWISE, speed));
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, dir, enable ? speed : 0.0f));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void OnKeyDown(wxKeyEvent& evt)
|
|
{
|
|
if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_DOWN))
|
|
{
|
|
// Key event has been handled by the tool, so don't try
|
|
// to use it for camera motion too
|
|
return;
|
|
}
|
|
|
|
if (KeyScroll(evt, true))
|
|
return;
|
|
|
|
// Slight hack: Only pass 'special' keys; normal keys will generate a translated Char event instead
|
|
if (evt.GetKeyCode() >= 256)
|
|
POST_MESSAGE(GuiKeyEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey(), true));
|
|
|
|
evt.Skip();
|
|
}
|
|
|
|
void OnKeyUp(wxKeyEvent& evt)
|
|
{
|
|
if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_UP))
|
|
return;
|
|
|
|
if (KeyScroll(evt, false))
|
|
return;
|
|
|
|
// Slight hack: Only pass 'special' keys; normal keys will generate a translated Char event instead
|
|
if (evt.GetKeyCode() >= 256)
|
|
POST_MESSAGE(GuiKeyEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey(), false));
|
|
|
|
evt.Skip();
|
|
}
|
|
|
|
void OnChar(wxKeyEvent& evt)
|
|
{
|
|
if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnKey(evt, ITool::KEY_CHAR))
|
|
return;
|
|
|
|
// Alt+enter toggles fullscreen
|
|
if (evt.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_ALT))
|
|
{
|
|
if (m_ScenarioEditor.IsFullScreen())
|
|
m_ScenarioEditor.ShowFullScreen(false);
|
|
else
|
|
m_ScenarioEditor.ShowFullScreen(true, wxFULLSCREEN_NOBORDER | wxFULLSCREEN_NOCAPTION);
|
|
return;
|
|
}
|
|
|
|
if (evt.GetKeyCode() == 'c')
|
|
{
|
|
POST_MESSAGE(CameraReset, ());
|
|
return;
|
|
}
|
|
|
|
int dir = 0;
|
|
if (evt.GetKeyCode() == '-' || evt.GetKeyCode() == '_')
|
|
dir = -1;
|
|
else if (evt.GetKeyCode() == '+' || evt.GetKeyCode() == '=')
|
|
dir = +1;
|
|
// TODO: internationalisation (-/_ and +/= don't always share a key)
|
|
|
|
if (dir)
|
|
{
|
|
float speed = 16.f * ScenarioEditor::GetSpeedModifier();
|
|
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, speed*dir));
|
|
}
|
|
else
|
|
{
|
|
// Slight hack: Only pass 'normal' keys; special keys will generate a KeyDown/KeyUp event instead
|
|
if (evt.GetKeyCode() < 256)
|
|
POST_MESSAGE(GuiCharEvent, (GetSDLKeyFromWxKeyCode(evt.GetKeyCode()), evt.GetUnicodeKey()));
|
|
|
|
evt.Skip();
|
|
}
|
|
}
|
|
|
|
void OnKillFocus(wxFocusEvent& evt)
|
|
{
|
|
// Stop any scrolling, since otherwise we'll carry on forever if
|
|
// we lose focus and the KeyUp events go to a different window
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::LEFT, 0.0f));
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::RIGHT, 0.0f));
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::FORWARDS, 0.0f));
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::BACKWARDS, 0.0f));
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::CLOCKWISE, 0.0f));
|
|
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::ANTICLOCKWISE, 0.0f));
|
|
|
|
evt.Skip();
|
|
}
|
|
|
|
virtual void HandleMouseEvent(wxMouseEvent& evt)
|
|
{
|
|
|
|
// TODO or at least to think about: When using other controls in the
|
|
// editor, it's annoying that keyboard/scrollwheel no longer navigate
|
|
// around the world until you click on it.
|
|
// Setting focus back whenever the mouse moves over the GL window
|
|
// feels like a fairly natural solution to me, since I can use
|
|
// e.g. brush-editing controls normally, and then move the mouse to
|
|
// see the brush outline and magically get given back full control
|
|
// of the camera.
|
|
if (evt.Moving())
|
|
SetFocus();
|
|
|
|
if (m_ScenarioEditor.GetToolManager().GetCurrentTool()->OnMouse(evt))
|
|
{
|
|
// Mouse event has been handled by the tool, so don't try
|
|
// to use it for camera motion too
|
|
return;
|
|
}
|
|
|
|
// Global mouse event handlers (for camera motion)
|
|
|
|
if (evt.GetWheelRotation())
|
|
{
|
|
float speed = 16.f * ScenarioEditor::GetSpeedModifier();
|
|
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, evt.GetWheelRotation() * speed / evt.GetWheelDelta()));
|
|
}
|
|
else
|
|
{
|
|
if (evt.MiddleIsDown())
|
|
{
|
|
if (wxGetKeyState(WXK_CONTROL) || evt.RightIsDown())
|
|
m_MouseState = ROTATEAROUND;
|
|
else
|
|
m_MouseState = SCROLL;
|
|
}
|
|
else
|
|
m_MouseState = NONE;
|
|
|
|
if (m_MouseState != m_LastMouseState)
|
|
{
|
|
switch (m_MouseState)
|
|
{
|
|
case NONE: break;
|
|
case SCROLL: POST_MESSAGE(Scroll, (eRenderView::GAME, eScrollType::FROM, evt.GetPosition())); break;
|
|
case ROTATEAROUND: POST_MESSAGE(RotateAround, (eRenderView::GAME, eRotateAroundType::FROM, evt.GetPosition())); break;
|
|
default: wxFAIL;
|
|
}
|
|
m_LastMouseState = m_MouseState;
|
|
}
|
|
else if (evt.Dragging())
|
|
{
|
|
switch (m_MouseState)
|
|
{
|
|
case NONE: break;
|
|
case SCROLL: POST_MESSAGE(Scroll, (eRenderView::GAME, eScrollType::TO, evt.GetPosition())); break;
|
|
case ROTATEAROUND: POST_MESSAGE(RotateAround, (eRenderView::GAME, eRotateAroundType::TO, evt.GetPosition())); break;
|
|
default: wxFAIL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Button down and double click appear to be mutually exclusive events,
|
|
// meaning a second button down event is not sent before a double click
|
|
if (evt.ButtonDown() || evt.ButtonDClick())
|
|
POST_MESSAGE(GuiMouseButtonEvent, (evt.GetButton(), true, evt.GetPosition()));
|
|
else if (evt.ButtonUp())
|
|
POST_MESSAGE(GuiMouseButtonEvent, (evt.GetButton(), false, evt.GetPosition()));
|
|
else if (evt.GetEventType() == wxEVT_MOTION)
|
|
POST_MESSAGE(GuiMouseMotionEvent, (evt.GetPosition()));
|
|
}
|
|
|
|
enum { NONE, SCROLL, ROTATEAROUND };
|
|
int m_MouseState, m_LastMouseState;
|
|
|
|
ScenarioEditor& m_ScenarioEditor;
|
|
|
|
DECLARE_EVENT_TABLE();
|
|
};
|
|
|
|
BEGIN_EVENT_TABLE(GameCanvas, Canvas)
|
|
EVT_KEY_DOWN(GameCanvas::OnKeyDown)
|
|
EVT_KEY_UP(GameCanvas::OnKeyUp)
|
|
EVT_CHAR(GameCanvas::OnChar)
|
|
EVT_KILL_FOCUS(GameCanvas::OnKillFocus)
|
|
END_EVENT_TABLE()
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
volatile bool g_FrameHasEnded;
|
|
// Called from game thread
|
|
ATLASDLLIMPEXP void Atlas_NotifyEndOfFrame()
|
|
{
|
|
g_FrameHasEnded = true;
|
|
}
|
|
|
|
enum
|
|
{
|
|
ID_Quit = 1,
|
|
|
|
ID_New,
|
|
ID_Open,
|
|
ID_Save,
|
|
ID_SaveAs,
|
|
ID_ImportHeightmap,
|
|
|
|
ID_Copy,
|
|
ID_Paste,
|
|
|
|
ID_Wireframe,
|
|
ID_MessageTrace,
|
|
ID_Screenshot,
|
|
ID_BigScreenshot,
|
|
ID_JavaScript,
|
|
ID_CameraReset,
|
|
ID_RenderPathFixed,
|
|
ID_RenderPathShader,
|
|
ID_DumpState,
|
|
ID_DumpBinaryState,
|
|
|
|
ID_Toolbar // must be last in the list
|
|
};
|
|
|
|
BEGIN_EVENT_TABLE(ScenarioEditor, wxFrame)
|
|
EVT_CLOSE(ScenarioEditor::OnClose)
|
|
EVT_TIMER(wxID_ANY, ScenarioEditor::OnTimer)
|
|
|
|
EVT_MENU(ID_New, ScenarioEditor::OnNew)
|
|
EVT_MENU(ID_Open, ScenarioEditor::OnOpen)
|
|
EVT_MENU(ID_Save, ScenarioEditor::OnSave)
|
|
EVT_MENU(ID_SaveAs, ScenarioEditor::OnSaveAs)
|
|
EVT_MENU(ID_ImportHeightmap, ScenarioEditor::OnImportHeightmap)
|
|
EVT_MENU_RANGE(wxID_FILE1, wxID_FILE9, ScenarioEditor::OnMRUFile)
|
|
|
|
EVT_MENU(ID_Quit, ScenarioEditor::OnQuit)
|
|
EVT_MENU(wxID_UNDO, ScenarioEditor::OnUndo)
|
|
EVT_MENU(wxID_REDO, ScenarioEditor::OnRedo)
|
|
EVT_MENU(ID_Copy, ScenarioEditor::OnCopy)
|
|
EVT_MENU(ID_Paste, ScenarioEditor::OnPaste)
|
|
|
|
EVT_MENU(ID_Wireframe, ScenarioEditor::OnWireframe)
|
|
EVT_MENU(ID_MessageTrace, ScenarioEditor::OnMessageTrace)
|
|
EVT_MENU(ID_Screenshot, ScenarioEditor::OnScreenshot)
|
|
EVT_MENU(ID_BigScreenshot, ScenarioEditor::OnScreenshot)
|
|
EVT_MENU(ID_JavaScript, ScenarioEditor::OnJavaScript)
|
|
EVT_MENU(ID_CameraReset, ScenarioEditor::OnCameraReset)
|
|
EVT_MENU(ID_DumpState, ScenarioEditor::OnDumpState)
|
|
EVT_MENU(ID_DumpBinaryState, ScenarioEditor::OnDumpState)
|
|
EVT_MENU(ID_RenderPathFixed, ScenarioEditor::OnRenderPath)
|
|
EVT_MENU(ID_RenderPathShader, ScenarioEditor::OnRenderPath)
|
|
|
|
EVT_MENU_OPEN(ScenarioEditor::OnMenuOpen)
|
|
|
|
EVT_IDLE(ScenarioEditor::OnIdle)
|
|
END_EVENT_TABLE()
|
|
|
|
static AtlasWindowCommandProc g_CommandProc;
|
|
AtlasWindowCommandProc& ScenarioEditor::GetCommandProc() { return g_CommandProc; }
|
|
|
|
ScenarioEditor::ScenarioEditor(wxWindow* parent)
|
|
: wxFrame(parent, wxID_ANY, _T(""), wxDefaultPosition, wxSize(1024, 768))
|
|
, m_FileHistory(_T("Scenario Editor"))
|
|
, m_ObjectSettings(g_SelectedObjects, AtlasMessage::eRenderView::GAME)
|
|
, m_ToolManager(this)
|
|
{
|
|
// Global application initialisation:
|
|
|
|
wxImage::AddHandler(new wxICOHandler);
|
|
|
|
/* "osx.openfiledialog.always-show-types: Per default a wxFileDialog with wxFD_OPEN
|
|
does not show a types-popup on OSX but allows the selection of files from any of
|
|
the supported types. Setting this to 1 shows a wxChoice for selection (if there
|
|
is more than one supported filetype)." */
|
|
wxSystemOptions::SetOption(_T("osx.openfiledialog.always-show-types"), 1); // has global effect
|
|
|
|
// wxLog::SetTraceMask(wxTraceMessages);
|
|
|
|
g_SelectedTexture = _T("grass1_spring");
|
|
g_SelectedTexture.NotifyObservers();
|
|
|
|
SetOpenFilename(_T(""));
|
|
|
|
#if defined(__WXMSW__)
|
|
m_Icon = wxIcon(_T("ICON_ScenarioEditor")); // load from atlas.rc
|
|
#else
|
|
{
|
|
const wxString relativePath (_T("tools/atlas/icons/ScenarioEditor.ico"));
|
|
wxFileName filename (relativePath, wxPATH_UNIX);
|
|
filename.MakeAbsolute(Datafile::GetDataDirectory());
|
|
m_Icon = wxIcon(filename.GetFullPath(), wxBITMAP_TYPE_ICO);
|
|
}
|
|
#endif
|
|
SetIcon(m_Icon);
|
|
|
|
wxToolTip::Enable(true);
|
|
|
|
wxImage::AddHandler(new wxPNGHandler);
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Do some early game initialisation:
|
|
// (This must happen before constructing the GL canvas.)
|
|
|
|
POST_MESSAGE(Init, ());
|
|
|
|
// Wait for it to finish running Init
|
|
qPing qry;
|
|
qry.Post();
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Menu
|
|
|
|
wxMenuBar* menuBar = new wxMenuBar;
|
|
SetMenuBar(menuBar);
|
|
|
|
wxMenu *menuFile = new wxMenu;
|
|
menuBar->Append(menuFile, _("&File"));
|
|
{
|
|
menuFile->Append(ID_New, _("&New..."));
|
|
menuFile->Append(ID_Open, _("&Open..."));
|
|
menuFile->Append(ID_Save, _("&Save"));
|
|
menuFile->Append(ID_SaveAs, _("Save &As..."));
|
|
menuFile->AppendSeparator();//-----------
|
|
menuFile->Append(ID_ImportHeightmap, _("&Import Heightmap..."));
|
|
menuFile->AppendSeparator();//-----------
|
|
menuFile->Append(ID_Quit, _("E&xit"));
|
|
m_FileHistory.UseMenu(menuFile);//-------
|
|
m_FileHistory.AddFilesToMenu();
|
|
}
|
|
|
|
// m_menuItem_Save = menuFile->FindItem(ID_Save); // remember this item, to let it be greyed out
|
|
// wxASSERT(m_menuItem_Save);
|
|
|
|
wxMenu *menuEdit = new wxMenu;
|
|
menuBar->Append(menuEdit, _("&Edit"));
|
|
{
|
|
menuEdit->Append(wxID_UNDO, _("&Undo"));
|
|
menuEdit->Append(wxID_REDO, _("&Redo"));
|
|
menuEdit->AppendSeparator();
|
|
menuEdit->Append(ID_Copy, _("&Copy"));
|
|
menuEdit->Enable(ID_Copy, false);
|
|
menuEdit->Append(ID_Paste, _("&Paste"));
|
|
menuEdit->Enable(ID_Paste, false);
|
|
}
|
|
|
|
g_SelectedObjects.RegisterObserver(0, &ScenarioEditor::OnSelectedObjectsChange, this);
|
|
|
|
GetCommandProc().SetEditMenu(menuEdit);
|
|
GetCommandProc().Initialize();
|
|
|
|
|
|
wxMenu *menuMisc = new wxMenu;
|
|
menuBar->Append(menuMisc, _("&Misc hacks"));
|
|
{
|
|
menuMisc->AppendCheckItem(ID_Wireframe, _("&Wireframe"));
|
|
menuMisc->AppendCheckItem(ID_MessageTrace, _("Message debug trace"));
|
|
menuMisc->Append(ID_Screenshot, _("&Screenshot"));
|
|
menuMisc->Append(ID_BigScreenshot, _("Big screenshot"));
|
|
menuMisc->Append(ID_JavaScript, _("&JS console"));
|
|
menuMisc->Append(ID_CameraReset, _("&Reset camera"));
|
|
|
|
wxMenu *menuSS = new wxMenu;
|
|
menuMisc->AppendSubMenu(menuSS, _("Si&mulation state"));
|
|
menuSS->Append(ID_DumpState, _("&Dump to disk"));
|
|
menuSS->Append(ID_DumpBinaryState, _("Dump &binary to disk"));
|
|
|
|
wxMenu *menuRP = new wxMenu;
|
|
menuMisc->AppendSubMenu(menuRP, _("Render &path"));
|
|
menuRP->Append(ID_RenderPathFixed, _("&Fixed function"));
|
|
menuRP->Append(ID_RenderPathShader, _("&Shader (default)"));
|
|
}
|
|
|
|
m_FileHistory.LoadFromSubDir(*wxConfigBase::Get());
|
|
|
|
|
|
m_SectionLayout.SetWindow(this);
|
|
|
|
// Toolbar:
|
|
// wxOSX/Cocoa 2.9 doesn't seem to like SetToolBar, so we use CreateToolBar which implicitly associates
|
|
// the toolbar with the frame, and use OnCreateToolBar to construct our custom toolbar
|
|
// (this should be equivalent behavior on all platforms)
|
|
CreateToolBar(wxNO_BORDER|wxTB_FLAT|wxTB_HORIZONTAL, ID_Toolbar)->Realize();
|
|
|
|
// Set the default tool to be selected
|
|
m_ToolManager.SetCurrentTool(_T(""));
|
|
|
|
|
|
// Set up GL canvas:
|
|
|
|
int glAttribList[] = {
|
|
WX_GL_RGBA,
|
|
WX_GL_DOUBLEBUFFER,
|
|
WX_GL_DEPTH_SIZE, 24, // TODO: wx documentation doesn't say 24 is valid
|
|
WX_GL_STENCIL_SIZE, 8,
|
|
WX_GL_BUFFER_SIZE, 24, // color bits
|
|
WX_GL_MIN_ALPHA, 8, // alpha bits
|
|
0
|
|
};
|
|
Canvas* canvas = new GameCanvas(*this, m_SectionLayout.GetCanvasParent(), glAttribList);
|
|
m_SectionLayout.SetCanvas(canvas);
|
|
|
|
// Set up sidebars:
|
|
|
|
m_SectionLayout.Build(*this);
|
|
|
|
#if defined(__WXMSW__)
|
|
// The canvas' context gets made current on creation; but it can only be
|
|
// current for one thread at a time, and it needs to be current for the
|
|
// thread that is doing the draw calls, so disable it for this one.
|
|
wglMakeCurrent(NULL, NULL);
|
|
#elif defined(__WXGTK__) || defined(__WXOSX__) || defined(__WXMAC__)
|
|
// Need to make sure the canvas is realised, so that its context is valid
|
|
// this solves the "invalid drawable" error
|
|
Show(true);
|
|
Raise();
|
|
#endif
|
|
#ifdef __WXGTK__
|
|
// TODO: wxSafeYield causes issues on wxOSX 2.9, is it necessary?
|
|
wxSafeYield();
|
|
#endif
|
|
|
|
// Send setup messages to game engine:
|
|
|
|
POST_MESSAGE(InitSDL, ());
|
|
|
|
POST_MESSAGE(SetCanvas, (static_cast<wxGLCanvas*>(canvas),
|
|
canvas->GetClientSize().GetWidth(), canvas->GetClientSize().GetHeight()));
|
|
|
|
POST_MESSAGE(InitGraphics, ());
|
|
|
|
canvas->InitSize();
|
|
|
|
// Start with a blank map (so that the editor can assume there's always
|
|
// a valid map loaded)
|
|
POST_MESSAGE(LoadMap, (_T("maps/scenarios/_default.xml")));
|
|
POST_MESSAGE(SimPlay, (0.f, false));
|
|
|
|
// Select the initial sidebar (after the map has loaded)
|
|
m_SectionLayout.SelectPage(_T("MapSidebar"));
|
|
|
|
// Wait for blank map
|
|
qry.Post();
|
|
|
|
POST_MESSAGE(RenderEnable, (eRenderView::GAME));
|
|
|
|
// Set up a timer to make sure tool-updates happen frequently (in addition
|
|
// to the idle handler (which makes them happen more frequently if there's nothing
|
|
// else to do))
|
|
m_Timer.SetOwner(this);
|
|
m_Timer.Start(20);
|
|
}
|
|
|
|
wxToolBar* ScenarioEditor::OnCreateToolBar(long style, wxWindowID id, const wxString& WXUNUSED(name))
|
|
{
|
|
ToolButtonBar* toolbar = new ToolButtonBar(m_ToolManager, this, &m_SectionLayout, id, style);
|
|
// TODO: configurable small vs large icon images
|
|
|
|
// (button label; tooltip text; image; internal tool name; section to switch to)
|
|
toolbar->AddToolButton(_("Default"), _("Default"), _T("default.png"), _T(""), _T(""));
|
|
toolbar->AddToolButton(_("Move"), _("Move/rotate object"), _T("moveobject.png"), _T("TransformObject"), _T("")/*_T("ObjectSidebar")*/);
|
|
toolbar->AddToolButton(_("Elevation"), _("Alter terrain elevation"), _T("alterelevation.png"), _T("AlterElevation"), _T("")/*_T("TerrainSidebar")*/);
|
|
toolbar->AddToolButton(_("Smooth"), _("Smooth terrain elevation"), _T("smoothelevation.png"), _T("SmoothElevation"), _T("")/*_T("TerrainSidebar")*/);
|
|
toolbar->AddToolButton(_("Flatten"), _("Flatten terrain elevation"), _T("flattenelevation.png"), _T("FlattenElevation"), _T("")/*_T("TerrainSidebar")*/);
|
|
toolbar->AddToolButton(_("Paint Terrain"), _("Paint terrain texture"), _T("paintterrain.png"), _T("PaintTerrain"), _T("")/*_T("TerrainSidebar")*/);
|
|
|
|
return toolbar;
|
|
}
|
|
|
|
float ScenarioEditor::GetSpeedModifier()
|
|
{
|
|
if (wxGetKeyState(WXK_SHIFT) && wxGetKeyState(WXK_CONTROL))
|
|
return 1.f/64.f;
|
|
else if (wxGetKeyState(WXK_CONTROL))
|
|
return 1.f/4.f;
|
|
else if (wxGetKeyState(WXK_SHIFT))
|
|
return 4.f;
|
|
else
|
|
return 1.f;
|
|
}
|
|
|
|
void ScenarioEditor::OnClose(wxCloseEvent& event)
|
|
{
|
|
if (event.CanVeto() && GetCommandProc().IsDirty())
|
|
{
|
|
if (wxMessageBox(_T("You have unsaved changes. Are you sure you want to quit?"), _T("Discard unsaved changes?"), wxICON_QUESTION | wxYES_NO) != wxYES)
|
|
{
|
|
event.Veto();
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_ToolManager.SetCurrentTool(_T(""));
|
|
|
|
m_FileHistory.SaveToSubDir(*wxConfigBase::Get());
|
|
|
|
POST_MESSAGE(Shutdown, ());
|
|
|
|
qExit().Post();
|
|
// blocks until engine has noticed the message, so we won't be
|
|
// destroying the GLCanvas while it's still rendering
|
|
|
|
Destroy();
|
|
}
|
|
|
|
|
|
static void UpdateTool(ToolManager& toolManager)
|
|
{
|
|
// Don't keep posting events if the game can't keep up
|
|
if (g_FrameHasEnded)
|
|
{
|
|
g_FrameHasEnded = false; // (thread safety doesn't matter here)
|
|
// TODO: Smoother timing stuff?
|
|
static double last = g_Timer.GetTime();
|
|
double time = g_Timer.GetTime();
|
|
toolManager.GetCurrentTool()->OnTick(time-last);
|
|
last = time;
|
|
}
|
|
}
|
|
void ScenarioEditor::OnTimer(wxTimerEvent&)
|
|
{
|
|
UpdateTool(m_ToolManager);
|
|
}
|
|
void ScenarioEditor::OnIdle(wxIdleEvent&)
|
|
{
|
|
UpdateTool(m_ToolManager);
|
|
}
|
|
|
|
void ScenarioEditor::OnQuit(wxCommandEvent&)
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void ScenarioEditor::OnUndo(wxCommandEvent&)
|
|
{
|
|
GetCommandProc().Undo();
|
|
}
|
|
|
|
void ScenarioEditor::OnRedo(wxCommandEvent&)
|
|
{
|
|
GetCommandProc().Redo();
|
|
}
|
|
|
|
void ScenarioEditor::OnCopy(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
if (GetToolManager().GetCurrentToolName() == _T("TransformObject"))
|
|
GetToolManager().GetCurrentTool()->OnCommand(_T("copy"), NULL);
|
|
}
|
|
|
|
void ScenarioEditor::OnPaste(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
if (GetToolManager().GetCurrentToolName() != _T("TransformObject"))
|
|
GetToolManager().SetCurrentTool(_T("TransformObject"), NULL);
|
|
|
|
GetToolManager().GetCurrentTool()->OnCommand(_T("paste"), NULL);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
void ScenarioEditor::OnNew(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
if (wxMessageBox(_("Discard current map and start blank new map?"), _("New map"), wxOK|wxCANCEL|wxICON_QUESTION, this) == wxOK)
|
|
OpenFile(_T(""), _T("maps/scenarios/_default.xml"));
|
|
}
|
|
|
|
bool ScenarioEditor::OpenFile(const wxString& name, const wxString& filename)
|
|
{
|
|
wxBusyInfo busy(_("Loading ") + name);
|
|
wxBusyCursor busyc;
|
|
|
|
AtlasMessage::qVFSFileExists qry(filename.wc_str());
|
|
qry.Post();
|
|
if (!qry.exists)
|
|
return false;
|
|
|
|
// Deactivate tools, so they don't carry forwards into the new CWorld
|
|
// and crash.
|
|
m_ToolManager.SetCurrentTool(_T(""));
|
|
// TODO: clear the undo buffer, etc
|
|
|
|
std::wstring map(filename.wc_str());
|
|
POST_MESSAGE(LoadMap, (map));
|
|
|
|
SetOpenFilename(name);
|
|
|
|
{ // Wait for it to load, while the wxBusyInfo is telling the user that we're doing that
|
|
qPing qry;
|
|
qry.Post();
|
|
}
|
|
|
|
NotifyOnMapReload();
|
|
|
|
GetCommandProc().ClearCommands();
|
|
|
|
return true;
|
|
// TODO: Make this a non-undoable command
|
|
}
|
|
|
|
// TODO (eventually): replace all this file-handling stuff with the Workspace Editor
|
|
|
|
void ScenarioEditor::OnOpen(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
if (DiscardChangesDialog())
|
|
return;
|
|
|
|
MapDialog dlg (NULL, MAPDIALOG_OPEN, m_Icon);
|
|
if (dlg.ShowModal() == wxID_OK)
|
|
{
|
|
wxString filename = dlg.GetFilename();
|
|
if (!OpenFile(filename, filename))
|
|
wxLogError(_("Map '%ls' does not exist"), filename.c_str());
|
|
}
|
|
|
|
// TODO: Make this a non-undoable command
|
|
}
|
|
|
|
void ScenarioEditor::OnImportHeightmap(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
if (DiscardChangesDialog())
|
|
return;
|
|
|
|
wxFileDialog dlg (NULL, wxFileSelectorPromptStr,
|
|
_T(""), _T(""),
|
|
_T("Valid Image files|*.png;*.jpg;*.bmp|All files (*.*)|*.*"),
|
|
wxFD_OPEN);
|
|
// Set default filter
|
|
dlg.SetFilterIndex(0);
|
|
|
|
if (dlg.ShowModal() != wxID_OK)
|
|
return;
|
|
|
|
OpenFile(_T(""), _T("maps/scenarios/_default.xml"));
|
|
|
|
std::wstring image(dlg.GetPath().wc_str());
|
|
POST_MESSAGE(ImportHeightmap, (image));
|
|
|
|
// TODO: Make this a non-undoable command
|
|
}
|
|
|
|
void ScenarioEditor::OnMRUFile(wxCommandEvent& event)
|
|
{
|
|
wxString filename(m_FileHistory.GetHistoryFile(event.GetId() - wxID_FILE1));
|
|
|
|
// Handle old MRU filenames
|
|
if (filename.Mid(0, 5) != _T("maps/"))
|
|
{
|
|
filename = L"maps/scenarios/" + filename;
|
|
m_FileHistory.RemoveFileFromHistory(event.GetId() - wxID_FILE1);
|
|
}
|
|
|
|
if (DiscardChangesDialog())
|
|
return;
|
|
|
|
if (!OpenFile(filename, filename))
|
|
{
|
|
// Missing or invalid - warn and remove from MRU
|
|
wxLogError(_("Map '%ls' does not exist"), filename.c_str());
|
|
m_FileHistory.RemoveFileFromHistory(event.GetId() - wxID_FILE1);
|
|
}
|
|
}
|
|
|
|
void ScenarioEditor::OnSave(wxCommandEvent& event)
|
|
{
|
|
if (m_OpenFilename.IsEmpty())
|
|
{
|
|
OnSaveAs(event);
|
|
}
|
|
else
|
|
{
|
|
wxBusyInfo busy(_("Saving ") + m_OpenFilename);
|
|
wxBusyCursor busyc;
|
|
|
|
// Deactivate tools, so things like unit previews don't get saved.
|
|
// (TODO: Would be nicer to leave the tools active, and just not save
|
|
// the preview units.)
|
|
m_ToolManager.SetCurrentTool(_T(""));
|
|
|
|
std::wstring map(m_OpenFilename.wc_str());
|
|
POST_MESSAGE(SaveMap, (map));
|
|
|
|
// Wait for it to finish saving
|
|
qPing qry;
|
|
qry.Post();
|
|
|
|
GetCommandProc().MarkAsSaved();
|
|
}
|
|
}
|
|
|
|
void ScenarioEditor::OnSaveAs(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
MapDialog dlg(NULL, MAPDIALOG_SAVE, m_Icon);
|
|
if (dlg.ShowModal() == wxID_OK)
|
|
{
|
|
wxString filename(dlg.GetFilename());
|
|
wxBusyInfo busy(_("Saving ") + filename);
|
|
wxBusyCursor busyc;
|
|
|
|
m_ToolManager.SetCurrentTool(_T(""));
|
|
|
|
std::wstring map(filename.wc_str());
|
|
POST_MESSAGE(SaveMap, (map));
|
|
|
|
SetOpenFilename(filename);
|
|
|
|
// Wait for it to finish saving
|
|
qPing qry;
|
|
qry.Post();
|
|
|
|
GetCommandProc().MarkAsSaved();
|
|
}
|
|
}
|
|
|
|
void ScenarioEditor::SetOpenFilename(const wxString& filename)
|
|
{
|
|
SetTitle(wxString::Format(_("Atlas - Scenario Editor - %s"),
|
|
(filename.IsEmpty() ? wxString(_("(untitled)")) : filename).c_str()));
|
|
|
|
m_OpenFilename = filename;
|
|
|
|
if (! filename.IsEmpty())
|
|
m_FileHistory.AddFileToHistory(filename);
|
|
}
|
|
|
|
void ScenarioEditor::NotifyOnMapReload()
|
|
{
|
|
m_SectionLayout.OnMapReload();
|
|
|
|
// Notify observers, here so it's independent of individual panels
|
|
m_MapSettings.NotifyObservers();
|
|
}
|
|
|
|
bool ScenarioEditor::DiscardChangesDialog()
|
|
{
|
|
return GetCommandProc().IsDirty() &&
|
|
wxMessageBox(_T("You have unsaved changes. Are you sure you want to open another map?"), _T("Discard unsaved changes?"), wxICON_QUESTION | wxYES_NO) != wxYES;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
void ScenarioEditor::OnWireframe(wxCommandEvent& event)
|
|
{
|
|
POST_MESSAGE(RenderStyle, (event.IsChecked()));
|
|
}
|
|
|
|
void ScenarioEditor::OnMessageTrace(wxCommandEvent& event)
|
|
{
|
|
POST_MESSAGE(MessageTrace, (event.IsChecked()));
|
|
}
|
|
|
|
void ScenarioEditor::OnScreenshot(wxCommandEvent& event)
|
|
{
|
|
switch (event.GetId())
|
|
{
|
|
case ID_BigScreenshot:
|
|
POST_MESSAGE(Screenshot, (true, 10));
|
|
break;
|
|
case ID_Screenshot:
|
|
POST_MESSAGE(Screenshot, (false, 0));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ScenarioEditor::OnJavaScript(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
wxString cmd = ::wxGetTextFromUser(_T(""), _("JS command"), _T(""), this);
|
|
if (cmd.IsEmpty())
|
|
return;
|
|
POST_MESSAGE(JavaScript, ((std::wstring)cmd.wc_str()));
|
|
}
|
|
|
|
void ScenarioEditor::OnCameraReset(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
POST_MESSAGE(CameraReset, ());
|
|
}
|
|
|
|
void ScenarioEditor::OnRenderPath(wxCommandEvent& event)
|
|
{
|
|
switch (event.GetId())
|
|
{
|
|
case ID_RenderPathFixed:
|
|
POST_MESSAGE(SetViewParamS, (eRenderView::GAME, L"renderpath", L"fixed"));
|
|
break;
|
|
case ID_RenderPathShader:
|
|
POST_MESSAGE(SetViewParamS, (eRenderView::GAME, L"renderpath", L"shader"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ScenarioEditor::OnDumpState(wxCommandEvent& event)
|
|
{
|
|
wxDateTime time = wxDateTime::Now();
|
|
wxString filename;
|
|
bool doBinary = false;
|
|
|
|
switch (event.GetId())
|
|
{
|
|
case ID_DumpState:
|
|
filename = wxString::Format(_T("sim-dump-%d.txt"), time.GetTicks());
|
|
break;
|
|
case ID_DumpBinaryState:
|
|
doBinary = true;
|
|
filename = wxString::Format(_T("sim-dump-%d.dat"), time.GetTicks());
|
|
break;
|
|
}
|
|
|
|
qSimStateDebugDump q(doBinary);
|
|
q.Post();
|
|
|
|
std::wstring dump = *q.dump;
|
|
wxString state(dump.c_str());
|
|
|
|
wxFFile file(filename.c_str(), _T("w"));
|
|
if (file.IsOpened() && !file.Error())
|
|
{
|
|
file.Write(state);
|
|
file.Close();
|
|
}
|
|
else
|
|
{
|
|
wxLogError(_("Error writing to file '%ls'"), filename.c_str());
|
|
}
|
|
}
|
|
|
|
void ScenarioEditor::OnSelectedObjectsChange(const std::vector<ObjectID>& selectedObjects)
|
|
{
|
|
GetMenuBar()->Enable(ID_Copy, !selectedObjects.empty());
|
|
}
|
|
|
|
void ScenarioEditor::OnMenuOpen(wxMenuEvent& event)
|
|
{
|
|
// This could be done far more elegantly if wxMenuItem had changeable id.
|
|
wxMenu* pasteMenuItem = NULL;
|
|
event.GetMenu()->FindItem(ID_Paste, &pasteMenuItem);
|
|
|
|
GetMenuBar()->Enable(ID_Paste, false);
|
|
|
|
if (!pasteMenuItem)
|
|
return;
|
|
|
|
wxString content;
|
|
if (wxTheClipboard->Open())
|
|
{
|
|
if (wxTheClipboard->IsSupported(wxDF_TEXT))
|
|
{
|
|
wxTextDataObject data;
|
|
wxTheClipboard->GetData(data);
|
|
content = data.GetText();
|
|
}
|
|
|
|
wxTheClipboard->Close();
|
|
}
|
|
|
|
if (content.empty())
|
|
return;
|
|
|
|
wxInputStream* is = new wxStringInputStream(content);
|
|
wxXmlDocument doc;
|
|
{
|
|
wxLogNull stopComplaining;
|
|
static_cast<void>(stopComplaining);
|
|
if (!doc.Load(*is))
|
|
return;
|
|
}
|
|
|
|
wxXmlNode* root = doc.GetRoot();
|
|
if (!root || root->GetName() != wxT("Entities"))
|
|
return;
|
|
|
|
GetMenuBar()->Enable(ID_Paste, true);
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
Position::Position(const wxPoint& pt)
|
|
: type(1)
|
|
{
|
|
type1.x = pt.x;
|
|
type1.y = pt.y;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Disabled (and should be removed if it turns out to be unnecessary)
|
|
- see MessagePasserImpl.cpp for information
|
|
|
|
static void QueryCallback()
|
|
{
|
|
// If this thread completely blocked on the semaphore inside Query, it would
|
|
// never respond to window messages, and the system deadlocks if the
|
|
// game tries to display an assertion failure dialog. (See
|
|
// WaitForSingleObject on MSDN.)
|
|
// So, this callback is called occasionally, and gives wx a change to
|
|
// handle messages.
|
|
|
|
// This is kind of like wxYield, but without the ProcessPendingEvents -
|
|
// it's enough to make Windows happy and stop deadlocking, without actually
|
|
// calling the event handlers (which could lead to nasty recursion)
|
|
|
|
// while (wxEventLoop::GetActive()->Pending())
|
|
// wxEventLoop::GetActive()->Dispatch();
|
|
|
|
// Oh dear, we can't use that either - it (at least in wx 2.6.3) still
|
|
// processes messages, which causes reentry into various things that we
|
|
// don't want to be reentrant. So do it all manually, accepting Windows
|
|
// messages and sticking them on a list for later processing (in a custom
|
|
// event loop class):
|
|
|
|
// (TODO: Rethink this entire process on Linux)
|
|
// (Alt TODO: Could we make the game never pop up windows (or use the Win32
|
|
// GUI in any other way) when it's running under Atlas, so we wouldn't need
|
|
// to do any message processing here at all?)
|
|
|
|
#ifdef _WIN32
|
|
AtlasEventLoop* evtLoop = (AtlasEventLoop*)wxEventLoop::GetActive();
|
|
|
|
// evtLoop might be NULL, particularly if we're still initialising windows
|
|
// and haven't got into the normal event loop yet. But we'd have to process
|
|
// messages anyway, to avoid the deadlocks that this is for. So, don't bother
|
|
// with that and just crash instead.
|
|
// (Maybe it could be solved better by constructing/finding an event loop
|
|
// object here and setting it as the global one, assuming it's not overwritten
|
|
// later by wx.)
|
|
|
|
while (evtLoop->Pending())
|
|
{
|
|
// Based on src/msw/evtloop.cpp's wxEventLoop::Dispatch()
|
|
|
|
MSG msg;
|
|
BOOL rc = ::GetMessage(&msg, (HWND) NULL, 0, 0);
|
|
|
|
if (rc == 0)
|
|
{
|
|
// got WM_QUIT
|
|
return;
|
|
}
|
|
|
|
if (rc == -1)
|
|
{
|
|
wxLogLastError(wxT("GetMessage"));
|
|
return;
|
|
}
|
|
|
|
// Our special bits:
|
|
|
|
if (msg.message == WM_PAINT)
|
|
{
|
|
// "GetMessage does not remove WM_PAINT messages from the queue.
|
|
// The messages remain in the queue until processed."
|
|
// So let's process them, to avoid infinite loops...
|
|
PAINTSTRUCT paint;
|
|
::BeginPaint(msg.hwnd, &paint);
|
|
::EndPaint(msg.hwnd, &paint);
|
|
// Remember that some painting was needed - we'll just repaint
|
|
// the whole screen when this is finished.
|
|
evtLoop->NeedsPaint();
|
|
}
|
|
else
|
|
{
|
|
// Add this message to a queue for later processing. (That's
|
|
// probably kind of valid, at least in most cases.)
|
|
MSG* pMsg = new MSG(msg);
|
|
evtLoop->AddMessage(pMsg);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
*/
|
|
void QueryMessage::Post()
|
|
{
|
|
// g_MessagePasser->Query(this, &QueryCallback);
|
|
g_MessagePasser->Query(this, NULL);
|
|
}
|