diff --git a/ChangeLog b/ChangeLog index e916d8aa6..a50e9f412 100644 --- a/ChangeLog +++ b/ChangeLog @@ -23,6 +23,8 @@ - Added a new welcome/landing window on launching the GUI without a game. This has the effect of avoiding creating a window with a trivial extensive game which the user then has to dismiss if they do not require it. (#80) +- Clarified handling of .efg/.nfg/.gbt file types in graphical interface and refined warnings about + unsaved work. (#12) ### Removed - Built-in plotting of logit QRE for strategic games has been removed in the GUI (#809) diff --git a/src/gui/app.cc b/src/gui/app.cc index 3daabb807..b4dde6baf 100644 --- a/src/gui/app.cc +++ b/src/gui/app.cc @@ -145,7 +145,7 @@ AppLoadResult Application::LoadFile(const wxString &p_filename, wxWindow *p_pare m_fileHistory.AddFileToHistory(p_filename); m_fileHistory.Save(*wxConfigBase::Get()); doc = new GameDocument(nfg); - doc->SetFilename(""); + doc->SetFilename(p_filename); (void)new GameFrame(nullptr, doc); return GBT_APP_FILE_OK; } diff --git a/src/gui/gamedoc.cc b/src/gui/gamedoc.cc index cba37d518..4d7d5b8d7 100644 --- a/src/gui/gamedoc.cc +++ b/src/gui/gamedoc.cc @@ -99,8 +99,8 @@ bool StrategyDominanceStack::PreviousLevel() //========================================================================= GameDocument::GameDocument(Game p_game) - : m_game(p_game), m_selectNode(nullptr), m_modified(false), m_stratSupports(this, true), - m_currentProfileList(0) + : m_game(p_game), m_selectNode(nullptr), m_gameModified(false), m_unsavedResults(false), + m_stratSupports(this, true), m_currentProfileList(0) { wxGetApp().AddDocument(this); @@ -112,7 +112,7 @@ GameDocument::~GameDocument() { wxGetApp().RemoveDocument(this); } bool GameDocument::LoadDocument(const wxString &p_filename) { - TiXmlDocument doc((const char *)p_filename.mb_str()); + TiXmlDocument doc(p_filename.mb_str()); if (!doc.LoadFile()) { // Some error occurred. Do something smart later. return false; @@ -259,12 +259,10 @@ void GameDocument::SaveDocument(std::ostream &p_file) const void GameDocument::UpdateViews(GameModificationType p_modifications) { - if (p_modifications != GBT_DOC_MODIFIED_NONE) { - m_modified = true; - std::ostringstream s; - SaveDocument(s); + if (p_modifications == GBT_DOC_MODIFIED_GAME || p_modifications == GBT_DOC_MODIFIED_PAYOFFS || + p_modifications == GBT_DOC_MODIFIED_LABELS) { + m_gameModified = true; } - if (p_modifications == GBT_DOC_MODIFIED_GAME || p_modifications == GBT_DOC_MODIFIED_PAYOFFS) { m_stratSupports.Reset(); @@ -373,27 +371,32 @@ void GameDocument::SetSelectNode(GameNode p_node) // Commands for model part of MVC architecture start here. //====================================================================== -void GameDocument::DoSave(const wxString &p_filename) +void GameDocument::DoSave(const wxString &p_filename, GameSaveFormat p_format) { - std::ofstream file(static_cast(p_filename.mb_str())); - SaveDocument(file); - m_filename = p_filename; - SetModified(false); - UpdateViews(GBT_DOC_MODIFIED_NONE); -} + std::ofstream file(p_filename.mb_str()); + if (!file) { + throw std::runtime_error(std::string("Unable to open file for writing: ") + + static_cast(p_filename.mb_str())); + } + switch (p_format) { + case GameSaveFormat::Workbook: + SaveDocument(file); + m_filename = p_filename; + m_gameModified = false; + m_unsavedResults = false; + break; -void GameDocument::DoExportEfg(const wxString &p_filename) -{ - std::ofstream file(static_cast(p_filename.mb_str())); - m_game->Write(file, "efg"); - UpdateViews(GBT_DOC_MODIFIED_NONE); -} + case GameSaveFormat::Efg: + m_game->Write(file, "efg"); + m_gameModified = false; + break; -void GameDocument::DoExportNfg(const wxString &p_filename) -{ - std::ofstream file(static_cast(p_filename.mb_str())); - BuildNfg(); - m_game->Write(file, "nfg"); + case GameSaveFormat::Nfg: + BuildNfg(); + m_game->Write(file, "nfg"); + m_gameModified = false; + break; + } UpdateViews(GBT_DOC_MODIFIED_NONE); } diff --git a/src/gui/gamedoc.h b/src/gui/gamedoc.h index ab246b9d1..32f2e9e1e 100644 --- a/src/gui/gamedoc.h +++ b/src/gui/gamedoc.h @@ -152,7 +152,7 @@ class GameDocument { TreeRenderConfig m_style; GameNode m_selectNode; - bool m_modified; + bool m_gameModified, m_unsavedResults; StrategyDominanceStack m_stratSupports; @@ -181,8 +181,11 @@ class GameDocument { const wxString &GetFilename() const { return m_filename; } void SetFilename(const wxString &p_filename) { m_filename = p_filename; } - bool IsModified() const { return m_modified; } - void SetModified(bool p_modified) { m_modified = p_modified; } + bool IsModified() const { return m_gameModified || m_unsavedResults; } + bool IsGameModified() const { return m_gameModified; } + bool AreResultsUnsaved() const { return m_unsavedResults; } + void SetGameModified(bool p_modified) { m_gameModified = p_modified; } + void SetUnsavedResults(bool p_unsaved) { m_unsavedResults = p_unsaved; } const TreeRenderConfig &GetStyle() const { return m_style; } void SetStyle(const TreeRenderConfig &p_style); @@ -237,9 +240,18 @@ class GameDocument { void PostPendingChanges(); /// Operations on game model - void DoSave(const wxString &p_filename); - void DoExportEfg(const wxString &p_filename); - void DoExportNfg(const wxString &p_filename); + enum class GameSaveFormat { Efg, Nfg, Workbook }; + GameSaveFormat GetCurrentSaveFormat() const + { + if (m_filename.EndsWith(".efg")) { + return GameSaveFormat::Efg; + } + if (m_filename.EndsWith(".nfg")) { + return GameSaveFormat::Nfg; + } + return GameSaveFormat::Workbook; + } + void DoSave(const wxString &p_filename, GameSaveFormat p_format); void DoSetTitle(const wxString &p_title, const wxString &p_comment); void DoNewPlayer(); void DoSetPlayerLabel(GamePlayer p_player, const wxString &p_label); diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index edce52027..dbd4d61da 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -27,6 +27,7 @@ #ifndef WX_PRECOMP #include #endif // WX_PRECOMP +#include #include #include #if !defined(__WXMSW__) || wxUSE_POSTSCRIPT @@ -188,8 +189,6 @@ EVT_MENU(wxID_OPEN, GameFrame::OnFileOpen) EVT_MENU(wxID_CLOSE, GameFrame::OnFileClose) EVT_MENU(wxID_SAVE, GameFrame::OnFileSave) EVT_MENU(wxID_SAVEAS, GameFrame::OnFileSave) -EVT_MENU(GBT_MENU_FILE_EXPORT_EFG, GameFrame::OnFileExportEfg) -EVT_MENU(GBT_MENU_FILE_EXPORT_NFG, GameFrame::OnFileExportNfg) EVT_MENU(GBT_MENU_FILE_EXPORT_BMP, GameFrame::OnFileExportGraphic) EVT_MENU(GBT_MENU_FILE_EXPORT_JPEG, GameFrame::OnFileExportGraphic) EVT_MENU(GBT_MENU_FILE_EXPORT_PNG, GameFrame::OnFileExportGraphic) @@ -323,8 +322,6 @@ void GameFrame::OnUpdate() const GameNode selectNode = m_doc->GetSelectNode(); wxMenuBar *menuBar = GetMenuBar(); - menuBar->Enable(GBT_MENU_FILE_EXPORT_EFG, m_doc->IsTree()); - menuBar->Enable(GBT_MENU_EDIT_INSERT_MOVE, selectNode != nullptr); menuBar->Enable(GBT_MENU_EDIT_INSERT_ACTION, selectNode && selectNode->GetInfoset()); menuBar->Enable(GBT_MENU_EDIT_REVEAL, selectNode && selectNode->GetInfoset()); @@ -419,11 +416,6 @@ void GameFrame::MakeMenus() fileMenu->AppendSeparator(); auto *fileExportMenu = new wxMenu; - fileExportMenu->Append(GBT_MENU_FILE_EXPORT_EFG, _("Gambit .&efg format"), - _("Save the extensive game in .efg format")); - fileExportMenu->Append(GBT_MENU_FILE_EXPORT_NFG, _("Gambit .&nfg format"), - _("Save the strategic game in .nfg format")); - fileExportMenu->AppendSeparator(); fileExportMenu->Append(GBT_MENU_FILE_EXPORT_BMP, _("&BMP"), _("Save a rendering of the game as a Windows bitmap")); fileExportMenu->Append(GBT_MENU_FILE_EXPORT_JPEG, _("&JPEG"), @@ -641,29 +633,59 @@ void GameFrame::OnFileClose(wxCommandEvent &) { Close(); } void GameFrame::OnFileSave(wxCommandEvent &p_event) { - if (p_event.GetId() == wxID_SAVEAS || m_doc->GetFilename().empty()) { - wxFileDialog dialog(this, _("Choose file"), wxPathOnly(m_doc->GetFilename()), - wxFileNameFromPath(m_doc->GetFilename()), - wxT("Gambit workbooks (*.gbt)|*.gbt|") wxT("All files (*.*)|*.*"), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (dialog.ShowModal() == wxID_OK) { - try { - m_doc->DoSave(dialog.GetPath()); - } - catch (std::exception &ex) { - ExceptionDialog(this, ex.what()).ShowModal(); - } - } - } - else { + const bool saveAs = p_event.GetId() == wxID_SAVEAS || m_doc->GetFilename().empty(); + + auto doSave = [this](const wxString &path, GameDocument::GameSaveFormat format) { try { - m_doc->DoSave(m_doc->GetFilename()); + m_doc->DoSave(path, format); } - catch (std::exception &ex) { + catch (const std::exception &ex) { ExceptionDialog(this, ex.what()).ShowModal(); } + }; + + if (!saveAs) { + doSave(wxString::FromUTF8(m_doc->GetFilename()), m_doc->GetCurrentSaveFormat()); + return; + } + + const wxString currentFilename = wxString::FromUTF8(m_doc->GetFilename()); + + wxFileDialog dialog( + this, _("Save game as"), wxPathOnly(currentFilename), wxFileNameFromPath(currentFilename), + wxT("Gambit workbooks (*.gbt)|*.gbt|") wxT("Gambit extensive games (*.efg)|*.efg|") + wxT("Gambit strategic games (*.nfg)|*.nfg|") wxT("All files (*.*)|*.*"), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (dialog.ShowModal() != wxID_OK) { + return; + } + + wxFileName filename(dialog.GetPath()); + + if (!filename.HasExt()) { + switch (dialog.GetFilterIndex()) { + case 0: + filename.SetExt(wxT("gbt")); + break; + case 1: + filename.SetExt(wxT("efg")); + break; + case 2: + filename.SetExt(wxT("nfg")); + break; + default: + break; + } } + GameDocument::GameSaveFormat format = GameDocument::GameSaveFormat::Workbook; + if (filename.GetExt() == wxT("efg")) { + format = GameDocument::GameSaveFormat::Efg; + } + else if (filename.GetExt() == wxT("nfg")) { + format = GameDocument::GameSaveFormat::Nfg; + } + doSave(filename.GetFullPath(), format); } void GameFrame::OnFilePageSetup(wxCommandEvent &) @@ -724,28 +746,6 @@ void GameFrame::OnFilePrint(wxCommandEvent &) } } -void GameFrame::OnFileExportEfg(wxCommandEvent &) -{ - wxFileDialog dialog(this, _("Choose file"), wxGetApp().GetCurrentDir(), _T(""), - wxT("Gambit extensive games (*.efg)|*.efg|") wxT("All files (*.*)|*.*"), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (dialog.ShowModal() == wxID_OK) { - m_doc->DoExportEfg(dialog.GetPath()); - } -} - -void GameFrame::OnFileExportNfg(wxCommandEvent &) -{ - wxFileDialog dialog(this, _("Choose file"), wxGetApp().GetCurrentDir(), _T(""), - wxT("Gambit strategic games (*.nfg)|*.nfg|") wxT("All files (*.*)|*.*"), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (dialog.ShowModal() == wxID_OK) { - m_doc->DoExportNfg(dialog.GetPath()); - } -} - void GameFrame::OnFileExportGraphic(wxCommandEvent &p_event) { wxBitmap bitmap = wxNullBitmap; @@ -1275,16 +1275,46 @@ void GameFrame::OnUnsplit(wxSplitterEvent &) GetToolBar()->ToggleTool(GBT_MENU_VIEW_PROFILES, false); } +namespace { + +wxString CloseWarningMessage(GameDocument *p_doc) +{ + if (p_doc->IsGameModified() && !p_doc->AreResultsUnsaved()) { + return _("This game has unsaved changes.\n\n" + "Close without saving?"); + } + if (!p_doc->IsGameModified() && p_doc->AreResultsUnsaved()) { + return _("There are unsaved computational results.\n\n" + "These changes are not saved in ordinary game files.\n" + "Close without saving?"); + } + if (p_doc->IsGameModified() && p_doc->AreResultsUnsaved()) { + return _("This game has unsaved changes, and there are unsaved computational results " + "unsaved computational results or workspace changes.\n\n" + "Close without saving?"); + } + return wxEmptyString; +} + +} // namespace + void GameFrame::OnCloseWindow(wxCloseEvent &p_event) { - if (p_event.CanVeto() && m_doc->IsModified()) { - if (wxMessageBox(wxT("Game has been modified.\n") wxT("Unsaved changes will be lost!\n") - wxT("Close anyway?"), - _("Warning"), wxOK | wxCANCEL) == wxCANCEL) { + if (!p_event.CanVeto()) { + p_event.Skip(); + return; + } + + if (m_doc->IsModified()) { + const int response = wxMessageBox(CloseWarningMessage(m_doc), _("Unsaved Changes"), + wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this); + + if (response != wxYES) { p_event.Veto(); return; } } + p_event.Skip(); } diff --git a/src/gui/gameframe.h b/src/gui/gameframe.h index a036a4274..49415be31 100644 --- a/src/gui/gameframe.h +++ b/src/gui/gameframe.h @@ -56,8 +56,6 @@ class GameFrame final : public wxFrame, public GameView { void OnFileOpen(wxCommandEvent &); void OnFileClose(wxCommandEvent &); void OnFileSave(wxCommandEvent &); - void OnFileExportEfg(wxCommandEvent &); - void OnFileExportNfg(wxCommandEvent &); void OnFileExportGraphic(wxCommandEvent &); void OnFileExportPS(wxCommandEvent &); void OnFileExportSVG(wxCommandEvent &); diff --git a/src/gui/menuconst.h b/src/gui/menuconst.h index 2263a7381..c52bdf9f9 100644 --- a/src/gui/menuconst.h +++ b/src/gui/menuconst.h @@ -33,8 +33,6 @@ enum MenuItems { GBT_MENU_FILE_EXPORT_PNG = 1105, GBT_MENU_FILE_EXPORT_POSTSCRIPT = 1106, GBT_MENU_FILE_EXPORT_SVG = 1109, - GBT_MENU_FILE_EXPORT_EFG = 1107, - GBT_MENU_FILE_EXPORT_NFG = 1108, GBT_MENU_EDIT_INSERT_MOVE = 1200, GBT_MENU_EDIT_INSERT_ACTION = 1201,