From 1771c0830fdb84ca4d5a43f5702409a055b25f17 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 09:55:26 +0000 Subject: [PATCH 01/22] Initial work on #106. Moved the load order preview window to a new header/source file, and cut out the move up/down buttons. Started to implement mini editor. --- CMakeLists.txt | 1 + src/gui/editor.h | 3 - src/gui/main.cpp | 201 +------------------------------------------- src/gui/main.h | 19 ----- src/gui/sorting.cpp | 168 ++++++++++++++++++++++++++++++++++++ src/gui/sorting.h | 66 +++++++++++++++ 6 files changed, 239 insertions(+), 219 deletions(-) create mode 100644 src/gui/sorting.cpp create mode 100644 src/gui/sorting.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 70cedffb..d5820139 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ set (BOSS_GUI_SRC ${BOSS_SRC} "${CMAKE_SOURCE_DIR}/src/gui/settings.cpp" "${CMAKE_SOURCE_DIR}/src/gui/editor.cpp" "${CMAKE_SOURCE_DIR}/src/gui/viewer.cpp" + "${CMAKE_SOURCE_DIR}/src/gui/sorting.cpp" "${CMAKE_SOURCE_DIR}/src/resource.rc") set (BOSS_API_SRC ${BOSS_SRC} diff --git a/src/gui/editor.h b/src/gui/editor.h index 55f565b2..3d94c292 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -61,13 +61,10 @@ public: void OnPluginCopyName(wxCommandEvent& event); void OnPluginCopyMetadata(wxCommandEvent& event); void OnPluginClearMetadata(wxCommandEvent& event); - void OnEnabledToggle(wxCommandEvent& event); - void OnPriorityChange(wxSpinEvent& event); void OnListBookChange(wxBookCtrlEvent& event); void OnAddRow(wxCommandEvent& event); void OnEditRow(wxCommandEvent& event); void OnRemoveRow(wxCommandEvent& event); - void OnRecalc(wxCommandEvent& event); void OnRowSelect(wxListEvent& event); void OnQuit(wxCommandEvent& event); private: diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 1e957dd5..0e3d692e 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -24,6 +24,7 @@ #include "settings.h" #include "editor.h" #include "viewer.h" +#include "sorting.h" #include "../backend/globals.h" #include "../backend/metadata.h" @@ -680,7 +681,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { if (preview.ShowModal() == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "Load order accepted."; list newPluginsList, editedPlugins; - newPluginsList = preview.GetLoadOrder(); + /*newPluginsList = preview.GetLoadOrder(); BOOST_LOG_TRIVIAL(trace) << "Building list of edited plugins."; for (list::iterator it=newPluginsList.begin(),endit=newPluginsList.end(); it != endit; ++it) { @@ -746,7 +747,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { uout << yout.c_str(); uout.close(); } - + */ //Now set load order. BOOST_LOG_TRIVIAL(debug) << "Setting load order."; try { @@ -1093,198 +1094,4 @@ void Launcher::OnAbout(wxCommandEvent& event) { "along with this program. If not, see ."); aboutInfo.SetIcon(wxIconLocation("BOSS.exe")); wxAboutBox(aboutInfo); -} - -LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _plugins(plugins), _game(game) { - - //Init controls. - _loadOrder = new wxListView(this, LIST_LoadOrder, wxDefaultPosition, wxDefaultSize, wxLC_REPORT); - - _moveUp = new wxButton(this, BUTTON_MoveUp, translate("Up")); - _moveDown = new wxButton(this, BUTTON_MoveDown, translate("Down")); - - //Populate list. - _loadOrder->AppendColumn(translate("Load Order")); - size_t i=0; - for (list::const_iterator it=plugins.begin(), endit=plugins.end(); it != endit; ++it, ++i) { - _loadOrder->InsertItem(i, FromUTF8(it->Name())); - if (it->FormIDs().empty()) { - _loadOrder->SetItemTextColour(i, wxColour(122, 122, 122)); - } - else if (it->LoadsBSA(_game)) { - _loadOrder->SetItemTextColour(i, wxColour(0, 142, 219)); - } - } - _loadOrder->SetColumnWidth(0, wxLIST_AUTOSIZE); - - //Set up event handling. - Bind(wxEVT_BUTTON, &LoadOrderPreview::OnMoveUp, this, BUTTON_MoveUp); - Bind(wxEVT_BUTTON, &LoadOrderPreview::OnMoveDown, this, BUTTON_MoveDown); - Bind(wxEVT_LIST_ITEM_SELECTED, &LoadOrderPreview::OnPluginSelect, this, LIST_LoadOrder); - - //Set up layout. - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - bigBox->Add(_loadOrder, 1, wxEXPAND|wxALL, 15); - - wxBoxSizer * hbox = new wxBoxSizer(wxHORIZONTAL); - hbox->Add(_moveUp, 0, wxRIGHT, 5); - hbox->Add(_moveDown, 0, wxLEFT, 5); - bigBox->Add(hbox, 0, wxALIGN_RIGHT|wxBOTTOM|wxRIGHT, 15); - - bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Please submit any alterations made for reasons other than user preference \nto the BOSS team so that they may include the changes in the masterlist.")), 0, wxEXPAND|wxLEFT|wxRIGHT|wxBOTTOM, 15); - - //Need to add 'OK' and 'Cancel' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxOK|wxCANCEL); - - //Now add TabHolder and OK button to window sizer. - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND|wxLEFT|wxBOTTOM|wxRIGHT, 15); - - //Set initial up/down button states. - _moveUp->Enable(false); - _moveDown->Enable(false); - - //Now set the layout and sizes. - SetBackgroundColour(wxColour(255,255,255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} - -void LoadOrderPreview::OnPluginSelect(wxListEvent& event) { - _moveUp->Enable(true); - _moveDown->Enable(true); -} - -void LoadOrderPreview::OnMoveUp(wxCommandEvent& event) { - BOOST_LOG_TRIVIAL(debug) << "Moving plugin(s) up the load order."; - long selected = _loadOrder->GetFirstSelected(); - - if (selected == 0) - return; - - while (selected != -1) { - wxString selectedText = _loadOrder->GetItemText(selected); - wxString aboveText = _loadOrder->GetItemText(selected - 1); - - BOOST_LOG_TRIVIAL(trace) << "Moving plugin \"" << string(selectedText.ToUTF8()); - - //Check that move is OK. - list::const_iterator selectedPlugin = find(_plugins.begin(), _plugins.end(), boss::Plugin(string(selectedText.ToUTF8()))); - list::const_iterator abovePlugin = find(_plugins.begin(), _plugins.end(), boss::Plugin(string(aboveText.ToUTF8()))); - - if (selectedPlugin != _plugins.end() && abovePlugin != _plugins.end()) { - if (selectedPlugin->MustLoadAfter(*abovePlugin)) { - BOOST_LOG_TRIVIAL(error) << "Cannot load \"" << selectedPlugin->Name() << "\" before \"" << abovePlugin->Name() << "\"."; - wxMessageBox( - FromUTF8(format(loc::translate("Error: Cannot load \"%1%\" before \"%2%\".")) % selectedPlugin->Name() % abovePlugin->Name()), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - NULL); - selected = _loadOrder->GetNextSelected(selected); - continue; - } - } - - _loadOrder->SetItemText(selected, aboveText); - _loadOrder->SetItemText(selected - 1, selectedText); - - _movedPlugins.insert(string(selectedText.ToUTF8())); - - _loadOrder->Select(selected, false); - _loadOrder->Select(selected - 1, true); - - selected = _loadOrder->GetNextSelected(selected); - } - BOOST_LOG_TRIVIAL(debug) << "Plugin(s) moved up."; -} - -void LoadOrderPreview::OnMoveDown(wxCommandEvent& event) { - BOOST_LOG_TRIVIAL(debug) << "Moving plugin(s) down the load order."; - long i=_loadOrder->GetItemCount() - 1; - - if (_loadOrder->IsSelected(i)) - return; - --i; - - for (i; i > -1; --i) { - if (_loadOrder->IsSelected(i)) { - wxString selectedText = _loadOrder->GetItemText(i); - wxString belowText = _loadOrder->GetItemText(i + 1); - - BOOST_LOG_TRIVIAL(trace) << "Moving plugin \"" << string(selectedText.ToUTF8()); - - //Check that move is OK. - list::const_iterator selectedPlugin = find(_plugins.begin(), _plugins.end(), boss::Plugin(string(selectedText.ToUTF8()))); - list::const_iterator belowPlugin = find(_plugins.begin(), _plugins.end(), boss::Plugin(string(belowText.ToUTF8()))); - - if (selectedPlugin != _plugins.end() && belowPlugin != _plugins.end()) { - if (belowPlugin->MustLoadAfter(*selectedPlugin)) { - BOOST_LOG_TRIVIAL(error) << "Cannot load \"" << belowPlugin->Name() << "\" before \"" << selectedPlugin->Name() << "\"."; - wxMessageBox( - FromUTF8(format(loc::translate("Error: Cannot load \"%1%\" before \"%2%\".")) % belowPlugin->Name() % selectedPlugin->Name()), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - NULL); - continue; - } - } - - _loadOrder->SetItemText(i, belowText); - _loadOrder->SetItemText(i + 1, selectedText); - - _movedPlugins.insert(string(selectedText.ToUTF8())); - - _loadOrder->Select(i, false); - _loadOrder->Select(i + 1, true); - } - } - BOOST_LOG_TRIVIAL(debug) << "Plugin(s) moved down."; -} - -std::list LoadOrderPreview::GetLoadOrder() const { - BOOST_LOG_TRIVIAL(debug) << "Getting full load order from preview window."; - list plugins; - bool wasMovedUp = false; - for (size_t i=0,max=_loadOrder->GetItemCount(); i < max; ++i) { - string name = string(_loadOrder->GetItemText(i).ToUTF8()); - - list::const_iterator it = find(_plugins.begin(), _plugins.end(), boss::Plugin(name)); - - if (it == _plugins.end()) - continue; - - plugins.push_back(*it); - - if (wasMovedUp) { - BOOST_LOG_TRIVIAL(trace) << "The previous plugin was moved up in the load order, so adding it to the 'load after' set for the current plugin."; - list::const_iterator jt = ----plugins.end(); - set loadAfter = plugins.back().LoadAfter(); - loadAfter.insert(File(jt->Name())); - plugins.back().LoadAfter(loadAfter); - wasMovedUp = false; - } - - if (_movedPlugins.find(name) != _movedPlugins.end()) { - BOOST_LOG_TRIVIAL(trace) << "The current plugin was moved - checking if it was moved up or down."; - //Check if this plugin has been moved earlier or later by comparing distances in the original list and the new one. - size_t newDist = plugins.size() - 1; - size_t oldDist = distance(_plugins.begin(), it); - - if (newDist > oldDist) { - BOOST_LOG_TRIVIAL(trace) << "The current plugin was moved down, add the preceding plugin to the current plugin's 'load after' set."; - //Record the preceding plugin in this plugin's "load after" set. - list::const_iterator jt = ----plugins.end(); - set loadAfter = plugins.back().LoadAfter(); - loadAfter.insert(File(jt->Name())); - plugins.back().LoadAfter(loadAfter); - } else { - BOOST_LOG_TRIVIAL(trace) << "The current plugin was moved up."; - //Record this plugin in the following plugin's "load after" set. - wasMovedUp = true; - } - } - } - - return plugins; -} +} \ No newline at end of file diff --git a/src/gui/main.h b/src/gui/main.h index f682cdd3..ce6d59a7 100644 --- a/src/gui/main.h +++ b/src/gui/main.h @@ -70,23 +70,4 @@ private: std::vector& _games; }; -class LoadOrderPreview : public wxDialog { -public: - LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game); - - void OnPluginSelect(wxListEvent& event); - void OnMoveUp(wxCommandEvent& event); - void OnMoveDown(wxCommandEvent& event); - - std::list GetLoadOrder() const; -private: - wxListView * _loadOrder; - wxButton * _moveUp; - wxButton * _moveDown; - - const std::list _plugins; - std::set _movedPlugins; - const boss::Game& _game; -}; - #endif diff --git a/src/gui/sorting.cpp b/src/gui/sorting.cpp new file mode 100644 index 00000000..e3881f87 --- /dev/null +++ b/src/gui/sorting.cpp @@ -0,0 +1,168 @@ +/* BOSS + +A plugin load order optimiser for games that use the esp/esm plugin system. + +Copyright (C) 2013-2014 WrinklyNinja + +This file is part of BOSS. + +BOSS 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 3 of +the License, or (at your option) any later version. + +BOSS 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 BOSS. If not, see +. +*/ + +#include "sorting.h" +#include "../backend/helpers.h" + +using namespace std; + +MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::vector& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _basePlugins(plugins) { + //Initialise controls. + pluginText = new wxStaticText(this, wxID_ANY, ""); + prioritySpin = new wxSpinCtrl(this, wxID_ANY, "0"); + prioritySpin->SetRange(std::numeric_limits::min(), std::numeric_limits::max()); + filterCheckbox = new wxCheckBox(this, wxID_ANY, translate("Show only conflicting plugins.")); + + pluginList = new wxListView(this, LIST_Plugins, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + loadAfterList = new wxListView(this, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + + removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove")); + applyBtn = new wxButton(this, BUTTON_Apply, translate("Apply Changes")); + cancelBtn = new wxButton(this, BUTTON_Cancel, translate("Cancel")); + + wxMenu * pluginMenu = new wxMenu(); + + //Set up list columns. + pluginList->AppendColumn(translate("Plugin")); + pluginList->AppendColumn(translate("Priority")); + loadAfterList->AppendColumn(translate("Filename")); + + //Set up plugin right-click menu. + pluginMenu->Append(MENU_CopyName, translate("Copy Name")); + pluginMenu->Append(MENU_CopyMetadata, translate("Copy Metadata As Text")); + pluginMenu->Append(MENU_ClearMetadata, translate("Remove All User-Added Metadata")); + + //Initialise control states. + removeBtn->Enable(false); + prioritySpin->Enable(false); + filterCheckbox->Enable(false); + + //Make plugin name bold text. + pluginText->SetFont(pluginText->GetFont().Bold()); + + //Set up layout. + wxBoxSizer * bigBox = new wxBoxSizer(wxHORIZONTAL); + + bigBox->Add(pluginList, 1, wxEXPAND | wxALL, 10); + + wxBoxSizer * mainBox = new wxBoxSizer(wxVERTICAL); + + mainBox->Add(pluginText, 0, wxTOP | wxBOTTOM, 10); + + wxBoxSizer * hbox1 = new wxBoxSizer(wxHORIZONTAL); + hbox1->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); + hbox1->Add(prioritySpin, 0, wxALIGN_RIGHT); + mainBox->Add(hbox1, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 5); + + + mainBox->Add(loadAfterList, 1, wxEXPAND); + mainBox->Add(removeBtn, 0, wxALIGN_RIGHT, 5); + + bigBox->Add(mainBox, 2, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); + + //Need to add 'Yes' and 'No' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxYES | wxNO | wxCANCEL); + + //Now add buttons to window sizer. + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + //Fill pluginList with the contents of basePlugins. + for (int i = 0, max = _basePlugins.size(); i < max; ++i) { + pluginList->InsertItem(i, FromUTF8(_basePlugins[i].Name())); + pluginList->SetItem(i, 2, FromUTF8(boss::IntToString(_basePlugins[i].Priority()))); + if (_basePlugins[i].LoadsBSA(game)) { + pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); + } + if (std::find(_editedPlugins.begin(), _editedPlugins.end(), _basePlugins[i]) != _editedPlugins.end()) { + pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + } + } + pluginList->SetColumnWidth(0, wxLIST_AUTOSIZE); + + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + + SetSizerAndFit(bigBox); + Layout(); +} + +void MiniEditor::OnPluginSelect(wxListEvent& event) { + +} + +void MiniEditor::OnPluginListRightClick(wxListEvent& event) { + +} + +void MiniEditor::OnPluginCopy(wxCommandEvent& event) { + +} + +void MiniEditor::OnFilterToggle(wxCommandEvent& event) { + +} + +void MiniEditor::OnQuit(wxCommandEvent& event) { + +} + + +LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + //Init controls. + _loadOrder = new wxListView(this, LIST_LoadOrder, wxDefaultPosition, wxDefaultSize, wxLC_REPORT); + + //Populate list. + _loadOrder->AppendColumn(translate("Load Order")); + size_t i = 0; + for (list::const_iterator it = plugins.begin(), endit = plugins.end(); it != endit; ++it, ++i) { + _loadOrder->InsertItem(i, FromUTF8(it->Name())); + if (it->FormIDs().empty()) { + _loadOrder->SetItemTextColour(i, wxColour(122, 122, 122)); + } + else if (it->LoadsBSA(game)) { + _loadOrder->SetItemTextColour(i, wxColour(0, 142, 219)); + } + } + _loadOrder->SetColumnWidth(0, wxLIST_AUTOSIZE); + + //Set up layout. + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + bigBox->Add(_loadOrder, 1, wxEXPAND | wxALL, 15); + + bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you wish to make any changes to the load order above?")), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 15); + + //Need to add 'Yes' and 'No' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxYES | wxNO | wxCANCEL); + + //Now add buttons to window sizer. + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + //Now set the layout and sizes. + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + SetSizerAndFit(bigBox); +} diff --git a/src/gui/sorting.h b/src/gui/sorting.h new file mode 100644 index 00000000..4d371a09 --- /dev/null +++ b/src/gui/sorting.h @@ -0,0 +1,66 @@ +/* BOSS + +A plugin load order optimiser for games that use the esp/esm plugin system. + +Copyright (C) 2013-2014 WrinklyNinja + +This file is part of BOSS. + +BOSS 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 3 of +the License, or (at your option) any later version. + +BOSS 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 BOSS. If not, see +. +*/ +#ifndef __BOSS_GUI_SORTING__ +#define __BOSS_GUI_SORTING__ + +#include "ids.h" +#include "../backend/metadata.h" + +#include +#include +#include +#include +#include + +class MiniEditor : public wxDialog { +public: + MiniEditor(wxWindow *parent, const wxString& title, const std::vector& plugins, const boss::Game& game); + + void OnPluginSelect(wxListEvent& event); + void OnPluginListRightClick(wxListEvent& event); + void OnPluginCopy(wxCommandEvent& event); + void OnFilterToggle(wxCommandEvent& event); + void OnQuit(wxCommandEvent& event); +private: + wxButton * removeBtn; + wxButton * applyBtn; + wxButton * cancelBtn; + wxListView * pluginList; + wxListView * loadAfterList; + wxCheckBox * filterCheckbox; + wxSpinCtrl * prioritySpin; + wxStaticText * pluginText; + + + const std::vector& _basePlugins; + std::list _editedPlugins; +}; + +class LoadOrderPreview : public wxDialog { +public: + LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game); +private: + wxListView * _loadOrder; +}; + +#endif From 425a49cee72d69ec43b39b8825001f171cc28eba Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 10:28:03 +0000 Subject: [PATCH 02/22] MiniEditor layout and plugin list work. --- src/gui/sorting.cpp | 47 +++++++++++++++++++++++---------------------- src/gui/sorting.h | 6 ++---- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/gui/sorting.cpp b/src/gui/sorting.cpp index e3881f87..4e927111 100644 --- a/src/gui/sorting.cpp +++ b/src/gui/sorting.cpp @@ -26,7 +26,7 @@ along with BOSS. If not, see using namespace std; -MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::vector& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _basePlugins(plugins) { +MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _basePlugins(plugins) { //Initialise controls. pluginText = new wxStaticText(this, wxID_ANY, ""); prioritySpin = new wxSpinCtrl(this, wxID_ANY, "0"); @@ -37,8 +37,6 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::vecto loadAfterList = new wxListView(this, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove")); - applyBtn = new wxButton(this, BUTTON_Apply, translate("Apply Changes")); - cancelBtn = new wxButton(this, BUTTON_Cancel, translate("Cancel")); wxMenu * pluginMenu = new wxMenu(); @@ -48,9 +46,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::vecto loadAfterList->AppendColumn(translate("Filename")); //Set up plugin right-click menu. - pluginMenu->Append(MENU_CopyName, translate("Copy Name")); - pluginMenu->Append(MENU_CopyMetadata, translate("Copy Metadata As Text")); - pluginMenu->Append(MENU_ClearMetadata, translate("Remove All User-Added Metadata")); + pluginMenu->Append(MENU_CopyName, translate("Copy Into Load After List")); //Initialise control states. removeBtn->Enable(false); @@ -61,42 +57,47 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::vecto pluginText->SetFont(pluginText->GetFont().Bold()); //Set up layout. - wxBoxSizer * bigBox = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - bigBox->Add(pluginList, 1, wxEXPAND | wxALL, 10); + wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); + + hBox->Add(pluginList, 1, wxEXPAND | wxALL, 10); wxBoxSizer * mainBox = new wxBoxSizer(wxVERTICAL); mainBox->Add(pluginText, 0, wxTOP | wxBOTTOM, 10); + mainBox->Add(filterCheckbox, 0, wxTOP | wxBOTTOM, 10); - wxBoxSizer * hbox1 = new wxBoxSizer(wxHORIZONTAL); - hbox1->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); - hbox1->Add(prioritySpin, 0, wxALIGN_RIGHT); - mainBox->Add(hbox1, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 5); - + wxBoxSizer * hbox2 = new wxBoxSizer(wxHORIZONTAL); + hbox2->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); + hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); + mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 5); mainBox->Add(loadAfterList, 1, wxEXPAND); - mainBox->Add(removeBtn, 0, wxALIGN_RIGHT, 5); + mainBox->Add(removeBtn, 0, wxTOP | wxBOTTOM | wxALIGN_RIGHT, 10); - bigBox->Add(mainBox, 2, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); + hBox->Add(mainBox, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); + bigBox->Add(hBox, 1, wxEXPAND); //Need to add 'Yes' and 'No' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxYES | wxNO | wxCANCEL); + wxSizer * sizer = CreateSeparatedButtonSizer(wxAPPLY | wxCANCEL); //Now add buttons to window sizer. if (sizer != NULL) bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); //Fill pluginList with the contents of basePlugins. - for (int i = 0, max = _basePlugins.size(); i < max; ++i) { - pluginList->InsertItem(i, FromUTF8(_basePlugins[i].Name())); - pluginList->SetItem(i, 2, FromUTF8(boss::IntToString(_basePlugins[i].Priority()))); - if (_basePlugins[i].LoadsBSA(game)) { + int i = 0; + for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + pluginList->InsertItem(i, FromUTF8(it->Name())); + pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); + if (it->FormIDs().empty()) { + pluginList->SetItemTextColour(i, wxColour(122, 122, 122)); + } + else if (it->LoadsBSA(game)) { pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); } - if (std::find(_editedPlugins.begin(), _editedPlugins.end(), _basePlugins[i]) != _editedPlugins.end()) { - pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); - } + ++i; } pluginList->SetColumnWidth(0, wxLIST_AUTOSIZE); diff --git a/src/gui/sorting.h b/src/gui/sorting.h index 4d371a09..ef0b4871 100644 --- a/src/gui/sorting.h +++ b/src/gui/sorting.h @@ -34,7 +34,7 @@ along with BOSS. If not, see class MiniEditor : public wxDialog { public: - MiniEditor(wxWindow *parent, const wxString& title, const std::vector& plugins, const boss::Game& game); + MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game); void OnPluginSelect(wxListEvent& event); void OnPluginListRightClick(wxListEvent& event); @@ -43,8 +43,6 @@ public: void OnQuit(wxCommandEvent& event); private: wxButton * removeBtn; - wxButton * applyBtn; - wxButton * cancelBtn; wxListView * pluginList; wxListView * loadAfterList; wxCheckBox * filterCheckbox; @@ -52,7 +50,7 @@ private: wxStaticText * pluginText; - const std::vector& _basePlugins; + const std::list& _basePlugins; std::list _editedPlugins; }; From 99cd3575527252afad49dd1858bd4fb87e04ede7 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 13:27:25 +0000 Subject: [PATCH 03/22] GUI code refactoring. Helper dialogs and controls are now in misc.h/.cpp, and the MiniEditor class got moved into editor.h/.cpp. Also , the Editor class now takes plugin lists, not vectors, to match the rest of the code. The alpha sort function had to be updated to invert the order, to give the same result. --- CMakeLists.txt | 2 +- src/backend/metadata.cpp | 2 +- src/gui/editor.cpp | 692 +++++++++++++-------------------------- src/gui/editor.h | 126 +++---- src/gui/misc.cpp | 519 +++++++++++++++++++++++++++++ src/gui/misc.h | 139 ++++++++ src/gui/sorting.cpp | 169 ---------- src/gui/sorting.h | 64 ---- 8 files changed, 933 insertions(+), 780 deletions(-) create mode 100644 src/gui/misc.cpp create mode 100644 src/gui/misc.h delete mode 100644 src/gui/sorting.cpp delete mode 100644 src/gui/sorting.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d5820139..26c7fe2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ set (BOSS_GUI_SRC ${BOSS_SRC} "${CMAKE_SOURCE_DIR}/src/gui/settings.cpp" "${CMAKE_SOURCE_DIR}/src/gui/editor.cpp" "${CMAKE_SOURCE_DIR}/src/gui/viewer.cpp" - "${CMAKE_SOURCE_DIR}/src/gui/sorting.cpp" + "${CMAKE_SOURCE_DIR}/src/gui/misc.cpp" "${CMAKE_SOURCE_DIR}/src/resource.rc") set (BOSS_API_SRC ${BOSS_SRC} diff --git a/src/backend/metadata.cpp b/src/backend/metadata.cpp index 21f85253..41e9c3f4 100644 --- a/src/backend/metadata.cpp +++ b/src/backend/metadata.cpp @@ -741,7 +741,7 @@ namespace boss { } bool alpha_sort(const Plugin& lhs, const Plugin& rhs) { - return boost::ilexicographical_compare(lhs.Name(), rhs.Name()); + return boost::ilexicographical_compare(rhs.Name(), lhs.Name()); } bool master_sort(const Plugin& lhs, const Plugin& rhs) { diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 95ef6d27..005de5db 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -34,80 +34,238 @@ using namespace std; -wxString Type[3] = { - translate("Note"), - translate("Warning"), - translate("Error") -}; +MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _basePlugins(plugins), _game(game) { + //Initialise controls. + filterCheckbox = new wxCheckBox(this, wxID_ANY, translate("Show only conflicting plugins.")); -wxString State[2] = { - translate("Add"), - translate("Remove") -}; + //Set up list columns. + pluginList->AppendColumn(translate("Plugin")); + pluginList->AppendColumn(translate("Priority")); + loadAfterList->AppendColumn(translate("Load After")); -MessageList::MessageList(wxWindow * parent, wxWindowID id, const unsigned int language) : wxListView(parent, id, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxLC_VIRTUAL|wxLC_SINGLE_SEL), _language(language) { - InsertColumn(0, translate("Type")); - InsertColumn(1, translate("Content")); - InsertColumn(2, translate("Condition")); - InsertColumn(3, translate("Language")); + //Initialise control states. + removeBtn->Enable(false); + prioritySpin->Enable(false); + filterCheckbox->Enable(false); - Bind(wxEVT_LIST_DELETE_ITEM, &MessageList::OnDeleteItem, this); + //Make plugin name bold text. + pluginText->SetFont(pluginText->GetFont().Bold()); - SetItemCount(0); + //Set up event handling. + Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); + Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnRowSelect, this, LIST_LoadAfter); + Bind(wxEVT_BUTTON, &MiniEditor::OnRemoveRow, this, BUTTON_RemoveRow); + Bind(wxEVT_CHECKBOX, &MiniEditor::OnFilterToggle, this); + + //Set up layout. + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); + + hBox->Add(pluginList, 0, wxEXPAND | wxALL, 10); + + wxBoxSizer * mainBox = new wxBoxSizer(wxVERTICAL); + + mainBox->Add(pluginText, 0, wxTOP | wxBOTTOM, 10); + mainBox->Add(filterCheckbox, 0, wxTOP | wxBOTTOM, 10); + + wxBoxSizer * hbox2 = new wxBoxSizer(wxHORIZONTAL); + hbox2->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); + hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); + mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 10); + + mainBox->Add(loadAfterList, 1, wxEXPAND); + mainBox->Add(removeBtn, 0, wxTOP | wxBOTTOM | wxALIGN_RIGHT, 10); + + hBox->Add(mainBox, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); + bigBox->Add(hBox, 1, wxEXPAND | wxALL, 5); + + //Need to add 'Yes' and 'No' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxAPPLY | wxCANCEL); + + //Now add buttons to window sizer. + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + //Fill pluginList with the contents of basePlugins. + int i = 0; + for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + pluginList->InsertItem(i, FromUTF8(it->Name())); + pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); + if (it->FormIDs().empty()) { + pluginList->SetItemTextColour(i, wxColour(122, 122, 122)); + } + else if (it->LoadsBSA(_game)) { + pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); + } + ++i; + } + pluginList->SetColumnWidth(0, wxLIST_AUTOSIZE); + + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + + SetSizerAndFit(bigBox); + Layout(); } -std::vector MessageList::GetItems() const { - return _messages; -} +void MiniEditor::OnPluginSelect(wxListEvent& event) { + //Create Plugin object for selected plugin. + wxString selectedPlugin = pluginList->GetItemText(event.GetIndex()); + wxString currentPlugin = pluginText->GetLabelText(); -void MessageList::SetItems(const std::vector& messages) { - _messages = messages; - SetItemCount(_messages.size()); - RefreshItems(0, _messages.size() - 1); -} + //Check if the selected plugin is the same as the current plugin. + if (selectedPlugin != currentPlugin) { + BOOST_LOG_TRIVIAL(debug) << "User selected plugin: " << selectedPlugin.ToUTF8(); -boss::Message MessageList::GetItem(long item) const { - return _messages[item]; -} + //Apply any current edits. + if (!currentPlugin.empty()) + ApplyEdits(currentPlugin); -void MessageList::SetItem(long item, const boss::Message& message) { - _messages[item] = message; - RefreshItem(item); -} - -void MessageList::AppendItem(const boss::Message& message) { - _messages.push_back(message); - SetItemCount(_messages.size()); - RefreshItem(_messages.size() - 1); -} - -void MessageList::OnDeleteItem(wxListEvent& event) { - _messages.erase(_messages.begin() + event.GetIndex()); - SetItemCount(_messages.size()); - RefreshItems(event.GetIndex(), _messages.size() - 1); -} - -wxString MessageList::OnGetItemText(long item, long column) const { - if (column < 0 || column > 3 || item < 0 || item > _messages.size() - 1) - return wxString(); - - if (column == 0) { - if (_messages[item].Type() == boss::g_message_say) - return Type[0]; - else if (_messages[item].Type() == boss::g_message_warn) - return Type[1]; + //Merge base metadata and edits. + boss::Plugin plugin(string(selectedPlugin.ToUTF8())); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + if (it != _basePlugins.end()) + plugin = *it; else - return Type[2]; - } else if (column == 1) { - return FromUTF8(_messages[item].ChooseContent(_language).Str()); - } else if (column == 2) { - return FromUTF8(_messages[item].Condition()); - } else { - return FromUTF8(boss::Language(_messages[item].ChooseContent(_language).Language()).Name()); + BOOST_LOG_TRIVIAL(warning) << "Could not find plugin in base list: " << plugin.Name(); + it = std::find(_editedPlugins.begin(), _editedPlugins.end(), plugin); + if (it != _editedPlugins.end()) + plugin.MergeMetadata(*it); + else + BOOST_LOG_TRIVIAL(info) << "Could not find plugin in edited list: " << plugin.Name(); + + //Now fill editor fields with new plugin's info and update control states. + BOOST_LOG_TRIVIAL(debug) << "Filling editor fields with plugin info."; + pluginText->SetLabelText(FromUTF8(plugin.Name())); + + prioritySpin->SetValue(plugin.Priority()); + loadAfterList->DeleteAllItems(); + + set files = plugin.LoadAfter(); + int i = 0; + for (set::const_iterator it = files.begin(), endit = files.end(); it != endit; ++it) { + loadAfterList->InsertItem(i, FromUTF8(it->Name())); + ++i; + } + + //Set control states. + prioritySpin->Enable(true); + filterCheckbox->Enable(true); + removeBtn->Enable(false); } } -Editor::Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::vector& basePlugins, std::vector& editedPlugins, const unsigned int language, const boss::Game& game) : wxFrame(parent, wxID_ANY, title), _userlistPath(userlistPath), _basePlugins(basePlugins), _editedPlugins(editedPlugins), _game(game) { +void MiniEditor::OnFilterToggle(wxCommandEvent& event) { + if (event.IsChecked()) { + boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); + list::const_iterator pos = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + + if (pos != _basePlugins.end()) { + pluginList->DeleteAllItems(); + + int i = 0; + for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + if (pos->DoFormIDsOverlap(*it)) { + pluginList->InsertItem(i, FromUTF8(it->Name())); + pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); + if (it->FormIDs().empty()) { + pluginList->SetItemTextColour(i, wxColour(122, 122, 122)); + } + else if (it->LoadsBSA(_game)) { + pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); + } + ++i; + } + } + } + } + else { + pluginList->DeleteAllItems(); + int i = 0; + for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + pluginList->InsertItem(i, FromUTF8(it->Name())); + pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); + if (it->FormIDs().empty()) { + pluginList->SetItemTextColour(i, wxColour(122, 122, 122)); + } + else if (it->LoadsBSA(_game)) { + pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); + } + ++i; + } + } +} + +void MiniEditor::OnRowSelect(wxListEvent& event) { + + boss::File file(string(loadAfterList->GetItemText(event.GetIndex(), 0).ToUTF8())); + boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); + + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + + if (it != _basePlugins.end()) + plugin = *it; + else + BOOST_LOG_TRIVIAL(warning) << "Could not find plugin in base list: " << plugin.Name(); + + set loadAfter = plugin.LoadAfter(); + + if (loadAfter.find(file) == loadAfter.end()) { + BOOST_LOG_TRIVIAL(trace) << "File \"" << file.Name() << "\" was not found in base plugin metadata. Removal enabled."; + removeBtn->Enable(true); + } + else { + BOOST_LOG_TRIVIAL(trace) << "File \"" << file.Name() << "\" was found in base plugin metadata. Removal disabled."; + removeBtn->Enable(false); + } +} + +void MiniEditor::OnRemoveRow(wxCommandEvent& event) { + loadAfterList->DeleteItem(loadAfterList->GetFirstSelected()); + removeBtn->Enable(false); +} + +const std::list& MiniEditor::GetEditedPlugins() const { + return _editedPlugins; +} + +void MiniEditor::ApplyEdits(const wxString& plugin) { + BOOST_LOG_TRIVIAL(debug) << "Applying edits to plugin: " << plugin.ToUTF8(); + + //Get recorded data. + boss::Plugin master; + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), boss::Plugin(string(plugin.ToUTF8()))); + if (it != _basePlugins.end()) + master = *it; + else + master = boss::Plugin(string(plugin.ToUTF8())); + + //Get new data. + boss::Plugin edited = boss::Plugin(string(plugin.ToUTF8())); + edited.Priority(prioritySpin->GetValue()); + set files; + + boss::Plugin diff = master.DiffMetadata(edited); + + list::iterator pos = std::find(_editedPlugins.begin(), _editedPlugins.end(), diff); + + if (pos != _editedPlugins.end()) + *pos = diff; + else + _editedPlugins.push_back(diff); + + //Also mark plugin as edited in list. + long i = pluginList->FindItem(-1, plugin); + if (!diff.HasNameOnly()) + pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + else + pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); + +} + + +Editor::Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const boss::Game& game) : wxFrame(parent, wxID_ANY, title), _userlistPath(userlistPath), _basePlugins(basePlugins), _editedPlugins(editedPlugins), _game(game) { //Initialise child windows. listBook = new wxNotebook(this, BOOK_Lists); @@ -270,12 +428,13 @@ Editor::Editor(wxWindow *parent, const wxString& title, const std::string userli bigBox->Add(mainBox, 2, wxEXPAND|wxTOP|wxBOTTOM|wxRIGHT, 10); //Fill pluginList with the contents of basePlugins. - for (int i=0, max=_basePlugins.size(); i < max; ++i) { - pluginList->InsertItem(i, FromUTF8(_basePlugins[i].Name())); - if (_basePlugins[i].LoadsBSA(_game)) { + int i = 0; + for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + pluginList->InsertItem(i, FromUTF8(it->Name())); + if (it->LoadsBSA(_game)) { pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); } - if (std::find(_editedPlugins.begin(), _editedPlugins.end(), _basePlugins[i]) != _editedPlugins.end()) { + if (std::find(_editedPlugins.begin(), _editedPlugins.end(), *it) != _editedPlugins.end()) { pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); } } @@ -429,7 +588,7 @@ void Editor::OnPluginClearMetadata(wxCommandEvent& event) { //Need to clear what's currently in the editor and what's from the userlist. - vector::const_iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), p); + list::const_iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), p); //Delete existing userlist entry. if (it != _editedPlugins.end()) @@ -678,7 +837,7 @@ void Editor::OnRowSelect(wxListEvent& event) { boss::File file = RowToFile(reqsList, event.GetIndex()); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); if (it != _basePlugins.end()) plugin = *it; @@ -702,7 +861,7 @@ void Editor::OnRowSelect(wxListEvent& event) { boss::File file = RowToFile(incsList, event.GetIndex()); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); if (it != _basePlugins.end()) plugin = *it; @@ -726,7 +885,7 @@ void Editor::OnRowSelect(wxListEvent& event) { boss::File file = RowToFile(loadAfterList, event.GetIndex()); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); if (it != _basePlugins.end()) plugin = *it; @@ -750,7 +909,7 @@ void Editor::OnRowSelect(wxListEvent& event) { boss::Message message = messageList->GetItem(event.GetIndex()); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); if (it != _basePlugins.end()) plugin = *it; @@ -774,7 +933,7 @@ void Editor::OnRowSelect(wxListEvent& event) { boss::Tag tag = RowToTag(tagsList, event.GetIndex()); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); if (it != _basePlugins.end()) plugin = *it; @@ -797,7 +956,7 @@ void Editor::OnRowSelect(wxListEvent& event) { boss::PluginDirtyInfo dirtyData = RowToPluginDirtyInfo(dirtyList, event.GetIndex()); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); if (it != _basePlugins.end()) plugin = *it; @@ -851,7 +1010,7 @@ void Editor::ApplyEdits(const wxString& plugin) { boss::Plugin diff = master.DiffMetadata(edited); - vector::iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), diff); + list::iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), diff); if (it != _editedPlugins.end()) *it = diff; @@ -872,7 +1031,7 @@ boss::Plugin Editor::GetMasterData(const wxString& plugin) const { boss::Plugin p; boss::Plugin p_in(string(plugin.ToUTF8())); - vector::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), p_in); + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), p_in); if (it != _basePlugins.end()) p = *it; @@ -884,7 +1043,7 @@ boss::Plugin Editor::GetUserData(const wxString& plugin) const { BOOST_LOG_TRIVIAL(debug) << "Getting userlist metadata for plugin: " << plugin.ToUTF8(); boss::Plugin p(string(plugin.ToUTF8())); - vector::const_iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), p); + list::const_iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), p); if (it != _editedPlugins.end()) p = *it; @@ -971,386 +1130,3 @@ boss::PluginDirtyInfo Editor::RowToPluginDirtyInfo(wxListView * list, long row) atoi(string(list->GetItemText(row, 3).ToUTF8()).c_str()), string(list->GetItemText(row, 4).ToUTF8())); } - -FileEditDialog::FileEditDialog(wxWindow *parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { - - _name = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_EMPTY)); - _display = new wxTextCtrl(this, wxID_ANY); - _condition = new wxTextCtrl(this, wxID_ANY); - - wxSizerFlags leftItem(0); - leftItem.Left(); - - wxSizerFlags rightItem(1); - rightItem.Right().Expand(); - - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); - GridSizer->AddGrowableCol(1,1); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Filename (required):")), leftItem); - GridSizer->Add(_name, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Displayed Name:")), leftItem); - GridSizer->Add(_display, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Condition:")), leftItem); - GridSizer->Add(_condition, rightItem); - - bigBox->Add(GridSizer, 0, wxEXPAND|wxALL, 15); - - bigBox->AddSpacer(10); - bigBox->AddStretchSpacer(1); - - //Need to add 'OK' and 'Cancel' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxOK|wxCANCEL); - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND|wxLEFT|wxBOTTOM|wxRIGHT, 15); - - SetBackgroundColour(wxColour(255,255,255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} - -void FileEditDialog::SetValues(const wxString& name, const wxString& display, const wxString& condition) { - - _name->SetValue(name); - _display->SetValue(display); - _condition->SetValue(condition); -} - -wxString FileEditDialog::GetName() const { - return _name->GetValue(); -} - -wxString FileEditDialog::GetDisplayName() const { - return _display->GetValue(); -} - -wxString FileEditDialog::GetCondition() const { - return _condition->GetValue(); -} - -MessageEditDialog::MessageEditDialog(wxWindow *parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { - - wxArrayString languages; - vector langs = boss::Language::Names(); - for (size_t i = 0; i < langs.size(); i++) { - languages.Add(FromUTF8(langs[i])); - } - - //Initialise controls. - _type = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, Type); - _language = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, languages); - - _condition = new wxTextCtrl(this, wxID_ANY); - _str = new wxTextCtrl(this, wxID_ANY); - - _content = new wxListView(this, LIST_MessageContent, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxLC_SINGLE_SEL); - - addBtn = new wxButton(this, BUTTON_AddContent, translate("Add Content")); - editBtn = new wxButton(this, BUTTON_EditContent, translate("Edit Content")); - removeBtn = new wxButton(this, BUTTON_RemoveContent, translate("Remove Content")); - - _content->InsertColumn(0, translate("Language")); - _content->InsertColumn(1, translate("String")); - - //Set up event handling. - Bind(wxEVT_BUTTON, &MessageEditDialog::OnAdd, this, BUTTON_AddContent); - Bind(wxEVT_BUTTON, &MessageEditDialog::OnEdit, this, BUTTON_EditContent); - Bind(wxEVT_BUTTON, &MessageEditDialog::OnRemove, this, BUTTON_RemoveContent); - Bind(wxEVT_LIST_ITEM_SELECTED, &MessageEditDialog::OnSelect, this, LIST_MessageContent); - - wxSizerFlags leftItem(0); - leftItem.Left(); - - wxSizerFlags rightItem(1); - rightItem.Right(); - - wxSizerFlags wholeItem(0); - wholeItem.Expand().Border(wxLEFT|wxRIGHT|wxBOTTOM, 15); - - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); - GridSizer->AddGrowableCol(1,1); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Type:")), leftItem); - GridSizer->Add(_type, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Condition:")), leftItem); - GridSizer->Add(_condition, rightItem); - - bigBox->AddSpacer(15); - - bigBox->Add(GridSizer, wholeItem); - - bigBox->Add(_content, wholeItem); - - wxFlexGridSizer * GridSizer2 = new wxFlexGridSizer(2, 5, 5); - GridSizer2->AddGrowableCol(1,1); - - GridSizer2->Add(new wxStaticText(this, wxID_ANY, translate("Language:")), leftItem); - GridSizer2->Add(_language, rightItem); - - GridSizer2->Add(new wxStaticText(this, wxID_ANY, translate("Content:")), leftItem); - GridSizer2->Add(_str, rightItem); - - bigBox->Add(GridSizer2, wholeItem); - - wxBoxSizer * hbox = new wxBoxSizer(wxHORIZONTAL); - hbox->Add(addBtn, 0, wxRIGHT, 5); - hbox->Add(editBtn, 0, wxLEFT|wxRIGHT, 5); - hbox->Add(removeBtn, 0, wxLEFT, 5); - bigBox->Add(hbox, 0, wxALIGN_RIGHT|wxLEFT|wxRIGHT, 15); - - bigBox->AddSpacer(10); - bigBox->AddStretchSpacer(1); - - //Need to add 'OK' and 'Cancel' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxOK|wxCANCEL); - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND|wxLEFT|wxBOTTOM|wxRIGHT, 15); - - //Set defaults. - _type->SetSelection(0); - _language->SetSelection(0); - editBtn->Enable(false); - removeBtn->Enable(false); - - SetBackgroundColour(wxColour(255,255,255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} - -void MessageEditDialog::SetMessage(const boss::Message& message) { - - if (message.Type() == boss::g_message_say) - _type->SetSelection(0); - else if (message.Type() == boss::g_message_warn) - _type->SetSelection(1); - else - _type->SetSelection(2); - - _condition->SetValue(FromUTF8(message.Condition())); - - vector contents = message.Content(); - for (size_t i=0, max=contents.size(); i < max; ++i) { - _content->InsertItem(i, FromUTF8(boss::Language(contents[i].Language()).Name())); - _content->SetItem(i, 1, FromUTF8(contents[i].Str())); - } -} - -boss::Message MessageEditDialog::GetMessage() const { - - unsigned int type; - string condition; - if (_type->GetSelection() == 0) - type = boss::g_message_say; - else if (_type->GetSelection() == 1) - type = boss::g_message_warn; - else - type = boss::g_message_error; - - condition = string(_condition->GetValue().ToUTF8()); - - vector contents; - for (size_t i=0, max=_content->GetItemCount(); i < max; ++i) { - - string str = string(_content->GetItemText(i, 1).ToUTF8()); - unsigned int lang = boss::Language(string(_content->GetItemText(i, 0).ToUTF8())).Code(); - - contents.push_back(boss::MessageContent(str, lang)); - } - - return boss::Message(type, contents, condition); -} - -void MessageEditDialog::OnSelect(wxListEvent& event) { - _language->SetSelection(boss::Language(string(_content->GetItemText(event.GetIndex(), 0).ToUTF8())).Code()); - _str->SetValue(_content->GetItemText(event.GetIndex(), 1)); - editBtn->Enable(true); - removeBtn->Enable(true); -} - -void MessageEditDialog::OnAdd(wxCommandEvent& event) { - long i = _content->GetItemCount(); - _content->InsertItem(i, FromUTF8(boss::Language(_language->GetSelection()).Name())); - _content->SetItem(i, 1, _str->GetValue()); -} - -void MessageEditDialog::OnEdit(wxCommandEvent& event) { - if (_content->GetFirstSelected() == -1) { - BOOST_LOG_TRIVIAL(error) << "Attempting to edit message content, but no content row selected."; - wxMessageBox( - translate("Error: No content row selected."), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - NULL); - return; - } - long i = _content->GetFirstSelected(); - _content->SetItem(i, 0, FromUTF8(boss::Language(_language->GetSelection()).Name())); - _content->SetItem(i, 1, _str->GetValue()); -} - -void MessageEditDialog::OnRemove(wxCommandEvent& event) { - if (_content->GetFirstSelected() == -1) { - BOOST_LOG_TRIVIAL(error) << "Attempting to remove message content, but no content row selected."; - wxMessageBox( - translate("Error: No content row selected."), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - NULL); - return; - } - _content->DeleteItem(_content->GetFirstSelected()); - editBtn->Enable(false); - removeBtn->Enable(false); -} - -TagEditDialog::TagEditDialog(wxWindow *parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { - - //Initialise controls. - _state = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, State); - - _name = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_EMPTY)); - _condition = new wxTextCtrl(this, wxID_ANY); - - wxSizerFlags leftItem(0); - leftItem.Left(); - - wxSizerFlags rightItem(1); - rightItem.Right().Expand(); - - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); - GridSizer->AddGrowableCol(1,1); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Add/Remove:")), leftItem); - GridSizer->Add(_state, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Name (required):")), leftItem); - GridSizer->Add(_name, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Condition:")), leftItem); - GridSizer->Add(_condition, rightItem); - - bigBox->Add(GridSizer, 0, wxEXPAND|wxALL, 15); - - bigBox->AddSpacer(10); - bigBox->AddStretchSpacer(1); - - //Need to add 'OK' and 'Cancel' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxOK|wxCANCEL); - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND|wxLEFT|wxBOTTOM|wxRIGHT, 15); - - //Set defaults. - _state->SetSelection(0); - - SetBackgroundColour(wxColour(255,255,255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} - -void TagEditDialog::SetValues(int state, const wxString& name, const wxString& condition) { - - _state->SetSelection(state); - _name->SetValue(name); - _condition->SetValue(condition); -} - -wxString TagEditDialog::GetState() const { - return State[_state->GetSelection()]; -} - -wxString TagEditDialog::GetName() const { - return _name->GetValue(); -} - -wxString TagEditDialog::GetCondition() const { - return _condition->GetValue(); -} - -DirtInfoEditDialog::DirtInfoEditDialog(wxWindow * parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { - //Initialise controls. - wxTextValidator val(wxFILTER_EMPTY | wxFILTER_INCLUDE_CHAR_LIST); - val.SetCharIncludes("0123456789ABCDEFabcdef"); - - _itm = new wxSpinCtrl(this, wxID_ANY, "0"); - _udr = new wxSpinCtrl(this, wxID_ANY, "0"); - _nav = new wxSpinCtrl(this, wxID_ANY, "0"); - _crc = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, val); - _utility = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_EMPTY)); - - wxSizerFlags leftItem(0); - leftItem.Left(); - - wxSizerFlags rightItem(1); - rightItem.Right().Expand(); - - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); - GridSizer->AddGrowableCol(1,1); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("CRC (required):")), leftItem); - GridSizer->Add(_crc, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("ITM Count:")), leftItem); - GridSizer->Add(_itm, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("UDR Count:")), leftItem); - GridSizer->Add(_udr, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Deleted Navmesh Count:")), leftItem); - GridSizer->Add(_nav, rightItem); - - GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Cleaning Utility (required):")), leftItem); - GridSizer->Add(_utility, rightItem); - - bigBox->Add(GridSizer, 0, wxEXPAND|wxALL, 15); - - bigBox->AddSpacer(10); - bigBox->AddStretchSpacer(1); - - //Need to add 'OK' and 'Cancel' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxOK|wxCANCEL); - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND|wxLEFT|wxBOTTOM|wxRIGHT, 15); - - SetBackgroundColour(wxColour(255,255,255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} - -void DirtInfoEditDialog::SetValues(const wxString& crc, unsigned int itm, unsigned int udr, unsigned int nav, const wxString& utility) { - _crc->SetValue(crc); - _itm->SetValue(itm); - _udr->SetValue(udr); - _nav->SetValue(nav); - _utility->SetValue(utility); -} - -wxString DirtInfoEditDialog::GetCRC() const { - return _crc->GetValue(); -} - -unsigned int DirtInfoEditDialog::GetITMs() const { - return _itm->GetValue(); -} - -unsigned int DirtInfoEditDialog::GetUDRs() const { - return _udr->GetValue(); -} - -unsigned int DirtInfoEditDialog::GetDeletedNavmeshes() const { - return _nav->GetValue(); -} - -wxString DirtInfoEditDialog::GetUtility() const { - return _utility->GetValue(); -} diff --git a/src/gui/editor.h b/src/gui/editor.h index 3d94c292..1d977016 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -24,6 +24,7 @@ #define __BOSS_GUI_EDITOR__ #include "ids.h" +#include "misc.h" #include "../backend/metadata.h" #include @@ -32,29 +33,50 @@ #include #include -class MessageList : public wxListView { +/* Have two versions of the editor: one mini editor that only contains + controls for editing the load order related metadata, and a full editor + that contains controls for editing all the metadata. + + Both editors need to convert between metadata and control data, and to + keep track of what edits have been made. + + Have a immutable plugin list to keep track of non-user-edit metadata, + and a mutable plugin list to keep track of user-edit metadata. When + recording edits, diff the metadata obtained from the control values with + the metadata in the immutable plugin entry to get the new user-edit metadata. + + If a metadata type's corresponding control is not present (ie. in the mini + editor), then use the immutable plugin entry's metadata for that type. +*/ + +class MiniEditor : public wxDialog { public: - MessageList(wxWindow * parent, wxWindowID id, const unsigned int language); + MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game); - void SetItems(const std::vector& messages); - std::vector GetItems() const; - - boss::Message GetItem(long item) const; - void SetItem(long item, const boss::Message& message); - void AppendItem(const boss::Message& message); - - void OnDeleteItem(wxListEvent& event); -protected: - wxString OnGetItemText(long item, long column) const; + void OnPluginSelect(wxListEvent& event); + void OnRowSelect(wxListEvent& event); + void OnRemoveRow(wxCommandEvent& event); + void OnFilterToggle(wxCommandEvent& event); + const std::list& GetEditedPlugins() const; private: - std::vector _messages; - const unsigned int _language; + wxButton * removeBtn; + wxListView * pluginList; + wxListView * loadAfterList; + wxCheckBox * filterCheckbox; + wxSpinCtrl * prioritySpin; + wxStaticText * pluginText; + + void MiniEditor::ApplyEdits(const wxString& plugin); + + const std::list _basePlugins; + std::list _editedPlugins; + const boss::Game& _game; }; class Editor : public wxFrame { public: - Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::vector& basePlugins, std::vector& editedPlugins, const unsigned int language, const boss::Game& game); + Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const boss::Game& game); void OnPluginSelect(wxListEvent& event); void OnPluginListRightClick(wxListEvent& event); @@ -88,10 +110,9 @@ private: wxStaticText * pluginText; const std::string _userlistPath; - const std::vector _basePlugins; + const std::list _basePlugins; + std::list _editedPlugins; const boss::Game& _game; - std::vector _editedPlugins; - std::vector currentMessages; void ApplyEdits(const wxString& plugin); @@ -103,73 +124,4 @@ private: boss::Tag RowToTag(wxListView * list, long row) const; boss::PluginDirtyInfo RowToPluginDirtyInfo(wxListView * list, long row) const; }; - -class FileEditDialog : public wxDialog { -public: - FileEditDialog(wxWindow *parent, const wxString& title); - - void SetValues(const wxString& name, const wxString& display, const wxString& condition); - wxString GetName() const; - wxString GetDisplayName() const; - wxString GetCondition() const; -private: - wxTextCtrl * _name; - wxTextCtrl * _display; - wxTextCtrl * _condition; -}; - -class MessageEditDialog : public wxDialog { -public: - MessageEditDialog(wxWindow *parent, const wxString& title); - - void SetMessage(const boss::Message& message); - boss::Message GetMessage() const; - - void OnSelect(wxListEvent& event); - void OnAdd(wxCommandEvent& event); - void OnEdit(wxCommandEvent& event); - void OnRemove(wxCommandEvent& event); -private: - wxButton * addBtn; - wxButton * editBtn; - wxButton * removeBtn; - wxChoice * _type; - wxChoice * _language; - wxListView * _content; - wxTextCtrl * _condition; - wxTextCtrl * _str; -}; - -class TagEditDialog : public wxDialog { -public: - TagEditDialog(wxWindow *parent, const wxString& title); - - void SetValues(int state, const wxString& name, const wxString& condition); - wxString GetState() const; - wxString GetName() const; - wxString GetCondition() const; -private: - wxChoice * _state; - wxTextCtrl * _name; - wxTextCtrl * _condition; -}; - -class DirtInfoEditDialog : public wxDialog { -public: - DirtInfoEditDialog(wxWindow * parent, const wxString& title); - - void SetValues(const wxString& crc, unsigned int itm, unsigned int udr, unsigned int nav, const wxString& utility); - wxString GetCRC() const; - wxString GetUtility() const; - unsigned int GetITMs() const; - unsigned int GetUDRs() const; - unsigned int GetDeletedNavmeshes() const; -private: - wxTextCtrl * _crc; - wxSpinCtrl * _itm; - wxSpinCtrl * _udr; - wxSpinCtrl * _nav; - wxTextCtrl * _utility; -}; - #endif diff --git a/src/gui/misc.cpp b/src/gui/misc.cpp new file mode 100644 index 00000000..09257cc7 --- /dev/null +++ b/src/gui/misc.cpp @@ -0,0 +1,519 @@ +/* BOSS + +A plugin load order optimiser for games that use the esp/esm plugin system. + +Copyright (C) 2013-2014 WrinklyNinja + +This file is part of BOSS. + +BOSS 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 3 of +the License, or (at your option) any later version. + +BOSS 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 BOSS. If not, see +. +*/ + +#include "misc.h" +#include "../backend/helpers.h" + +#include + + + +using namespace std; + +MessageList::MessageList(wxWindow * parent, wxWindowID id, const unsigned int language) : wxListView(parent, id, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL | wxLC_SINGLE_SEL), _language(language) { + InsertColumn(0, translate("Type")); + InsertColumn(1, translate("Content")); + InsertColumn(2, translate("Condition")); + InsertColumn(3, translate("Language")); + + Bind(wxEVT_LIST_DELETE_ITEM, &MessageList::OnDeleteItem, this); + + SetItemCount(0); +} + +std::vector MessageList::GetItems() const { + return _messages; +} + +void MessageList::SetItems(const std::vector& messages) { + _messages = messages; + SetItemCount(_messages.size()); + RefreshItems(0, _messages.size() - 1); +} + +boss::Message MessageList::GetItem(long item) const { + return _messages[item]; +} + +void MessageList::SetItem(long item, const boss::Message& message) { + _messages[item] = message; + RefreshItem(item); +} + +void MessageList::AppendItem(const boss::Message& message) { + _messages.push_back(message); + SetItemCount(_messages.size()); + RefreshItem(_messages.size() - 1); +} + +void MessageList::OnDeleteItem(wxListEvent& event) { + _messages.erase(_messages.begin() + event.GetIndex()); + SetItemCount(_messages.size()); + RefreshItems(event.GetIndex(), _messages.size() - 1); +} + +wxString MessageList::OnGetItemText(long item, long column) const { + if (column < 0 || column > 3 || item < 0 || item > _messages.size() - 1) + return wxString(); + + if (column == 0) { + if (_messages[item].Type() == boss::g_message_say) + return Type[0]; + else if (_messages[item].Type() == boss::g_message_warn) + return Type[1]; + else + return Type[2]; + } + else if (column == 1) { + return FromUTF8(_messages[item].ChooseContent(_language).Str()); + } + else if (column == 2) { + return FromUTF8(_messages[item].Condition()); + } + else { + return FromUTF8(boss::Language(_messages[item].ChooseContent(_language).Language()).Name()); + } +} + + +FileEditDialog::FileEditDialog(wxWindow *parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + _name = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_EMPTY)); + _display = new wxTextCtrl(this, wxID_ANY); + _condition = new wxTextCtrl(this, wxID_ANY); + + wxSizerFlags leftItem(0); + leftItem.Left(); + + wxSizerFlags rightItem(1); + rightItem.Right().Expand(); + + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); + GridSizer->AddGrowableCol(1, 1); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Filename (required):")), leftItem); + GridSizer->Add(_name, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Displayed Name:")), leftItem); + GridSizer->Add(_display, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Condition:")), leftItem); + GridSizer->Add(_condition, rightItem); + + bigBox->Add(GridSizer, 0, wxEXPAND | wxALL, 15); + + bigBox->AddSpacer(10); + bigBox->AddStretchSpacer(1); + + //Need to add 'OK' and 'Cancel' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxOK | wxCANCEL); + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + SetSizerAndFit(bigBox); +} + +void FileEditDialog::SetValues(const wxString& name, const wxString& display, const wxString& condition) { + + _name->SetValue(name); + _display->SetValue(display); + _condition->SetValue(condition); +} + +wxString FileEditDialog::GetName() const { + return _name->GetValue(); +} + +wxString FileEditDialog::GetDisplayName() const { + return _display->GetValue(); +} + +wxString FileEditDialog::GetCondition() const { + return _condition->GetValue(); +} + +MessageEditDialog::MessageEditDialog(wxWindow *parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + wxArrayString languages; + vector langs = boss::Language::Names(); + for (size_t i = 0; i < langs.size(); i++) { + languages.Add(FromUTF8(langs[i])); + } + + //Initialise controls. + _type = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, Type); + _language = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, languages); + + _condition = new wxTextCtrl(this, wxID_ANY); + _str = new wxTextCtrl(this, wxID_ANY); + + _content = new wxListView(this, LIST_MessageContent, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + + addBtn = new wxButton(this, BUTTON_AddContent, translate("Add Content")); + editBtn = new wxButton(this, BUTTON_EditContent, translate("Edit Content")); + removeBtn = new wxButton(this, BUTTON_RemoveContent, translate("Remove Content")); + + _content->InsertColumn(0, translate("Language")); + _content->InsertColumn(1, translate("String")); + + //Set up event handling. + Bind(wxEVT_BUTTON, &MessageEditDialog::OnAdd, this, BUTTON_AddContent); + Bind(wxEVT_BUTTON, &MessageEditDialog::OnEdit, this, BUTTON_EditContent); + Bind(wxEVT_BUTTON, &MessageEditDialog::OnRemove, this, BUTTON_RemoveContent); + Bind(wxEVT_LIST_ITEM_SELECTED, &MessageEditDialog::OnSelect, this, LIST_MessageContent); + + wxSizerFlags leftItem(0); + leftItem.Left(); + + wxSizerFlags rightItem(1); + rightItem.Right(); + + wxSizerFlags wholeItem(0); + wholeItem.Expand().Border(wxLEFT | wxRIGHT | wxBOTTOM, 15); + + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); + GridSizer->AddGrowableCol(1, 1); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Type:")), leftItem); + GridSizer->Add(_type, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Condition:")), leftItem); + GridSizer->Add(_condition, rightItem); + + bigBox->AddSpacer(15); + + bigBox->Add(GridSizer, wholeItem); + + bigBox->Add(_content, wholeItem); + + wxFlexGridSizer * GridSizer2 = new wxFlexGridSizer(2, 5, 5); + GridSizer2->AddGrowableCol(1, 1); + + GridSizer2->Add(new wxStaticText(this, wxID_ANY, translate("Language:")), leftItem); + GridSizer2->Add(_language, rightItem); + + GridSizer2->Add(new wxStaticText(this, wxID_ANY, translate("Content:")), leftItem); + GridSizer2->Add(_str, rightItem); + + bigBox->Add(GridSizer2, wholeItem); + + wxBoxSizer * hbox = new wxBoxSizer(wxHORIZONTAL); + hbox->Add(addBtn, 0, wxRIGHT, 5); + hbox->Add(editBtn, 0, wxLEFT | wxRIGHT, 5); + hbox->Add(removeBtn, 0, wxLEFT, 5); + bigBox->Add(hbox, 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 15); + + bigBox->AddSpacer(10); + bigBox->AddStretchSpacer(1); + + //Need to add 'OK' and 'Cancel' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxOK | wxCANCEL); + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + //Set defaults. + _type->SetSelection(0); + _language->SetSelection(0); + editBtn->Enable(false); + removeBtn->Enable(false); + + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + SetSizerAndFit(bigBox); +} + +void MessageEditDialog::SetMessage(const boss::Message& message) { + + if (message.Type() == boss::g_message_say) + _type->SetSelection(0); + else if (message.Type() == boss::g_message_warn) + _type->SetSelection(1); + else + _type->SetSelection(2); + + _condition->SetValue(FromUTF8(message.Condition())); + + vector contents = message.Content(); + for (size_t i = 0, max = contents.size(); i < max; ++i) { + _content->InsertItem(i, FromUTF8(boss::Language(contents[i].Language()).Name())); + _content->SetItem(i, 1, FromUTF8(contents[i].Str())); + } +} + +boss::Message MessageEditDialog::GetMessage() const { + + unsigned int type; + string condition; + if (_type->GetSelection() == 0) + type = boss::g_message_say; + else if (_type->GetSelection() == 1) + type = boss::g_message_warn; + else + type = boss::g_message_error; + + condition = string(_condition->GetValue().ToUTF8()); + + vector contents; + for (size_t i = 0, max = _content->GetItemCount(); i < max; ++i) { + + string str = string(_content->GetItemText(i, 1).ToUTF8()); + unsigned int lang = boss::Language(string(_content->GetItemText(i, 0).ToUTF8())).Code(); + + contents.push_back(boss::MessageContent(str, lang)); + } + + return boss::Message(type, contents, condition); +} + +void MessageEditDialog::OnSelect(wxListEvent& event) { + _language->SetSelection(boss::Language(string(_content->GetItemText(event.GetIndex(), 0).ToUTF8())).Code()); + _str->SetValue(_content->GetItemText(event.GetIndex(), 1)); + editBtn->Enable(true); + removeBtn->Enable(true); +} + +void MessageEditDialog::OnAdd(wxCommandEvent& event) { + long i = _content->GetItemCount(); + _content->InsertItem(i, FromUTF8(boss::Language(_language->GetSelection()).Name())); + _content->SetItem(i, 1, _str->GetValue()); +} + +void MessageEditDialog::OnEdit(wxCommandEvent& event) { + if (_content->GetFirstSelected() == -1) { + BOOST_LOG_TRIVIAL(error) << "Attempting to edit message content, but no content row selected."; + wxMessageBox( + translate("Error: No content row selected."), + translate("BOSS: Error"), + wxOK | wxICON_ERROR, + NULL); + return; + } + long i = _content->GetFirstSelected(); + _content->SetItem(i, 0, FromUTF8(boss::Language(_language->GetSelection()).Name())); + _content->SetItem(i, 1, _str->GetValue()); +} + +void MessageEditDialog::OnRemove(wxCommandEvent& event) { + if (_content->GetFirstSelected() == -1) { + BOOST_LOG_TRIVIAL(error) << "Attempting to remove message content, but no content row selected."; + wxMessageBox( + translate("Error: No content row selected."), + translate("BOSS: Error"), + wxOK | wxICON_ERROR, + NULL); + return; + } + _content->DeleteItem(_content->GetFirstSelected()); + editBtn->Enable(false); + removeBtn->Enable(false); +} + +TagEditDialog::TagEditDialog(wxWindow *parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + //Initialise controls. + _state = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, State); + + _name = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_EMPTY)); + _condition = new wxTextCtrl(this, wxID_ANY); + + wxSizerFlags leftItem(0); + leftItem.Left(); + + wxSizerFlags rightItem(1); + rightItem.Right().Expand(); + + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); + GridSizer->AddGrowableCol(1, 1); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Add/Remove:")), leftItem); + GridSizer->Add(_state, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Name (required):")), leftItem); + GridSizer->Add(_name, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Condition:")), leftItem); + GridSizer->Add(_condition, rightItem); + + bigBox->Add(GridSizer, 0, wxEXPAND | wxALL, 15); + + bigBox->AddSpacer(10); + bigBox->AddStretchSpacer(1); + + //Need to add 'OK' and 'Cancel' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxOK | wxCANCEL); + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + //Set defaults. + _state->SetSelection(0); + + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + SetSizerAndFit(bigBox); +} + +void TagEditDialog::SetValues(int state, const wxString& name, const wxString& condition) { + + _state->SetSelection(state); + _name->SetValue(name); + _condition->SetValue(condition); +} + +wxString TagEditDialog::GetState() const { + return State[_state->GetSelection()]; +} + +wxString TagEditDialog::GetName() const { + return _name->GetValue(); +} + +wxString TagEditDialog::GetCondition() const { + return _condition->GetValue(); +} + +DirtInfoEditDialog::DirtInfoEditDialog(wxWindow * parent, const wxString& title) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + //Initialise controls. + wxTextValidator val(wxFILTER_EMPTY | wxFILTER_INCLUDE_CHAR_LIST); + val.SetCharIncludes("0123456789ABCDEFabcdef"); + + _itm = new wxSpinCtrl(this, wxID_ANY, "0"); + _udr = new wxSpinCtrl(this, wxID_ANY, "0"); + _nav = new wxSpinCtrl(this, wxID_ANY, "0"); + _crc = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, val); + _utility = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_EMPTY)); + + wxSizerFlags leftItem(0); + leftItem.Left(); + + wxSizerFlags rightItem(1); + rightItem.Right().Expand(); + + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + wxFlexGridSizer * GridSizer = new wxFlexGridSizer(2, 5, 5); + GridSizer->AddGrowableCol(1, 1); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("CRC (required):")), leftItem); + GridSizer->Add(_crc, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("ITM Count:")), leftItem); + GridSizer->Add(_itm, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("UDR Count:")), leftItem); + GridSizer->Add(_udr, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Deleted Navmesh Count:")), leftItem); + GridSizer->Add(_nav, rightItem); + + GridSizer->Add(new wxStaticText(this, wxID_ANY, translate("Cleaning Utility (required):")), leftItem); + GridSizer->Add(_utility, rightItem); + + bigBox->Add(GridSizer, 0, wxEXPAND | wxALL, 15); + + bigBox->AddSpacer(10); + bigBox->AddStretchSpacer(1); + + //Need to add 'OK' and 'Cancel' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxOK | wxCANCEL); + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + SetSizerAndFit(bigBox); +} + +void DirtInfoEditDialog::SetValues(const wxString& crc, unsigned int itm, unsigned int udr, unsigned int nav, const wxString& utility) { + _crc->SetValue(crc); + _itm->SetValue(itm); + _udr->SetValue(udr); + _nav->SetValue(nav); + _utility->SetValue(utility); +} + +wxString DirtInfoEditDialog::GetCRC() const { + return _crc->GetValue(); +} + +unsigned int DirtInfoEditDialog::GetITMs() const { + return _itm->GetValue(); +} + +unsigned int DirtInfoEditDialog::GetUDRs() const { + return _udr->GetValue(); +} + +unsigned int DirtInfoEditDialog::GetDeletedNavmeshes() const { + return _nav->GetValue(); +} + +wxString DirtInfoEditDialog::GetUtility() const { + return _utility->GetValue(); +} + +LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + //Init controls. + _loadOrder = new wxListView(this, LIST_LoadOrder, wxDefaultPosition, wxDefaultSize, wxLC_REPORT); + + //Populate list. + _loadOrder->AppendColumn(translate("Load Order")); + size_t i = 0; + for (list::const_iterator it = plugins.begin(), endit = plugins.end(); it != endit; ++it, ++i) { + _loadOrder->InsertItem(i, FromUTF8(it->Name())); + if (it->FormIDs().empty()) { + _loadOrder->SetItemTextColour(i, wxColour(122, 122, 122)); + } + else if (it->LoadsBSA(game)) { + _loadOrder->SetItemTextColour(i, wxColour(0, 142, 219)); + } + } + _loadOrder->SetColumnWidth(0, wxLIST_AUTOSIZE); + + //Set up layout. + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + bigBox->Add(_loadOrder, 1, wxEXPAND | wxALL, 15); + + bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you wish to make any changes to the load order above?")), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 15); + + //Need to add 'Yes' and 'No' buttons. + wxSizer * sizer = CreateSeparatedButtonSizer(wxYES | wxNO | wxCANCEL); + + //Now add buttons to window sizer. + if (sizer != NULL) + bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); + + //Now set the layout and sizes. + SetBackgroundColour(wxColour(255, 255, 255)); + SetIcon(wxIconLocation("BOSS.exe")); + SetSizerAndFit(bigBox); +} diff --git a/src/gui/misc.h b/src/gui/misc.h new file mode 100644 index 00000000..03151dfd --- /dev/null +++ b/src/gui/misc.h @@ -0,0 +1,139 @@ +/* BOSS + +A plugin load order optimiser for games that use the esp/esm plugin system. + +Copyright (C) 2013-2014 WrinklyNinja + +This file is part of BOSS. + +BOSS 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 3 of +the License, or (at your option) any later version. + +BOSS 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 BOSS. If not, see +. +*/ +#ifndef __BOSS_GUI_MISC__ +#define __BOSS_GUI_MISC__ + +#include "ids.h" +#include "../backend/metadata.h" + +#include +#include +#include +#include + +const wxString Type[3] = { + translate("Note"), + translate("Warning"), + translate("Error") +}; + +const wxString State[2] = { + translate("Add"), + translate("Remove") +}; + +class MessageList : public wxListView { +public: + MessageList(wxWindow * parent, wxWindowID id, const unsigned int language); + + void SetItems(const std::vector& messages); + std::vector GetItems() const; + + boss::Message GetItem(long item) const; + void SetItem(long item, const boss::Message& message); + void AppendItem(const boss::Message& message); + + void OnDeleteItem(wxListEvent& event); +protected: + wxString OnGetItemText(long item, long column) const; + +private: + std::vector _messages; + const unsigned int _language; +}; + +class FileEditDialog : public wxDialog { +public: + FileEditDialog(wxWindow *parent, const wxString& title); + + void SetValues(const wxString& name, const wxString& display, const wxString& condition); + wxString GetName() const; + wxString GetDisplayName() const; + wxString GetCondition() const; +private: + wxTextCtrl * _name; + wxTextCtrl * _display; + wxTextCtrl * _condition; +}; + +class MessageEditDialog : public wxDialog { +public: + MessageEditDialog(wxWindow *parent, const wxString& title); + + void SetMessage(const boss::Message& message); + boss::Message GetMessage() const; + + void OnSelect(wxListEvent& event); + void OnAdd(wxCommandEvent& event); + void OnEdit(wxCommandEvent& event); + void OnRemove(wxCommandEvent& event); +private: + wxButton * addBtn; + wxButton * editBtn; + wxButton * removeBtn; + wxChoice * _type; + wxChoice * _language; + wxListView * _content; + wxTextCtrl * _condition; + wxTextCtrl * _str; +}; + +class TagEditDialog : public wxDialog { +public: + TagEditDialog(wxWindow *parent, const wxString& title); + + void SetValues(int state, const wxString& name, const wxString& condition); + wxString GetState() const; + wxString GetName() const; + wxString GetCondition() const; +private: + wxChoice * _state; + wxTextCtrl * _name; + wxTextCtrl * _condition; +}; + +class DirtInfoEditDialog : public wxDialog { +public: + DirtInfoEditDialog(wxWindow * parent, const wxString& title); + + void SetValues(const wxString& crc, unsigned int itm, unsigned int udr, unsigned int nav, const wxString& utility); + wxString GetCRC() const; + wxString GetUtility() const; + unsigned int GetITMs() const; + unsigned int GetUDRs() const; + unsigned int GetDeletedNavmeshes() const; +private: + wxTextCtrl * _crc; + wxSpinCtrl * _itm; + wxSpinCtrl * _udr; + wxSpinCtrl * _nav; + wxTextCtrl * _utility; +}; + +class LoadOrderPreview : public wxDialog { +public: + LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game); +private: + wxListView * _loadOrder; +}; +#endif \ No newline at end of file diff --git a/src/gui/sorting.cpp b/src/gui/sorting.cpp deleted file mode 100644 index 4e927111..00000000 --- a/src/gui/sorting.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/* BOSS - -A plugin load order optimiser for games that use the esp/esm plugin system. - -Copyright (C) 2013-2014 WrinklyNinja - -This file is part of BOSS. - -BOSS 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 3 of -the License, or (at your option) any later version. - -BOSS 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 BOSS. If not, see -. -*/ - -#include "sorting.h" -#include "../backend/helpers.h" - -using namespace std; - -MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _basePlugins(plugins) { - //Initialise controls. - pluginText = new wxStaticText(this, wxID_ANY, ""); - prioritySpin = new wxSpinCtrl(this, wxID_ANY, "0"); - prioritySpin->SetRange(std::numeric_limits::min(), std::numeric_limits::max()); - filterCheckbox = new wxCheckBox(this, wxID_ANY, translate("Show only conflicting plugins.")); - - pluginList = new wxListView(this, LIST_Plugins, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); - loadAfterList = new wxListView(this, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); - - removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove")); - - wxMenu * pluginMenu = new wxMenu(); - - //Set up list columns. - pluginList->AppendColumn(translate("Plugin")); - pluginList->AppendColumn(translate("Priority")); - loadAfterList->AppendColumn(translate("Filename")); - - //Set up plugin right-click menu. - pluginMenu->Append(MENU_CopyName, translate("Copy Into Load After List")); - - //Initialise control states. - removeBtn->Enable(false); - prioritySpin->Enable(false); - filterCheckbox->Enable(false); - - //Make plugin name bold text. - pluginText->SetFont(pluginText->GetFont().Bold()); - - //Set up layout. - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); - - hBox->Add(pluginList, 1, wxEXPAND | wxALL, 10); - - wxBoxSizer * mainBox = new wxBoxSizer(wxVERTICAL); - - mainBox->Add(pluginText, 0, wxTOP | wxBOTTOM, 10); - mainBox->Add(filterCheckbox, 0, wxTOP | wxBOTTOM, 10); - - wxBoxSizer * hbox2 = new wxBoxSizer(wxHORIZONTAL); - hbox2->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); - hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); - mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 5); - - mainBox->Add(loadAfterList, 1, wxEXPAND); - mainBox->Add(removeBtn, 0, wxTOP | wxBOTTOM | wxALIGN_RIGHT, 10); - - hBox->Add(mainBox, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); - bigBox->Add(hBox, 1, wxEXPAND); - - //Need to add 'Yes' and 'No' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxAPPLY | wxCANCEL); - - //Now add buttons to window sizer. - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); - - //Fill pluginList with the contents of basePlugins. - int i = 0; - for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { - pluginList->InsertItem(i, FromUTF8(it->Name())); - pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); - if (it->FormIDs().empty()) { - pluginList->SetItemTextColour(i, wxColour(122, 122, 122)); - } - else if (it->LoadsBSA(game)) { - pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); - } - ++i; - } - pluginList->SetColumnWidth(0, wxLIST_AUTOSIZE); - - SetBackgroundColour(wxColour(255, 255, 255)); - SetIcon(wxIconLocation("BOSS.exe")); - - SetSizerAndFit(bigBox); - Layout(); -} - -void MiniEditor::OnPluginSelect(wxListEvent& event) { - -} - -void MiniEditor::OnPluginListRightClick(wxListEvent& event) { - -} - -void MiniEditor::OnPluginCopy(wxCommandEvent& event) { - -} - -void MiniEditor::OnFilterToggle(wxCommandEvent& event) { - -} - -void MiniEditor::OnQuit(wxCommandEvent& event) { - -} - - -LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { - - //Init controls. - _loadOrder = new wxListView(this, LIST_LoadOrder, wxDefaultPosition, wxDefaultSize, wxLC_REPORT); - - //Populate list. - _loadOrder->AppendColumn(translate("Load Order")); - size_t i = 0; - for (list::const_iterator it = plugins.begin(), endit = plugins.end(); it != endit; ++it, ++i) { - _loadOrder->InsertItem(i, FromUTF8(it->Name())); - if (it->FormIDs().empty()) { - _loadOrder->SetItemTextColour(i, wxColour(122, 122, 122)); - } - else if (it->LoadsBSA(game)) { - _loadOrder->SetItemTextColour(i, wxColour(0, 142, 219)); - } - } - _loadOrder->SetColumnWidth(0, wxLIST_AUTOSIZE); - - //Set up layout. - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - bigBox->Add(_loadOrder, 1, wxEXPAND | wxALL, 15); - - bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you wish to make any changes to the load order above?")), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 15); - - //Need to add 'Yes' and 'No' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxYES | wxNO | wxCANCEL); - - //Now add buttons to window sizer. - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); - - //Now set the layout and sizes. - SetBackgroundColour(wxColour(255, 255, 255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} diff --git a/src/gui/sorting.h b/src/gui/sorting.h deleted file mode 100644 index ef0b4871..00000000 --- a/src/gui/sorting.h +++ /dev/null @@ -1,64 +0,0 @@ -/* BOSS - -A plugin load order optimiser for games that use the esp/esm plugin system. - -Copyright (C) 2013-2014 WrinklyNinja - -This file is part of BOSS. - -BOSS 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 3 of -the License, or (at your option) any later version. - -BOSS 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 BOSS. If not, see -. -*/ -#ifndef __BOSS_GUI_SORTING__ -#define __BOSS_GUI_SORTING__ - -#include "ids.h" -#include "../backend/metadata.h" - -#include -#include -#include -#include -#include - -class MiniEditor : public wxDialog { -public: - MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game); - - void OnPluginSelect(wxListEvent& event); - void OnPluginListRightClick(wxListEvent& event); - void OnPluginCopy(wxCommandEvent& event); - void OnFilterToggle(wxCommandEvent& event); - void OnQuit(wxCommandEvent& event); -private: - wxButton * removeBtn; - wxListView * pluginList; - wxListView * loadAfterList; - wxCheckBox * filterCheckbox; - wxSpinCtrl * prioritySpin; - wxStaticText * pluginText; - - - const std::list& _basePlugins; - std::list _editedPlugins; -}; - -class LoadOrderPreview : public wxDialog { -public: - LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game); -private: - wxListView * _loadOrder; -}; - -#endif From 6c361045d8b002d925f54ddac60771a12432588a Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 14:29:37 +0000 Subject: [PATCH 04/22] Editor code refactoring. Also minor edits to MiniEditor and Editor: * MiniEditor now reconstructs plugin metadata using the original metadata for everything that's not got a control. * MiniEditor's Load After control now holds all related metadata, but only the filenames are visible by default to keep things simple. * The Editor and MiniEditor windows are now resized to fit the filename text, and their lists' columns are resized whenever new metadata is loaded into them. --- src/gui/editor.cpp | 297 +++++++++++++++++++++++++-------------------- src/gui/editor.h | 46 +++---- 2 files changed, 189 insertions(+), 154 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 005de5db..2b4d93ec 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -34,15 +34,130 @@ using namespace std; -MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), _basePlugins(plugins), _game(game) { +/////////////////////////////////// +// Common Editor Class +/////////////////////////////////// + +CommonEditor::CommonEditor(const std::list& plugins, const boss::Game& game) : _basePlugins(plugins), _game(game) {} + +CommonEditor::CommonEditor(const std::list& plugins, const boss::Game& game, std::list& editedPlugins) : _basePlugins(plugins), _game(game), _editedPlugins(editedPlugins) {} + +boss::Plugin CommonEditor::GetMasterData(const wxString& plugin) const { + BOOST_LOG_TRIVIAL(debug) << "Getting hardcoded and masterlist metadata for plugin: " << plugin.ToUTF8(); + boss::Plugin p; + boss::Plugin p_in(string(plugin.ToUTF8())); + + list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), p_in); + + if (it != _basePlugins.end()) + p = *it; + + return p; +} + +boss::Plugin CommonEditor::GetUserData(const wxString& plugin) const { + BOOST_LOG_TRIVIAL(debug) << "Getting userlist metadata for plugin: " << plugin.ToUTF8(); + boss::Plugin p(string(plugin.ToUTF8())); + + list::const_iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), p); + + if (it != _editedPlugins.end()) + p = *it; + + return p; +} + +void CommonEditor::ApplyEdits(const wxString& plugin, wxListView * wxList) { + BOOST_LOG_TRIVIAL(debug) << "Applying edits to plugin: " << plugin.ToUTF8(); + + //Get recorded data. + boss::Plugin master(GetMasterData(plugin)); + boss::Plugin edited(GetNewData(plugin)); + + boss::Plugin diff = master.DiffMetadata(edited); + + list::iterator pos = std::find(_editedPlugins.begin(), _editedPlugins.end(), diff); + long i = wxList->FindItem(-1, plugin); + + if (!diff.HasNameOnly()) { + wxList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + if (pos != _editedPlugins.end()) + *pos = diff; + else + _editedPlugins.push_back(diff); + } + else { + wxList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); + if (pos != _editedPlugins.end()) + _editedPlugins.erase(pos); //Prevents unnecessary writes. + } +} + + +boss::File CommonEditor::RowToFile(wxListView * list, long row) const { + return boss::File( + string(list->GetItemText(row, 0).ToUTF8()), + string(list->GetItemText(row, 1).ToUTF8()), + string(list->GetItemText(row, 2).ToUTF8()) + ); +} + +boss::Tag CommonEditor::RowToTag(wxListView * list, long row) const { + string name = string(list->GetItemText(row, 1).ToUTF8()); + + if (list->GetItemText(row, 0) == State[1]) + return boss::Tag( + name, + false, + string(list->GetItemText(row, 2).ToUTF8()) + ); + else + return boss::Tag( + name, + true, + string(list->GetItemText(row, 2).ToUTF8()) + ); +} + +boss::PluginDirtyInfo CommonEditor::RowToPluginDirtyInfo(wxListView * list, long row) const { + string text(list->GetItemText(row, 0).ToUTF8()); + uint32_t crc = strtoul(text.c_str(), NULL, 16); + return boss::PluginDirtyInfo( + crc, + atoi(string(list->GetItemText(row, 1).ToUTF8()).c_str()), + atoi(string(list->GetItemText(row, 2).ToUTF8()).c_str()), + atoi(string(list->GetItemText(row, 3).ToUTF8()).c_str()), + string(list->GetItemText(row, 4).ToUTF8())); +} + + +/////////////////////////////////// +// Mini Editor Class +/////////////////////////////////// + + +MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), CommonEditor(plugins, game) { //Initialise controls. + pluginText = new wxStaticText(this, wxID_ANY, ""); + prioritySpin = new wxSpinCtrl(this, wxID_ANY, "0"); + prioritySpin->SetRange(std::numeric_limits::min(), std::numeric_limits::max()); filterCheckbox = new wxCheckBox(this, wxID_ANY, translate("Show only conflicting plugins.")); + + pluginList = new wxListView(this, LIST_Plugins, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + loadAfterList = new wxListView(this, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + + removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove")); //Set up list columns. pluginList->AppendColumn(translate("Plugin")); pluginList->AppendColumn(translate("Priority")); - loadAfterList->AppendColumn(translate("Load After")); + loadAfterList->AppendColumn(translate("Filename")); + loadAfterList->AppendColumn(translate("Display Name")); + loadAfterList->AppendColumn(translate("Condition")); + loadAfterList->SetColumnWidth(1, 0); //Hide this from the user. + loadAfterList->SetColumnWidth(2, 0); //Hide this from the user. + //Initialise control states. removeBtn->Enable(false); prioritySpin->Enable(false); @@ -74,6 +189,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 10); + //mainBox->Add(new wxStaticText(this, wxID_ANY, translate("Load After")), 0, wxTOP | wxBOTTOM, 10); mainBox->Add(loadAfterList, 1, wxEXPAND); mainBox->Add(removeBtn, 0, wxTOP | wxBOTTOM | wxALIGN_RIGHT, 10); @@ -120,40 +236,36 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { //Apply any current edits. if (!currentPlugin.empty()) - ApplyEdits(currentPlugin); + ApplyEdits(currentPlugin, pluginList); - //Merge base metadata and edits. - boss::Plugin plugin(string(selectedPlugin.ToUTF8())); - list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); - if (it != _basePlugins.end()) - plugin = *it; - else - BOOST_LOG_TRIVIAL(warning) << "Could not find plugin in base list: " << plugin.Name(); - it = std::find(_editedPlugins.begin(), _editedPlugins.end(), plugin); - if (it != _editedPlugins.end()) - plugin.MergeMetadata(*it); - else - BOOST_LOG_TRIVIAL(info) << "Could not find plugin in edited list: " << plugin.Name(); + //Merge metadata. + boss::Plugin plugin = GetMasterData(selectedPlugin); + plugin.MergeMetadata(GetUserData(selectedPlugin)); //Now fill editor fields with new plugin's info and update control states. BOOST_LOG_TRIVIAL(debug) << "Filling editor fields with plugin info."; pluginText->SetLabelText(FromUTF8(plugin.Name())); prioritySpin->SetValue(plugin.Priority()); - loadAfterList->DeleteAllItems(); + loadAfterList->DeleteAllItems(); set files = plugin.LoadAfter(); int i = 0; for (set::const_iterator it = files.begin(), endit = files.end(); it != endit; ++it) { loadAfterList->InsertItem(i, FromUTF8(it->Name())); ++i; } + if (loadAfterList->GetItemCount() == 0) + loadAfterList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + else + loadAfterList->SetColumnWidth(0, wxLIST_AUTOSIZE); //Set control states. prioritySpin->Enable(true); filterCheckbox->Enable(true); removeBtn->Enable(false); } + Fit(); } void MiniEditor::OnFilterToggle(wxCommandEvent& event) { @@ -230,42 +342,24 @@ const std::list& MiniEditor::GetEditedPlugins() const { return _editedPlugins; } -void MiniEditor::ApplyEdits(const wxString& plugin) { - BOOST_LOG_TRIVIAL(debug) << "Applying edits to plugin: " << plugin.ToUTF8(); - - //Get recorded data. - boss::Plugin master; - list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), boss::Plugin(string(plugin.ToUTF8()))); - if (it != _basePlugins.end()) - master = *it; - else - master = boss::Plugin(string(plugin.ToUTF8())); - - //Get new data. - boss::Plugin edited = boss::Plugin(string(plugin.ToUTF8())); +boss::Plugin MiniEditor::GetNewData(const wxString& plugin) const { + //Get new data. Because most of it is missing in the MiniEditor, start with the existing data. + boss::Plugin edited(GetMasterData(plugin)); edited.Priority(prioritySpin->GetValue()); - set files; - - boss::Plugin diff = master.DiffMetadata(edited); - - list::iterator pos = std::find(_editedPlugins.begin(), _editedPlugins.end(), diff); - - if (pos != _editedPlugins.end()) - *pos = diff; - else - _editedPlugins.push_back(diff); - - //Also mark plugin as edited in list. - long i = pluginList->FindItem(-1, plugin); - if (!diff.HasNameOnly()) - pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); - else - pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); + set files; + for (int i = 0, max = loadAfterList->GetItemCount(); i < max; ++i) { + files.insert(RowToFile(loadAfterList, i)); + } + edited.LoadAfter(files); + return edited; } +/////////////////////////////////// +// Editor Class +/////////////////////////////////// -Editor::Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const boss::Game& game) : wxFrame(parent, wxID_ANY, title), _userlistPath(userlistPath), _basePlugins(basePlugins), _editedPlugins(editedPlugins), _game(game) { +Editor::Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const boss::Game& game) : wxFrame(parent, wxID_ANY, title), _userlistPath(userlistPath), CommonEditor(basePlugins, game, editedPlugins) { //Initialise child windows. listBook = new wxNotebook(this, BOOK_Lists); @@ -458,7 +552,7 @@ void Editor::OnPluginSelect(wxListEvent& event) { //Apply any current edits. if (!currentPlugin.empty()) - ApplyEdits(currentPlugin); + ApplyEdits(currentPlugin, pluginList); //Merge metadata. boss::Plugin plugin = GetMasterData(selectedPlugin); @@ -487,6 +581,10 @@ void Editor::OnPluginSelect(wxListEvent& event) { loadAfterList->SetItem(i, 2, FromUTF8(it->Condition())); ++i; } + if (loadAfterList->GetItemCount() == 0) + loadAfterList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + else + loadAfterList->SetColumnWidth(0, wxLIST_AUTOSIZE); files = plugin.Reqs(); i=0; @@ -496,6 +594,10 @@ void Editor::OnPluginSelect(wxListEvent& event) { reqsList->SetItem(i, 2, FromUTF8(it->Condition())); ++i; } + if (reqsList->GetItemCount() == 0) + reqsList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + else + reqsList->SetColumnWidth(0, wxLIST_AUTOSIZE); files = plugin.Incs(); i=0; @@ -505,6 +607,10 @@ void Editor::OnPluginSelect(wxListEvent& event) { incsList->SetItem(i, 2, FromUTF8(it->Condition())); ++i; } + if (incsList->GetItemCount() == 0) + incsList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + else + incsList->SetColumnWidth(0, wxLIST_AUTOSIZE); list messages = plugin.Messages(); vector vec(messages.begin(), messages.end()); @@ -521,6 +627,10 @@ void Editor::OnPluginSelect(wxListEvent& event) { tagsList->SetItem(i, 2, FromUTF8(it->Condition())); ++i; } + if (tagsList->GetItemCount() == 0) + tagsList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + else + tagsList->SetColumnWidth(0, wxLIST_AUTOSIZE); set dirtyInfo = plugin.DirtyInfo(); i=0; @@ -532,6 +642,10 @@ void Editor::OnPluginSelect(wxListEvent& event) { dirtyList->SetItem(i, 4, FromUTF8(it->CleaningUtility())); ++i; } + if (dirtyList->GetItemCount() == 0) + dirtyList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + else + dirtyList->SetColumnWidth(0, wxLIST_AUTOSIZE); //Set control states. prioritySpin->Enable(true); @@ -540,6 +654,7 @@ void Editor::OnPluginSelect(wxListEvent& event) { editBtn->Enable(false); removeBtn->Enable(false); } + Fit(); } void Editor::OnPluginListRightClick(wxListEvent& event) { @@ -984,7 +1099,7 @@ void Editor::OnQuit(wxCommandEvent& event) { //Apply any current edits. wxString currentPlugin = pluginText->GetLabelText(); if (!currentPlugin.empty()) - ApplyEdits(currentPlugin); + ApplyEdits(currentPlugin, pluginList); BOOST_LOG_TRIVIAL(debug) << "Saving metadata edits to userlist."; @@ -1003,54 +1118,6 @@ void Editor::OnQuit(wxCommandEvent& event) { Close(); } -void Editor::ApplyEdits(const wxString& plugin) { - BOOST_LOG_TRIVIAL(debug) << "Applying edits to plugin: " << plugin.ToUTF8(); - boss::Plugin master = GetMasterData(plugin); - boss::Plugin edited = GetNewData(plugin); - - boss::Plugin diff = master.DiffMetadata(edited); - - list::iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), diff); - - if (it != _editedPlugins.end()) - *it = diff; - else - _editedPlugins.push_back(diff); - - //Also mark plugin as edited in list. - long i = pluginList->FindItem(-1, plugin); - if (!diff.HasNameOnly()) - pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); - else - pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); - -} - -boss::Plugin Editor::GetMasterData(const wxString& plugin) const { - BOOST_LOG_TRIVIAL(debug) << "Getting hardcoded and masterlist metadata for plugin: " << plugin.ToUTF8(); - boss::Plugin p; - boss::Plugin p_in(string(plugin.ToUTF8())); - - list::const_iterator it = std::find(_basePlugins.begin(), _basePlugins.end(), p_in); - - if (it != _basePlugins.end()) - p = *it; - - return p; -} - -boss::Plugin Editor::GetUserData(const wxString& plugin) const { - BOOST_LOG_TRIVIAL(debug) << "Getting userlist metadata for plugin: " << plugin.ToUTF8(); - boss::Plugin p(string(plugin.ToUTF8())); - - list::const_iterator it = std::find(_editedPlugins.begin(), _editedPlugins.end(), p); - - if (it != _editedPlugins.end()) - p = *it; - - return p; -} - boss::Plugin Editor::GetNewData(const wxString& plugin) const { BOOST_LOG_TRIVIAL(debug) << "Getting metadata from editor fields for plugin: " << plugin.ToUTF8(); boss::Plugin p(string(plugin.ToUTF8())); @@ -1094,39 +1161,3 @@ boss::Plugin Editor::GetNewData(const wxString& plugin) const { return p; } - -boss::File Editor::RowToFile(wxListView * list, long row) const { - return boss::File( - string(list->GetItemText(row, 0).ToUTF8()), - string(list->GetItemText(row, 1).ToUTF8()), - string(list->GetItemText(row, 2).ToUTF8()) - ); -} - -boss::Tag Editor::RowToTag(wxListView * list, long row) const { - string name = string(list->GetItemText(row, 1).ToUTF8()); - - if (list->GetItemText(row, 0) == State[1]) - return boss::Tag( - name, - false, - string(list->GetItemText(row, 2).ToUTF8()) - ); - else - return boss::Tag( - name, - true, - string(list->GetItemText(row, 2).ToUTF8()) - ); -} - -boss::PluginDirtyInfo Editor::RowToPluginDirtyInfo(wxListView * list, long row) const { - string text(list->GetItemText(row, 0).ToUTF8()); - uint32_t crc = strtoul(text.c_str(), NULL, 16); - return boss::PluginDirtyInfo( - crc, - atoi(string(list->GetItemText(row, 1).ToUTF8()).c_str()), - atoi(string(list->GetItemText(row, 2).ToUTF8()).c_str()), - atoi(string(list->GetItemText(row, 3).ToUTF8()).c_str()), - string(list->GetItemText(row, 4).ToUTF8())); -} diff --git a/src/gui/editor.h b/src/gui/editor.h index 1d977016..87cb08a1 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -28,7 +28,7 @@ #include "../backend/metadata.h" #include -#include +#include #include #include #include @@ -49,7 +49,27 @@ editor), then use the immutable plugin entry's metadata for that type. */ -class MiniEditor : public wxDialog { +class CommonEditor { +public: + CommonEditor(const std::list& plugins, const boss::Game& game); + CommonEditor(const std::list& plugins, const boss::Game& game, std::list& editedPlugins); +protected: + const boss::Game& _game; + const std::list _basePlugins; + std::list _editedPlugins; + + boss::Plugin GetMasterData(const wxString& plugin) const; + boss::Plugin GetUserData(const wxString& plugin) const; + virtual boss::Plugin GetNewData(const wxString& plugin) const = 0; + + void ApplyEdits(const wxString& plugin, wxListView * list); + + boss::File RowToFile(wxListView * list, long row) const; + boss::Tag RowToTag(wxListView * list, long row) const; + boss::PluginDirtyInfo RowToPluginDirtyInfo(wxListView * list, long row) const; +}; + +class MiniEditor : public wxDialog, public CommonEditor { public: MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game); @@ -60,21 +80,17 @@ public: const std::list& GetEditedPlugins() const; private: - wxButton * removeBtn; wxListView * pluginList; + wxButton * removeBtn; wxListView * loadAfterList; wxCheckBox * filterCheckbox; wxSpinCtrl * prioritySpin; wxStaticText * pluginText; - void MiniEditor::ApplyEdits(const wxString& plugin); - - const std::list _basePlugins; - std::list _editedPlugins; - const boss::Game& _game; + boss::Plugin GetNewData(const wxString& plugin) const; }; -class Editor : public wxFrame { +class Editor : public wxFrame, public CommonEditor { public: Editor(wxWindow *parent, const wxString& title, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const boss::Game& game); @@ -90,7 +106,6 @@ public: void OnRowSelect(wxListEvent& event); void OnQuit(wxCommandEvent& event); private: - wxMenu * pluginMenu; wxButton * addBtn; wxButton * editBtn; @@ -110,18 +125,7 @@ private: wxStaticText * pluginText; const std::string _userlistPath; - const std::list _basePlugins; - std::list _editedPlugins; - const boss::Game& _game; - void ApplyEdits(const wxString& plugin); - - boss::Plugin GetMasterData(const wxString& plugin) const; - boss::Plugin GetUserData(const wxString& plugin) const; boss::Plugin GetNewData(const wxString& plugin) const; - - boss::File RowToFile(wxListView * list, long row) const; - boss::Tag RowToTag(wxListView * list, long row) const; - boss::PluginDirtyInfo RowToPluginDirtyInfo(wxListView * list, long row) const; }; #endif From 2df62856ef49d562d17ed6edf7af7296ef95f604 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 15:01:06 +0000 Subject: [PATCH 05/22] MiniEditor tweak. Plugin priorities in the plugins list now get updated when a new plugin is selected. Also fixed bolded entries for plugins that have been edited being unbolded by the filter. --- src/gui/editor.cpp | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 2b4d93ec..eabf2d71 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -235,8 +235,11 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { BOOST_LOG_TRIVIAL(debug) << "User selected plugin: " << selectedPlugin.ToUTF8(); //Apply any current edits. - if (!currentPlugin.empty()) + if (!currentPlugin.empty()) { ApplyEdits(currentPlugin, pluginList); + //Also update the item's priority value in the plugins list in case it has changed. + pluginList->SetItem(pluginList->FindItem(-1, currentPlugin), 1, FromUTF8(boss::IntToString(prioritySpin->GetValue()))); + } //Merge metadata. boss::Plugin plugin = GetMasterData(selectedPlugin); @@ -269,15 +272,26 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { } void MiniEditor::OnFilterToggle(wxCommandEvent& event) { + //First need to merge the base and edited plugin lists so that the right priority values get displayed. + + list plugins; + for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + plugins.push_back(*it); + } + for (list::const_iterator it = _editedPlugins.begin(); it != _editedPlugins.end(); ++it) { + list::iterator pos = std::find(plugins.begin(), plugins.end(), *it); + pos->MergeMetadata(*it); + } + if (event.IsChecked()) { boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); - list::const_iterator pos = std::find(_basePlugins.begin(), _basePlugins.end(), plugin); + list::const_iterator pos = std::find(plugins.begin(), plugins.end(), plugin); - if (pos != _basePlugins.end()) { + if (pos != plugins.end()) { pluginList->DeleteAllItems(); int i = 0; - for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + for (list::const_iterator it = plugins.begin(); it != plugins.end(); ++it) { if (pos->DoFormIDsOverlap(*it)) { pluginList->InsertItem(i, FromUTF8(it->Name())); pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); @@ -287,6 +301,9 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { else if (it->LoadsBSA(_game)) { pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); } + if (std::find(_editedPlugins.begin(), _editedPlugins.end(), *it) != _editedPlugins.end()) { + pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + } ++i; } } @@ -295,7 +312,7 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { else { pluginList->DeleteAllItems(); int i = 0; - for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { + for (list::const_iterator it = plugins.begin(); it != plugins.end(); ++it) { pluginList->InsertItem(i, FromUTF8(it->Name())); pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); if (it->FormIDs().empty()) { @@ -304,9 +321,15 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { else if (it->LoadsBSA(_game)) { pluginList->SetItemTextColour(i, wxColour(0, 142, 219)); } + if (std::find(_editedPlugins.begin(), _editedPlugins.end(), *it) != _editedPlugins.end()) { + pluginList->SetItemFont(i, wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + } ++i; } } + + //Now re-select the current plugin in the list. + pluginList->Select(pluginList->FindItem(-1, pluginText->GetLabelText())); } void MiniEditor::OnRowSelect(wxListEvent& event) { From 8d2d1578d8c4b735fbfa263639ad80fc735281ab Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 18:04:09 +0000 Subject: [PATCH 06/22] #106. Implemented drag 'n' drop in mini editor. --- src/gui/editor.cpp | 49 +++++++++++++++++++++++++++++++++++++++++----- src/gui/editor.h | 13 ++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index eabf2d71..c7061d02 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -34,6 +34,19 @@ using namespace std; +////////////////////////////// +// TextDropTarget class +////////////////////////////// + +TextDropTarget::TextDropTarget(wxListView * owner) { + targetOwner = owner; +} + +bool TextDropTarget::OnDropText(wxCoord x, wxCoord y, const wxString &data) { + targetOwner->InsertItem(targetOwner->GetItemCount(), data); + return true; +} + /////////////////////////////////// // Common Editor Class /////////////////////////////////// @@ -145,8 +158,10 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< pluginList = new wxListView(this, LIST_Plugins, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); loadAfterList = new wxListView(this, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + loadAfterList->SetDropTarget(new TextDropTarget(loadAfterList)); - removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove")); + addBtn = new wxToggleButton(this, BUTTON_AddRow, translate("Add Plugin(s)...")); + removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove Plugin")); //Set up list columns. pluginList->AppendColumn(translate("Plugin")); @@ -159,6 +174,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< loadAfterList->SetColumnWidth(2, 0); //Hide this from the user. //Initialise control states. + addBtn->Enable(false); removeBtn->Enable(false); prioritySpin->Enable(false); filterCheckbox->Enable(false); @@ -168,8 +184,10 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< //Set up event handling. Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); + Bind(wxEVT_LIST_BEGIN_DRAG, &MiniEditor::OnDragStart, this, LIST_Plugins); Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnRowSelect, this, LIST_LoadAfter); Bind(wxEVT_BUTTON, &MiniEditor::OnRemoveRow, this, BUTTON_RemoveRow); + Bind(wxEVT_TOGGLEBUTTON, &MiniEditor::OnAddRowToggle, this, BUTTON_AddRow); Bind(wxEVT_CHECKBOX, &MiniEditor::OnFilterToggle, this); //Set up layout. @@ -187,11 +205,15 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< wxBoxSizer * hbox2 = new wxBoxSizer(wxHORIZONTAL); hbox2->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); - mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP | wxBOTTOM, 10); + mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxBOTTOM, 10); - //mainBox->Add(new wxStaticText(this, wxID_ANY, translate("Load After")), 0, wxTOP | wxBOTTOM, 10); - mainBox->Add(loadAfterList, 1, wxEXPAND); - mainBox->Add(removeBtn, 0, wxTOP | wxBOTTOM | wxALIGN_RIGHT, 10); + wxStaticBoxSizer * staticBox = new wxStaticBoxSizer(wxVERTICAL, this, translate("Load After")); + staticBox->Add(loadAfterList, 1, wxEXPAND); + wxBoxSizer * hbox3 = new wxBoxSizer(wxHORIZONTAL); + hbox3->Add(addBtn, 0, wxRIGHT, 10); + hbox3->Add(removeBtn); + staticBox->Add(hbox3, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP, 10); + mainBox->Add(staticBox, 1, wxEXPAND); hBox->Add(mainBox, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); bigBox->Add(hBox, 1, wxEXPAND | wxALL, 5); @@ -266,6 +288,7 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { //Set control states. prioritySpin->Enable(true); filterCheckbox->Enable(true); + addBtn->Enable(true); removeBtn->Enable(false); } Fit(); @@ -356,11 +379,27 @@ void MiniEditor::OnRowSelect(wxListEvent& event) { } } +void MiniEditor::OnAddRowToggle(wxCommandEvent& event) { + //This exists because drag 'n' drop selects plugins when they're clicked on, and we + //don't want them being loaded. + if (event.IsChecked()) + Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); + else + Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); +} + void MiniEditor::OnRemoveRow(wxCommandEvent& event) { loadAfterList->DeleteItem(loadAfterList->GetFirstSelected()); removeBtn->Enable(false); } +void MiniEditor::OnDragStart(wxListEvent& event) { + wxTextDataObject data(pluginList->GetItemText(event.GetItem())); + wxDropSource dropSource(pluginList); + dropSource.SetData(data); + wxDragResult result = dropSource.DoDragDrop(); +} + const std::list& MiniEditor::GetEditedPlugins() const { return _editedPlugins; } diff --git a/src/gui/editor.h b/src/gui/editor.h index 87cb08a1..35f3e9cb 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -32,6 +32,8 @@ #include #include #include +#include +#include /* Have two versions of the editor: one mini editor that only contains controls for editing the load order related metadata, and a full editor @@ -49,6 +51,14 @@ editor), then use the immutable plugin entry's metadata for that type. */ +class TextDropTarget : public wxTextDropTarget { //Class to override virtual functions. +public: + TextDropTarget(wxListView * owner); + virtual bool OnDropText(wxCoord x, wxCoord y, const wxString &data); +private: + wxListView *targetOwner; +}; + class CommonEditor { public: CommonEditor(const std::list& plugins, const boss::Game& game); @@ -76,11 +86,14 @@ public: void OnPluginSelect(wxListEvent& event); void OnRowSelect(wxListEvent& event); void OnRemoveRow(wxCommandEvent& event); + void OnAddRowToggle(wxCommandEvent& event); void OnFilterToggle(wxCommandEvent& event); + void OnDragStart(wxListEvent& event); const std::list& GetEditedPlugins() const; private: wxListView * pluginList; + wxToggleButton * addBtn; wxButton * removeBtn; wxListView * loadAfterList; wxCheckBox * filterCheckbox; From c0efb5e431e9c652c39680a068a11c577e81ee61 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 18:45:26 +0000 Subject: [PATCH 07/22] Moved some of the launcher functions around. --- src/gui/main.cpp | 196 ++++++++++++++++++++++++----------------------- 1 file changed, 100 insertions(+), 96 deletions(-) diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 0e3d692e..b3cd3c40 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -488,6 +488,106 @@ void Launcher::OnClose(wxCloseEvent& event) { Destroy(); } +void Launcher::OnViewLastReport(wxCommandEvent& event) { + if (_settings["View Report Externally"] && _settings["View Report Externally"].as()) { + BOOST_LOG_TRIVIAL(debug) << "Opening report in external application..."; + wxLaunchDefaultBrowser(FromUTF8(ToFileURL(_game.ReportPath().string()))); + } + else { + //Create viewer window. + BOOST_LOG_TRIVIAL(debug) << "Opening viewer window..."; + Viewer *viewer = new Viewer(this, translate("BOSS: Report Viewer"), FromUTF8(ToFileURL(_game.ReportPath().string()))); + viewer->Show(); + } + BOOST_LOG_TRIVIAL(debug) << "Report displayed."; +} + +void Launcher::OnOpenSettings(wxCommandEvent& event) { + BOOST_LOG_TRIVIAL(debug) << "Opening settings window..."; + SettingsFrame *settings = new SettingsFrame(this, translate("BOSS: Settings"), _settings, _games); + settings->ShowModal(); + BOOST_LOG_TRIVIAL(debug) << "Settings window opened."; +} + +void Launcher::OnGameChange(wxCommandEvent& event) { + BOOST_LOG_TRIVIAL(debug) << "Changing current game..."; + _game = _games[event.GetId() - MENU_LowestDynamicGameID]; + try { + _game.Init(); //In case it hasn't already been done. + *find(_games.begin(), _games.end(), _game) = _game; //Sync changes. + BOOST_LOG_TRIVIAL(debug) << "New game is " << _game.Name(); + } + catch (boss::error& e) { + BOOST_LOG_TRIVIAL(error) << "Game-specific settings could not be initialised." << e.what(); + wxMessageBox( + FromUTF8(format(loc::translate("Error: Game-specific settings could not be initialised. %1%")) % e.what()), + translate("BOSS: Error"), + wxOK | wxICON_ERROR, + NULL); + } + SetTitle(FromUTF8("BOSS - " + _game.Name())); + if (_game.Id() == boss::g_game_tes5) + RedatePluginsItem->Enable(true); + else + RedatePluginsItem->Enable(false); +} + +void Launcher::OnHelp(wxCommandEvent& event) { + //Look for file. + BOOST_LOG_TRIVIAL(debug) << "Opening readme at: " << g_path_readme; + if (fs::exists(g_path_readme)) { + wxLaunchDefaultBrowser(FromUTF8(ToFileURL(g_path_readme.string()))); + } + else { //No readme exists, show a pop-up message saying so. + BOOST_LOG_TRIVIAL(error) << "File \"" << g_path_readme.string() << "\" could not be found."; + wxMessageBox( + FromUTF8(format(loc::translate("Error: \"%1%\" cannot be found.")) % g_path_readme.string()), + translate("BOSS: Error"), + wxOK | wxICON_ERROR, + this); + } +} + +void Launcher::OnOpenDebugLog(wxCommandEvent& event) { + //Look for file. + BOOST_LOG_TRIVIAL(debug) << "Opening debug log at: " << g_path_log; + if (fs::exists(g_path_log)) { + wxLaunchDefaultApplication(FromUTF8(g_path_log.string())); + } + else { //No log exists, show a pop-up message saying so. + BOOST_LOG_TRIVIAL(error) << "File \"" << g_path_log.string() << "\" could not be found."; + wxMessageBox( + FromUTF8(format(loc::translate("Error: \"%1%\" cannot be found.")) % g_path_log.string()), + translate("BOSS: Error"), + wxOK | wxICON_ERROR, + this); + } +} + +void Launcher::OnAbout(wxCommandEvent& event) { + BOOST_LOG_TRIVIAL(debug) << "Opening About dialog."; + wxAboutDialogInfo aboutInfo; + aboutInfo.SetName("BOSS"); + aboutInfo.SetVersion(IntToString(g_version_major) + "." + IntToString(g_version_minor) + "." + IntToString(g_version_patch)); + aboutInfo.SetDescription(translate("Load order optimisation for Oblivion, Skyrim, Fallout 3 and Fallout: New Vegas.")); + aboutInfo.SetCopyright("Copyright (C) 2009-2014 BOSS Development Team."); + aboutInfo.SetWebSite("http://boss-developers.github.io"); + aboutInfo.SetLicence("This program is free software: you can redistribute it and/or modify\n" + "it under the terms of the GNU General Public License as published by\n" + "the Free Software Foundation, either version 3 of the License, or\n" + "(at your option) any later version.\n" + "\n" + "This program is distributed in the hope that it will be useful,\n" + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + "GNU General Public License for more details.\n" + "\n" + "You should have received a copy of the GNU General Public License\n" + "along with this program. If not, see ."); + aboutInfo.SetIcon(wxIconLocation("BOSS.exe")); + wxAboutBox(aboutInfo); +} + void Launcher::OnSortPlugins(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Beginning sorting process."; @@ -938,19 +1038,6 @@ void Launcher::OnEditMetadata(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Editor window opened."; } -void Launcher::OnViewLastReport(wxCommandEvent& event) { - if (_settings["View Report Externally"] && _settings["View Report Externally"].as()) { - BOOST_LOG_TRIVIAL(debug) << "Opening report in external application..."; - wxLaunchDefaultBrowser(FromUTF8(ToFileURL(_game.ReportPath().string()))); - } else { - //Create viewer window. - BOOST_LOG_TRIVIAL(debug) << "Opening viewer window..."; - Viewer *viewer = new Viewer(this, translate("BOSS: Report Viewer"), FromUTF8(ToFileURL(_game.ReportPath().string()))); - viewer->Show(); - } - BOOST_LOG_TRIVIAL(debug) << "Report displayed."; -} - void Launcher::OnRedatePlugins(wxCommandEvent& event) { wxMessageDialog * dia = new wxMessageDialog(this, translate("This feature is provided so that modders using the Creation Kit may set the load order it uses. A side-effect is that any subscribed Steam Workshop mods will be re-downloaded by Steam. Do you wish to continue?"), translate("BOSS: Warning"), wxYES_NO|wxCANCEL|wxICON_EXCLAMATION); @@ -1012,86 +1099,3 @@ void Launcher::OnRedatePlugins(wxCommandEvent& event) { this); } } - -void Launcher::OnOpenSettings(wxCommandEvent& event) { - BOOST_LOG_TRIVIAL(debug) << "Opening settings window..."; - SettingsFrame *settings = new SettingsFrame(this, translate("BOSS: Settings"), _settings, _games); - settings->ShowModal(); - BOOST_LOG_TRIVIAL(debug) << "Settings window opened."; -} - -void Launcher::OnGameChange(wxCommandEvent& event) { - BOOST_LOG_TRIVIAL(debug) << "Changing current game..."; - _game = _games[event.GetId() - MENU_LowestDynamicGameID]; - try { - _game.Init(); //In case it hasn't already been done. - *find(_games.begin(), _games.end(), _game) = _game; //Sync changes. - BOOST_LOG_TRIVIAL(debug) << "New game is " << _game.Name(); - } catch (boss::error& e) { - BOOST_LOG_TRIVIAL(error) << "Game-specific settings could not be initialised." << e.what(); - wxMessageBox( - FromUTF8(format(loc::translate("Error: Game-specific settings could not be initialised. %1%")) % e.what()), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - NULL); - } - SetTitle(FromUTF8("BOSS - " + _game.Name())); - if (_game.Id() == boss::g_game_tes5) - RedatePluginsItem->Enable(true); - else - RedatePluginsItem->Enable(false); -} - -void Launcher::OnHelp(wxCommandEvent& event) { - //Look for file. - BOOST_LOG_TRIVIAL(debug) << "Opening readme at: " << g_path_readme; - if (fs::exists(g_path_readme)) { - wxLaunchDefaultBrowser(FromUTF8(ToFileURL(g_path_readme.string()))); - } else { //No readme exists, show a pop-up message saying so. - BOOST_LOG_TRIVIAL(error) << "File \"" << g_path_readme.string() << "\" could not be found."; - wxMessageBox( - FromUTF8(format(loc::translate("Error: \"%1%\" cannot be found.")) % g_path_readme.string()), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - this); - } -} - -void Launcher::OnOpenDebugLog(wxCommandEvent& event) { - //Look for file. - BOOST_LOG_TRIVIAL(debug) << "Opening debug log at: " << g_path_log; - if (fs::exists(g_path_log)) { - wxLaunchDefaultApplication(FromUTF8(g_path_log.string())); - } else { //No log exists, show a pop-up message saying so. - BOOST_LOG_TRIVIAL(error) << "File \"" << g_path_log.string() << "\" could not be found."; - wxMessageBox( - FromUTF8(format(loc::translate("Error: \"%1%\" cannot be found.")) % g_path_log.string()), - translate("BOSS: Error"), - wxOK | wxICON_ERROR, - this); - } -} - -void Launcher::OnAbout(wxCommandEvent& event) { - BOOST_LOG_TRIVIAL(debug) << "Opening About dialog."; - wxAboutDialogInfo aboutInfo; - aboutInfo.SetName("BOSS"); - aboutInfo.SetVersion(IntToString(g_version_major)+"."+IntToString(g_version_minor)+"."+IntToString(g_version_patch)); - aboutInfo.SetDescription(translate("Load order optimisation for Oblivion, Skyrim, Fallout 3 and Fallout: New Vegas.")); - aboutInfo.SetCopyright("Copyright (C) 2009-2014 BOSS Development Team."); - aboutInfo.SetWebSite("http://boss-developers.github.io"); - aboutInfo.SetLicence("This program is free software: you can redistribute it and/or modify\n" - "it under the terms of the GNU General Public License as published by\n" - "the Free Software Foundation, either version 3 of the License, or\n" - "(at your option) any later version.\n" - "\n" - "This program is distributed in the hope that it will be useful,\n" - "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" - "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" - "GNU General Public License for more details.\n" - "\n" - "You should have received a copy of the GNU General Public License\n" - "along with this program. If not, see ."); - aboutInfo.SetIcon(wxIconLocation("BOSS.exe")); - wxAboutBox(aboutInfo); -} \ No newline at end of file From bd2d39d9e51c71b05df57819ba0dc4fbee9ef38a Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 18:56:31 +0000 Subject: [PATCH 08/22] Updated metadata editor launching code. Now uses lists instead of vectors, as required. --- src/gui/main.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/gui/main.cpp b/src/gui/main.cpp index b3cd3c40..4f566fce 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -24,7 +24,7 @@ #include "settings.h" #include "editor.h" #include "viewer.h" -#include "sorting.h" +#include "misc.h" #include "../backend/globals.h" #include "../backend/metadata.h" @@ -936,7 +936,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { void Launcher::OnEditMetadata(wxCommandEvent& event) { //Should probably check for masterlist updates before opening metadata editor. - vector installed, mlist_plugins, ulist_plugins; + list installed, mlist_plugins, ulist_plugins; wxProgressDialog *progDia = new wxProgressDialog(translate("BOSS: Working..."),translate("BOSS working..."), 1000, this, wxPD_APP_MODAL|wxPD_AUTO_HIDE|wxPD_ELAPSED_TIME); @@ -967,7 +967,7 @@ void Launcher::OnEditMetadata(wxCommandEvent& event) { this); } if (mlist["plugins"]) - mlist_plugins = mlist["plugins"].as< vector >(); + mlist_plugins = mlist["plugins"].as< list >(); } progDia->Pulse(); @@ -989,26 +989,25 @@ void Launcher::OnEditMetadata(wxCommandEvent& event) { this); } if (ulist["plugins"]) - ulist_plugins = ulist["plugins"].as< vector >(); + ulist_plugins = ulist["plugins"].as< list >(); } progDia->Pulse(); //Merge the masterlist down into the installed mods list. BOOST_LOG_TRIVIAL(debug) << "Merging the masterlist down into the installed mods list."; - for (vector::const_iterator it=mlist_plugins.begin(), endit=mlist_plugins.end(); it != endit; ++it) { - vector::iterator pos = find(installed.begin(), installed.end(), *it); + for (list::const_iterator it = mlist_plugins.begin(), endit = mlist_plugins.end(); it != endit; ++it) { + list::iterator pos = find(installed.begin(), installed.end(), *it); if (pos != installed.end()) pos->MergeMetadata(*it); } - progDia->Pulse(); //Add empty entries for any userlist entries that aren't installed. - BOOST_LOG_TRIVIAL(debug) << "Padding the userlist to match the plugins in the installed mods list."; - for (vector::const_iterator it=ulist_plugins.begin(), endit=ulist_plugins.end(); it != endit; ++it) { + BOOST_LOG_TRIVIAL(debug) << "Padding the installed mods list to match the plugins in the userlist."; + for (list::const_iterator it = ulist_plugins.begin(), endit = ulist_plugins.end(); it != endit; ++it) { if (find(installed.begin(), installed.end(), *it) == installed.end()) installed.push_back(boss::Plugin(it->Name())); } @@ -1017,7 +1016,7 @@ void Launcher::OnEditMetadata(wxCommandEvent& event) { //Sort into alphabetical order. BOOST_LOG_TRIVIAL(debug) << "Sorting plugin list into alphabetical order."; - std::sort(installed.begin(), installed.end(), boss::alpha_sort); + installed.sort(boss::alpha_sort); progDia->Pulse(); From e15ddfd487bdc78ce3b1b8a8596783e2798dd14c Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 20:27:44 +0000 Subject: [PATCH 09/22] Fixed new dialogs not closing. --- src/gui/editor.cpp | 10 ++++++++++ src/gui/editor.h | 1 + src/gui/misc.cpp | 8 ++++++++ src/gui/misc.h | 2 ++ 4 files changed, 21 insertions(+) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index c7061d02..160af435 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -189,6 +189,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< Bind(wxEVT_BUTTON, &MiniEditor::OnRemoveRow, this, BUTTON_RemoveRow); Bind(wxEVT_TOGGLEBUTTON, &MiniEditor::OnAddRowToggle, this, BUTTON_AddRow); Bind(wxEVT_CHECKBOX, &MiniEditor::OnFilterToggle, this); + Bind(wxEVT_BUTTON, &MiniEditor::OnApply, this, wxID_APPLY); //Set up layout. wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); @@ -400,6 +401,15 @@ void MiniEditor::OnDragStart(wxListEvent& event) { wxDragResult result = dropSource.DoDragDrop(); } +void MiniEditor::OnApply(wxCommandEvent& event) { + //Apply any current edits. + wxString currentPlugin = pluginText->GetLabelText(); + if (!currentPlugin.empty()) + ApplyEdits(currentPlugin, pluginList); + + EndModal(event.GetId()); +} + const std::list& MiniEditor::GetEditedPlugins() const { return _editedPlugins; } diff --git a/src/gui/editor.h b/src/gui/editor.h index 35f3e9cb..f3af5f01 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -89,6 +89,7 @@ public: void OnAddRowToggle(wxCommandEvent& event); void OnFilterToggle(wxCommandEvent& event); void OnDragStart(wxListEvent& event); + void OnApply(wxCommandEvent& event); const std::list& GetEditedPlugins() const; private: diff --git a/src/gui/misc.cpp b/src/gui/misc.cpp index 09257cc7..ee8af661 100644 --- a/src/gui/misc.cpp +++ b/src/gui/misc.cpp @@ -498,6 +498,10 @@ LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const } _loadOrder->SetColumnWidth(0, wxLIST_AUTOSIZE); + //Set up event handling. + Bind(wxEVT_BUTTON, &LoadOrderPreview::OnYesNo, this, wxID_YES); + Bind(wxEVT_BUTTON, &LoadOrderPreview::OnYesNo, this, wxID_NO); + //Set up layout. wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); @@ -517,3 +521,7 @@ LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const SetIcon(wxIconLocation("BOSS.exe")); SetSizerAndFit(bigBox); } + +void LoadOrderPreview::OnYesNo(wxCommandEvent& event) { + EndModal(event.GetId()); +} \ No newline at end of file diff --git a/src/gui/misc.h b/src/gui/misc.h index 03151dfd..2b5ff4e8 100644 --- a/src/gui/misc.h +++ b/src/gui/misc.h @@ -133,6 +133,8 @@ private: class LoadOrderPreview : public wxDialog { public: LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game); + + void OnYesNo(wxCommandEvent& event); private: wxListView * _loadOrder; }; From 03ccbbe8c80c263668ed9d67e3163bd083030960 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 20:28:46 +0000 Subject: [PATCH 10/22] Started to implement changes to sorting. Haven't got the loop or the edge deletion in yet, but everything else is there, I think. --- src/gui/main.cpp | 152 +++++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 65 deletions(-) diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 4f566fce..9b0f81a8 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -602,6 +602,10 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { wxProgressDialog *progDia = new wxProgressDialog(translate("BOSS: Working..."),translate("BOSS working..."), 1000, this, wxPD_APP_MODAL|wxPD_AUTO_HIDE|wxPD_ELAPSED_TIME); + /////////////////////////////////////////////////////// + // Load Plugins & Lists + /////////////////////////////////////////////////////// + bool doUpdate = _settings["Update Masterlist"] && _settings["Update Masterlist"].as(); masterlist_updater_parser mup(doUpdate, _game, messages, mlist_plugins, mlist_messages, revision); group.create_thread(mup); @@ -661,6 +665,10 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { progDia->Pulse(); + /////////////////////////////////////////////////////// + // Merge & Check Metadata + /////////////////////////////////////////////////////// + if (fs::exists(_game.MasterlistPath()) || fs::exists(_game.UserlistPath())) { //Set language. @@ -750,6 +758,20 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { progDia->Pulse(); + /////////////////////////////////////////////////////// + // Build Graph Edges & Sort + /////////////////////////////////////////////////////// + + /* Need to loop this section. There are 3 ways to exit the loop: + + 1. Accept the load order at the preview with no changes. + 2. Cancel at the preview. + 3. Cancel making changes. + + Otherwise, the sorting must loop. + + */ + BOOST_LOG_TRIVIAL(debug) << "Building the plugin dependency graph..."; //Now add the interactions between plugins to the graph as edges. @@ -778,93 +800,89 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Displaying load order preview."; LoadOrderPreview preview(this, translate("BOSS: Calculated Load Order"), plugins, _game); - if (preview.ShowModal() == wxID_OK) { - BOOST_LOG_TRIVIAL(debug) << "Load order accepted."; - list newPluginsList, editedPlugins; - /*newPluginsList = preview.GetLoadOrder(); + long ret = preview.ShowModal(); - BOOST_LOG_TRIVIAL(trace) << "Building list of edited plugins."; - for (list::iterator it=newPluginsList.begin(),endit=newPluginsList.end(); it != endit; ++it) { - list::const_iterator jt = find(plugins.begin(), plugins.end(), *it); + if (ret == wxID_YES) { + preview.Close(); + //User wants to make changes. Need to display editor and loop around again. - if (jt == plugins.end()) - continue; + MiniEditor editor(this, translate("BOSS: Mini Editor"), plugins, _game); - boss::Plugin diff = it->DiffMetadata(*jt); + ret = editor.ShowModal(); - if (!diff.HasNameOnly()) - editedPlugins.push_back(diff); - } - plugins.swap(newPluginsList); + if (ret == wxID_APPLY) { + //User accepted edits, now apply them, then loop. + const std::list edits = editor.GetEditedPlugins(); - if (!editedPlugins.empty()) { + if (!edits.empty()) { + //Apply edits to the graph vertices. + boss::vertex_it vit, vitend; + for (boost::tie(vit, vitend) = boost::vertices(graph); vit != vitend; ++vit) { + std::list::const_iterator pos = std::find(edits.begin(), edits.end(), graph[*vit]); - BOOST_LOG_TRIVIAL(trace) << "Merging down edits to the userlist."; - for (list::const_iterator it=editedPlugins.begin(),endit=editedPlugins.end(); it != endit; ++it) { - list::iterator jt = find(ulist_plugins.begin(), ulist_plugins.end(), *it); - - if (jt != ulist_plugins.end()) { - jt->MergeMetadata(*it); - } else { - ulist_plugins.push_back(*it); + if (pos != edits.end()) { + BOOST_LOG_TRIVIAL(trace) << "Merging edits down to plugin list data."; + graph[*vit].MergeMetadata(*pos); + } } - } - BOOST_LOG_TRIVIAL(trace) << "Checking that any pre-existing 'load after' entries in the userlist are still valid."; - for (list::iterator it=ulist_plugins.begin(),endit=ulist_plugins.end(); it != endit; ++it) { - //Double-check that the plugin is still loading after any other plugins already in its "load after" metadata. - list::iterator mainPos = find(plugins.begin(), plugins.end(), *it); + //Clear all existing edges from the graph. - if (mainPos == plugins.end()) - continue; + //Save edits to the userlist. + BOOST_LOG_TRIVIAL(trace) << "Merging down edits to the userlist."; + for (list::const_iterator it = edits.begin(), endit = edits.end(); it != endit; ++it) { + list::iterator jt = find(ulist_plugins.begin(), ulist_plugins.end(), *it); - size_t mainDist = distance(plugins.begin(), mainPos); - - set loadAfter = it->LoadAfter(); - set::iterator jt = loadAfter.begin(); - while (jt != loadAfter.end()) { - list::iterator filePos = find(plugins.begin(), plugins.end(), *jt); - - size_t fileDist = distance(plugins.begin(), filePos); - - if (fileDist > mainDist) - loadAfter.erase(jt++); - else - ++jt; + if (jt != ulist_plugins.end()) { + jt->MergeMetadata(*it); + } + else { + ulist_plugins.push_back(*it); + } } - it->LoadAfter(loadAfter); + + //Save edits to userlist. + BOOST_LOG_TRIVIAL(debug) << "Saving edited userlist."; + YAML::Emitter yout; + yout.SetIndent(2); + yout << YAML::BeginMap + << YAML::Key << "plugins" << YAML::Value << ulist_plugins + << YAML::EndMap; + + boss::ofstream uout(_game.UserlistPath()); + uout << yout.c_str(); + uout.close(); + + //Now loop. } - - //Save edits to userlist. - BOOST_LOG_TRIVIAL(debug) << "Saving edited userlist."; - YAML::Emitter yout; - yout.SetIndent(2); - yout << YAML::BeginMap - << YAML::Key << "plugins" << YAML::Value << ulist_plugins - << YAML::EndMap; - - boss::ofstream uout(_game.UserlistPath()); - uout << yout.c_str(); - uout.close(); } - */ - //Now set load order. + else { + //User decided to cancel editing. Just go straight to displaying the report. + BOOST_LOG_TRIVIAL(info) << "The load order calculated was not applied as sorting was canceled."; + messages.push_back(boss::Message(boss::g_message_warn, loc::translate("The load order displayed in the Details tab was not applied as sorting was canceled."))); + } + } + else if (ret == wxID_NO) { + preview.Close(); + //User doesn't want to make any changes. Just go straight to applying the load order. BOOST_LOG_TRIVIAL(debug) << "Setting load order."; try { _game.SetLoadOrder(plugins); - } catch (boss::error& e) { + } + catch (boss::error& e) { BOOST_LOG_TRIVIAL(error) << "Failed to set the load order. Details: " << e.what(); messages.push_back(boss::Message(boss::g_message_error, (format(loc::translate("Failed to set the load order. Details: %1%")) % e.what()).str())); } - } else { + BOOST_LOG_TRIVIAL(trace) << "Load order set:"; + for (list::iterator it = plugins.begin(), endIt = plugins.end(); it != endIt; ++it) { + BOOST_LOG_TRIVIAL(trace) << '\t' << it->Name(); + } + } + else { + //User decided to cancel sorting. Just go straight to displaying the report. BOOST_LOG_TRIVIAL(info) << "The load order calculated was not applied as sorting was canceled."; messages.push_back(boss::Message(boss::g_message_warn, loc::translate("The load order displayed in the Details tab was not applied as sorting was canceled."))); } - - BOOST_LOG_TRIVIAL(trace) << "Load order set:"; - for (list::iterator it=plugins.begin(), endIt = plugins.end(); it != endIt; ++it) { - BOOST_LOG_TRIVIAL(trace) << '\t' << it->Name(); - } } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to calculate the load order. Details: " << e.what(); messages.push_back(boss::Message(boss::g_message_error, (format(loc::translate("Failed to calculate the load order. Details: %1%")) % e.what()).str())); @@ -873,6 +891,10 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { progDia = NULL; } + /////////////////////////////////////////////////////// + // Build & Display Report + /////////////////////////////////////////////////////// + //Read the details section of the previous report, if it exists. string oldDetails; if (fs::exists(_game.ReportPath().string())) { From fd2799c215d0a09e630975bf4d25e64949f30bcc Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 20:39:51 +0000 Subject: [PATCH 11/22] This should clear edges fine. Not entirely sure about what iterators get invalidated when. --- src/gui/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 9b0f81a8..cc70e606 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -827,6 +827,10 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { } //Clear all existing edges from the graph. + std::pair edge_range = boost::edges(graph); + for (edge_it it = edge_range.first; it != edge_range.second; ++it) { + boost::remove_edge(*it, graph); + } //Save edits to the userlist. BOOST_LOG_TRIVIAL(trace) << "Merging down edits to the userlist."; From 4505fbbb3485c4d0775d826d9a62c42b21b38940 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Sun, 9 Feb 2014 21:31:24 +0000 Subject: [PATCH 12/22] Sorting process updated. Behold the infinite loop! --- src/gui/main.cpp | 87 ++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/gui/main.cpp b/src/gui/main.cpp index cc70e606..4dc2b066 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -772,49 +772,62 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { */ - BOOST_LOG_TRIVIAL(debug) << "Building the plugin dependency graph..."; - - //Now add the interactions between plugins to the graph as edges. - BOOST_LOG_TRIVIAL(trace) << "Adding non-overlap edges."; - AddSpecificEdges(graph); - - BOOST_LOG_TRIVIAL(trace) << "Adding priority edges."; - AddPriorityEdges(graph); - - BOOST_LOG_TRIVIAL(trace) << "Adding overlap edges."; - AddOverlapEdges(graph); - //Check for back-edges, then perform a topological sort. try { - BOOST_LOG_TRIVIAL(debug) << "Checking to see if the graph is cyclic."; - boss::CheckForCycles(graph); + bool applyLoadOrder = false; + long ret; + do { + BOOST_LOG_TRIVIAL(debug) << "Building the plugin dependency graph..."; - progDia->Pulse(); + //Now add the interactions between plugins to the graph as edges. + BOOST_LOG_TRIVIAL(trace) << "Adding non-overlap edges."; + AddSpecificEdges(graph); - BOOST_LOG_TRIVIAL(debug) << "Performing a topological sort."; - boss::Sort(graph, plugins); + BOOST_LOG_TRIVIAL(trace) << "Adding priority edges."; + AddPriorityEdges(graph); - progDia->Destroy(); - progDia = NULL; + BOOST_LOG_TRIVIAL(trace) << "Adding overlap edges."; + AddOverlapEdges(graph); - BOOST_LOG_TRIVIAL(debug) << "Displaying load order preview."; - LoadOrderPreview preview(this, translate("BOSS: Calculated Load Order"), plugins, _game); + BOOST_LOG_TRIVIAL(debug) << "Checking to see if the graph is cyclic."; + boss::CheckForCycles(graph); - long ret = preview.ShowModal(); + progDia->Pulse(); - if (ret == wxID_YES) { - preview.Close(); - //User wants to make changes. Need to display editor and loop around again. + BOOST_LOG_TRIVIAL(debug) << "Performing a topological sort."; + boss::Sort(graph, plugins); - MiniEditor editor(this, translate("BOSS: Mini Editor"), plugins, _game); + progDia->Destroy(); + progDia = NULL; - ret = editor.ShowModal(); + BOOST_LOG_TRIVIAL(debug) << "Displaying load order preview."; + LoadOrderPreview preview(this, translate("BOSS: Calculated Load Order"), plugins, _game); - if (ret == wxID_APPLY) { - //User accepted edits, now apply them, then loop. + long ret = preview.ShowModal(); + + applyLoadOrder = ret == wxID_NO; + if (ret != wxID_YES) + break; + else { + //User wants to make changes. Need to display editor and loop around again. + + MiniEditor editor(this, translate("BOSS: Mini Editor"), plugins, _game); + + ret = editor.ShowModal(); const std::list edits = editor.GetEditedPlugins(); - if (!edits.empty()) { + if (ret != wxID_APPLY) + break; + else if (edits.empty()) { + //No edits were actually made, so just go straight to applying the load order. + applyLoadOrder = true; + break; + } + else { + //Recreate progress dialog. + progDia = new wxProgressDialog(translate("BOSS: Working..."), translate("BOSS working..."), 1000, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); + + //User accepted edits, now apply them, then loop. //Apply edits to the graph vertices. boss::vertex_it vit, vitend; for (boost::tie(vit, vitend) = boost::vertices(graph); vit != vitend; ++vit) { @@ -832,7 +845,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { boost::remove_edge(*it, graph); } - //Save edits to the userlist. + //Merge edits down to the userlist entries. BOOST_LOG_TRIVIAL(trace) << "Merging down edits to the userlist."; for (list::const_iterator it = edits.begin(), endit = edits.end(); it != endit; ++it) { list::iterator jt = find(ulist_plugins.begin(), ulist_plugins.end(), *it); @@ -860,14 +873,9 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { //Now loop. } } - else { - //User decided to cancel editing. Just go straight to displaying the report. - BOOST_LOG_TRIVIAL(info) << "The load order calculated was not applied as sorting was canceled."; - messages.push_back(boss::Message(boss::g_message_warn, loc::translate("The load order displayed in the Details tab was not applied as sorting was canceled."))); - } - } - else if (ret == wxID_NO) { - preview.Close(); + } while (true); + + if (applyLoadOrder) { //User doesn't want to make any changes. Just go straight to applying the load order. BOOST_LOG_TRIVIAL(debug) << "Setting load order."; try { @@ -887,6 +895,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(info) << "The load order calculated was not applied as sorting was canceled."; messages.push_back(boss::Message(boss::g_message_warn, loc::translate("The load order displayed in the Details tab was not applied as sorting was canceled."))); } + } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to calculate the load order. Details: " << e.what(); messages.push_back(boss::Message(boss::g_message_error, (format(loc::translate("Failed to calculate the load order. Details: %1%")) % e.what()).str())); From dc14c7ebf994e4ad33101b41ba84a6b8f06a0001 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 12:31:20 +0000 Subject: [PATCH 13/22] Obliterated load order preview dialog. Now the mini editor dialog has its editing part hidden until a plugin is selected. Need to add some text to the dialog still to explain what it's for. --- src/gui/editor.cpp | 58 ++++++++++++++++------- src/gui/editor.h | 3 ++ src/gui/main.cpp | 114 +++++++++++++++++++++------------------------ src/gui/misc.cpp | 47 ------------------- src/gui/misc.h | 9 ---- 5 files changed, 95 insertions(+), 136 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 160af435..d778002d 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -106,7 +106,6 @@ void CommonEditor::ApplyEdits(const wxString& plugin, wxListView * wxList) { } } - boss::File CommonEditor::RowToFile(wxListView * list, long row) const { return boss::File( string(list->GetItemText(row, 0).ToUTF8()), @@ -150,18 +149,21 @@ boss::PluginDirtyInfo CommonEditor::RowToPluginDirtyInfo(wxListView * list, long MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), CommonEditor(plugins, game) { + //Initialise editing panel. + editingPanel = new wxPanel(this, wxID_ANY); + //Initialise controls. - pluginText = new wxStaticText(this, wxID_ANY, ""); - prioritySpin = new wxSpinCtrl(this, wxID_ANY, "0"); + pluginText = new wxStaticText(editingPanel, wxID_ANY, ""); + prioritySpin = new wxSpinCtrl(editingPanel, wxID_ANY, "0"); prioritySpin->SetRange(std::numeric_limits::min(), std::numeric_limits::max()); - filterCheckbox = new wxCheckBox(this, wxID_ANY, translate("Show only conflicting plugins.")); + filterCheckbox = new wxCheckBox(editingPanel, wxID_ANY, translate("Show only conflicting plugins.")); pluginList = new wxListView(this, LIST_Plugins, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); - loadAfterList = new wxListView(this, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + loadAfterList = new wxListView(editingPanel, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); loadAfterList->SetDropTarget(new TextDropTarget(loadAfterList)); - addBtn = new wxToggleButton(this, BUTTON_AddRow, translate("Add Plugin(s)...")); - removeBtn = new wxButton(this, BUTTON_RemoveRow, translate("Remove Plugin")); + addBtn = new wxToggleButton(editingPanel, BUTTON_AddRow, translate("Add Plugin(s)...")); + removeBtn = new wxButton(editingPanel, BUTTON_RemoveRow, translate("Remove Plugin")); //Set up list columns. pluginList->AppendColumn(translate("Plugin")); @@ -191,24 +193,17 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< Bind(wxEVT_CHECKBOX, &MiniEditor::OnFilterToggle, this); Bind(wxEVT_BUTTON, &MiniEditor::OnApply, this, wxID_APPLY); - //Set up layout. - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); - - hBox->Add(pluginList, 0, wxEXPAND | wxALL, 10); - + //Set up editing panel layout. wxBoxSizer * mainBox = new wxBoxSizer(wxVERTICAL); - mainBox->Add(pluginText, 0, wxTOP | wxBOTTOM, 10); mainBox->Add(filterCheckbox, 0, wxTOP | wxBOTTOM, 10); wxBoxSizer * hbox2 = new wxBoxSizer(wxHORIZONTAL); - hbox2->Add(new wxStaticText(this, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); + hbox2->Add(new wxStaticText(editingPanel, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxBOTTOM, 10); - wxStaticBoxSizer * staticBox = new wxStaticBoxSizer(wxVERTICAL, this, translate("Load After")); + wxStaticBoxSizer * staticBox = new wxStaticBoxSizer(wxVERTICAL, editingPanel, translate("Load After")); staticBox->Add(loadAfterList, 1, wxEXPAND); wxBoxSizer * hbox3 = new wxBoxSizer(wxHORIZONTAL); hbox3->Add(addBtn, 0, wxRIGHT, 10); @@ -216,9 +211,19 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< staticBox->Add(hbox3, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP, 10); mainBox->Add(staticBox, 1, wxEXPAND); - hBox->Add(mainBox, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); + editingPanel->SetSizerAndFit(mainBox); + editingPanel->Layout(); + + //Set up rest of layout. + wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); + hBox->Add(pluginList, 0, wxEXPAND | wxALL, 10); + hBox->Add(editingPanel, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); bigBox->Add(hBox, 1, wxEXPAND | wxALL, 5); + bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you want to set the load order given above?"))); + //Need to add 'Yes' and 'No' buttons. wxSizer * sizer = CreateSeparatedButtonSizer(wxAPPLY | wxCANCEL); @@ -240,19 +245,30 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< ++i; } pluginList->SetColumnWidth(0, wxLIST_AUTOSIZE); + pluginList->SetColumnWidth(1, 0); SetBackgroundColour(wxColour(255, 255, 255)); SetIcon(wxIconLocation("BOSS.exe")); + editingPanel->Hide(); SetSizerAndFit(bigBox); Layout(); } void MiniEditor::OnPluginSelect(wxListEvent& event) { + //Change layout. + pluginList->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); + pluginList->InvalidateBestSize(); + if (!editingPanel->IsShown()) + editingPanel->Show(); + Refresh(); + //Create Plugin object for selected plugin. wxString selectedPlugin = pluginList->GetItemText(event.GetIndex()); wxString currentPlugin = pluginText->GetLabelText(); + //lastSelected = pluginText->GetLabelText(); + //Check if the selected plugin is the same as the current plugin. if (selectedPlugin != currentPlugin) { BOOST_LOG_TRIVIAL(debug) << "User selected plugin: " << selectedPlugin.ToUTF8(); @@ -292,6 +308,9 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { addBtn->Enable(true); removeBtn->Enable(false); } + editingPanel->InvalidateBestSize(); + editingPanel->Fit(); + editingPanel->SetMinSize(editingPanel->GetSize()); Fit(); } @@ -395,6 +414,9 @@ void MiniEditor::OnRemoveRow(wxCommandEvent& event) { } void MiniEditor::OnDragStart(wxListEvent& event) { + // if (!lastSelected.empty()) + // pluginList->Select(pluginList->FindItem(-1, lastSelected)); + wxTextDataObject data(pluginList->GetItemText(event.GetItem())); wxDropSource dropSource(pluginList); dropSource.SetData(data); diff --git a/src/gui/editor.h b/src/gui/editor.h index f3af5f01..a05bcc7c 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -94,6 +94,7 @@ public: const std::list& GetEditedPlugins() const; private: wxListView * pluginList; + wxPanel * editingPanel; wxToggleButton * addBtn; wxButton * removeBtn; wxListView * loadAfterList; @@ -101,6 +102,8 @@ private: wxSpinCtrl * prioritySpin; wxStaticText * pluginText; + wxString lastSelected; + boss::Plugin GetNewData(const wxString& plugin) const; }; diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 4dc2b066..58091932 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -801,77 +801,67 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { progDia = NULL; BOOST_LOG_TRIVIAL(debug) << "Displaying load order preview."; - LoadOrderPreview preview(this, translate("BOSS: Calculated Load Order"), plugins, _game); + MiniEditor editor(this, translate("BOSS: Calculated Load Order"), plugins, _game); - long ret = preview.ShowModal(); + long ret = editor.ShowModal(); + const std::list edits = editor.GetEditedPlugins(); - applyLoadOrder = ret == wxID_NO; - if (ret != wxID_YES) + if (ret != wxID_APPLY) { + applyLoadOrder = false; break; + } + else if (edits.empty()) { + applyLoadOrder = true; + break; + } else { - //User wants to make changes. Need to display editor and loop around again. + //Recreate progress dialog. + progDia = new wxProgressDialog(translate("BOSS: Working..."), translate("BOSS working..."), 1000, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); - MiniEditor editor(this, translate("BOSS: Mini Editor"), plugins, _game); + //User accepted edits, now apply them, then loop. + //Apply edits to the graph vertices. + boss::vertex_it vit, vitend; + for (boost::tie(vit, vitend) = boost::vertices(graph); vit != vitend; ++vit) { + std::list::const_iterator pos = std::find(edits.begin(), edits.end(), graph[*vit]); - ret = editor.ShowModal(); - const std::list edits = editor.GetEditedPlugins(); - - if (ret != wxID_APPLY) - break; - else if (edits.empty()) { - //No edits were actually made, so just go straight to applying the load order. - applyLoadOrder = true; - break; + if (pos != edits.end()) { + BOOST_LOG_TRIVIAL(trace) << "Merging edits down to plugin list data."; + graph[*vit].MergeMetadata(*pos); + } } - else { - //Recreate progress dialog. - progDia = new wxProgressDialog(translate("BOSS: Working..."), translate("BOSS working..."), 1000, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); - //User accepted edits, now apply them, then loop. - //Apply edits to the graph vertices. - boss::vertex_it vit, vitend; - for (boost::tie(vit, vitend) = boost::vertices(graph); vit != vitend; ++vit) { - std::list::const_iterator pos = std::find(edits.begin(), edits.end(), graph[*vit]); - - if (pos != edits.end()) { - BOOST_LOG_TRIVIAL(trace) << "Merging edits down to plugin list data."; - graph[*vit].MergeMetadata(*pos); - } - } - - //Clear all existing edges from the graph. - std::pair edge_range = boost::edges(graph); - for (edge_it it = edge_range.first; it != edge_range.second; ++it) { - boost::remove_edge(*it, graph); - } - - //Merge edits down to the userlist entries. - BOOST_LOG_TRIVIAL(trace) << "Merging down edits to the userlist."; - for (list::const_iterator it = edits.begin(), endit = edits.end(); it != endit; ++it) { - list::iterator jt = find(ulist_plugins.begin(), ulist_plugins.end(), *it); - - if (jt != ulist_plugins.end()) { - jt->MergeMetadata(*it); - } - else { - ulist_plugins.push_back(*it); - } - } - - //Save edits to userlist. - BOOST_LOG_TRIVIAL(debug) << "Saving edited userlist."; - YAML::Emitter yout; - yout.SetIndent(2); - yout << YAML::BeginMap - << YAML::Key << "plugins" << YAML::Value << ulist_plugins - << YAML::EndMap; - - boss::ofstream uout(_game.UserlistPath()); - uout << yout.c_str(); - uout.close(); - - //Now loop. + //Clear all existing edges from the graph. + std::pair edge_range = boost::edges(graph); + for (edge_it it = edge_range.first; it != edge_range.second; ++it) { + boost::remove_edge(*it, graph); } + + //Merge edits down to the userlist entries. + BOOST_LOG_TRIVIAL(trace) << "Merging down edits to the userlist."; + for (list::const_iterator it = edits.begin(), endit = edits.end(); it != endit; ++it) { + list::iterator jt = find(ulist_plugins.begin(), ulist_plugins.end(), *it); + + if (jt != ulist_plugins.end()) { + jt->MergeMetadata(*it); + } + else { + ulist_plugins.push_back(*it); + } + } + + //Save edits to userlist. + BOOST_LOG_TRIVIAL(debug) << "Saving edited userlist."; + YAML::Emitter yout; + yout.SetIndent(2); + yout << YAML::BeginMap + << YAML::Key << "plugins" << YAML::Value << ulist_plugins + << YAML::EndMap; + + boss::ofstream uout(_game.UserlistPath()); + uout << yout.c_str(); + uout.close(); + + //Now loop. } } while (true); diff --git a/src/gui/misc.cpp b/src/gui/misc.cpp index ee8af661..850a0725 100644 --- a/src/gui/misc.cpp +++ b/src/gui/misc.cpp @@ -477,51 +477,4 @@ unsigned int DirtInfoEditDialog::GetDeletedNavmeshes() const { wxString DirtInfoEditDialog::GetUtility() const { return _utility->GetValue(); -} - -LoadOrderPreview::LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { - - //Init controls. - _loadOrder = new wxListView(this, LIST_LoadOrder, wxDefaultPosition, wxDefaultSize, wxLC_REPORT); - - //Populate list. - _loadOrder->AppendColumn(translate("Load Order")); - size_t i = 0; - for (list::const_iterator it = plugins.begin(), endit = plugins.end(); it != endit; ++it, ++i) { - _loadOrder->InsertItem(i, FromUTF8(it->Name())); - if (it->FormIDs().empty()) { - _loadOrder->SetItemTextColour(i, wxColour(122, 122, 122)); - } - else if (it->LoadsBSA(game)) { - _loadOrder->SetItemTextColour(i, wxColour(0, 142, 219)); - } - } - _loadOrder->SetColumnWidth(0, wxLIST_AUTOSIZE); - - //Set up event handling. - Bind(wxEVT_BUTTON, &LoadOrderPreview::OnYesNo, this, wxID_YES); - Bind(wxEVT_BUTTON, &LoadOrderPreview::OnYesNo, this, wxID_NO); - - //Set up layout. - wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); - - bigBox->Add(_loadOrder, 1, wxEXPAND | wxALL, 15); - - bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you wish to make any changes to the load order above?")), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 15); - - //Need to add 'Yes' and 'No' buttons. - wxSizer * sizer = CreateSeparatedButtonSizer(wxYES | wxNO | wxCANCEL); - - //Now add buttons to window sizer. - if (sizer != NULL) - bigBox->Add(sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 15); - - //Now set the layout and sizes. - SetBackgroundColour(wxColour(255, 255, 255)); - SetIcon(wxIconLocation("BOSS.exe")); - SetSizerAndFit(bigBox); -} - -void LoadOrderPreview::OnYesNo(wxCommandEvent& event) { - EndModal(event.GetId()); } \ No newline at end of file diff --git a/src/gui/misc.h b/src/gui/misc.h index 2b5ff4e8..ad537d24 100644 --- a/src/gui/misc.h +++ b/src/gui/misc.h @@ -129,13 +129,4 @@ private: wxSpinCtrl * _nav; wxTextCtrl * _utility; }; - -class LoadOrderPreview : public wxDialog { -public: - LoadOrderPreview(wxWindow *parent, const wxString title, const std::list& plugins, const boss::Game& game); - - void OnYesNo(wxCommandEvent& event); -private: - wxListView * _loadOrder; -}; #endif \ No newline at end of file From 6bd9ca4cd2bdb5dc6588718c0937073d7294a19b Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 15:16:21 +0000 Subject: [PATCH 14/22] Mini editor filter fixes. Stopped flickering on toggle, and selection of other plugins is now disabled while the filter is active. --- src/gui/editor.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index d778002d..fce117dd 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -317,16 +317,15 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { void MiniEditor::OnFilterToggle(wxCommandEvent& event) { //First need to merge the base and edited plugin lists so that the right priority values get displayed. - list plugins; - for (list::const_iterator it = _basePlugins.begin(); it != _basePlugins.end(); ++it) { - plugins.push_back(*it); - } + list plugins(_basePlugins); for (list::const_iterator it = _editedPlugins.begin(); it != _editedPlugins.end(); ++it) { list::iterator pos = std::find(plugins.begin(), plugins.end(), *it); pos->MergeMetadata(*it); } + Freeze(); if (event.IsChecked()) { + Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); list::const_iterator pos = std::find(plugins.begin(), plugins.end(), plugin); @@ -353,6 +352,7 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { } } else { + Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); pluginList->DeleteAllItems(); int i = 0; for (list::const_iterator it = plugins.begin(); it != plugins.end(); ++it) { @@ -373,6 +373,7 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { //Now re-select the current plugin in the list. pluginList->Select(pluginList->FindItem(-1, pluginText->GetLabelText())); + Thaw(); } void MiniEditor::OnRowSelect(wxListEvent& event) { From d68653a34d9556af6304109676e3ccfcb8cc3029 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 15:36:36 +0000 Subject: [PATCH 15/22] Fixed text formatting in mini editor. --- src/gui/editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index fce117dd..8f8f313c 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -222,7 +222,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< hBox->Add(editingPanel, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); bigBox->Add(hBox, 1, wxEXPAND | wxALL, 5); - bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you want to set the load order given above?"))); + bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you want to apply the load order given above?")), 0, wxLEFT|wxRIGHT|wxBOTTOM, 15); //Need to add 'Yes' and 'No' buttons. wxSizer * sizer = CreateSeparatedButtonSizer(wxAPPLY | wxCANCEL); From 8603ab002ab29f10c022ae6ab08b0c87ea8ded53 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 15:58:57 +0000 Subject: [PATCH 16/22] Mini editor filter improvements. Now doesn't fight with the plugin addition toggle. Also now displays only those plugins that the selected plugin can validly load after, and displays plugins that have a BSA that may contain conflicting resources, if the selected plugin has a BSA. --- src/gui/editor.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 8f8f313c..ccf9175d 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -323,18 +323,27 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { pos->MergeMetadata(*it); } + //Disable list selection. + if (event.IsChecked() || addBtn->GetValue()) + Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); + else + Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); + Freeze(); if (event.IsChecked()) { - Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); list::const_iterator pos = std::find(plugins.begin(), plugins.end(), plugin); if (pos != plugins.end()) { pluginList->DeleteAllItems(); + bool loadsBSA = pos->LoadsBSA(_game); + int i = 0; for (list::const_iterator it = plugins.begin(); it != plugins.end(); ++it) { - if (pos->DoFormIDsOverlap(*it)) { + //Want to filter to show only those the selected plugin can load after validly, and which also either conflict with it, + //or which load a BSA (if the selected plugin loads a BSA). + if (!it->MustLoadAfter(*pos) && (pos->DoFormIDsOverlap(*it) || (loadsBSA && it->LoadsBSA(_game)))) { pluginList->InsertItem(i, FromUTF8(it->Name())); pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); if (it->FormIDs().empty()) { @@ -352,7 +361,6 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { } } else { - Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); pluginList->DeleteAllItems(); int i = 0; for (list::const_iterator it = plugins.begin(); it != plugins.end(); ++it) { @@ -403,7 +411,7 @@ void MiniEditor::OnRowSelect(wxListEvent& event) { void MiniEditor::OnAddRowToggle(wxCommandEvent& event) { //This exists because drag 'n' drop selects plugins when they're clicked on, and we //don't want them being loaded. - if (event.IsChecked()) + if (event.IsChecked() || filterCheckbox->IsChecked()) Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); else Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); From 0e03c2002d6803e45197336c0bbae870b77fc881 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 18:00:52 +0000 Subject: [PATCH 17/22] Removed "Add Plugins" toggle from mini editor. The filter checkbox now has the same effect, so it's unnecessary. --- src/gui/editor.cpp | 26 ++------------------------ src/gui/editor.h | 4 ---- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index ccf9175d..add38b47 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -162,7 +162,6 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< loadAfterList = new wxListView(editingPanel, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); loadAfterList->SetDropTarget(new TextDropTarget(loadAfterList)); - addBtn = new wxToggleButton(editingPanel, BUTTON_AddRow, translate("Add Plugin(s)...")); removeBtn = new wxButton(editingPanel, BUTTON_RemoveRow, translate("Remove Plugin")); //Set up list columns. @@ -176,7 +175,6 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< loadAfterList->SetColumnWidth(2, 0); //Hide this from the user. //Initialise control states. - addBtn->Enable(false); removeBtn->Enable(false); prioritySpin->Enable(false); filterCheckbox->Enable(false); @@ -189,7 +187,6 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< Bind(wxEVT_LIST_BEGIN_DRAG, &MiniEditor::OnDragStart, this, LIST_Plugins); Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnRowSelect, this, LIST_LoadAfter); Bind(wxEVT_BUTTON, &MiniEditor::OnRemoveRow, this, BUTTON_RemoveRow); - Bind(wxEVT_TOGGLEBUTTON, &MiniEditor::OnAddRowToggle, this, BUTTON_AddRow); Bind(wxEVT_CHECKBOX, &MiniEditor::OnFilterToggle, this); Bind(wxEVT_BUTTON, &MiniEditor::OnApply, this, wxID_APPLY); @@ -205,10 +202,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< wxStaticBoxSizer * staticBox = new wxStaticBoxSizer(wxVERTICAL, editingPanel, translate("Load After")); staticBox->Add(loadAfterList, 1, wxEXPAND); - wxBoxSizer * hbox3 = new wxBoxSizer(wxHORIZONTAL); - hbox3->Add(addBtn, 0, wxRIGHT, 10); - hbox3->Add(removeBtn); - staticBox->Add(hbox3, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP, 10); + staticBox->Add(removeBtn, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP, 5); mainBox->Add(staticBox, 1, wxEXPAND); editingPanel->SetSizerAndFit(mainBox); @@ -267,8 +261,6 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { wxString selectedPlugin = pluginList->GetItemText(event.GetIndex()); wxString currentPlugin = pluginText->GetLabelText(); - //lastSelected = pluginText->GetLabelText(); - //Check if the selected plugin is the same as the current plugin. if (selectedPlugin != currentPlugin) { BOOST_LOG_TRIVIAL(debug) << "User selected plugin: " << selectedPlugin.ToUTF8(); @@ -305,7 +297,6 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { //Set control states. prioritySpin->Enable(true); filterCheckbox->Enable(true); - addBtn->Enable(true); removeBtn->Enable(false); } editingPanel->InvalidateBestSize(); @@ -324,7 +315,7 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { } //Disable list selection. - if (event.IsChecked() || addBtn->GetValue()) + if (event.IsChecked()) Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); else Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); @@ -385,7 +376,6 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { } void MiniEditor::OnRowSelect(wxListEvent& event) { - boss::File file(string(loadAfterList->GetItemText(event.GetIndex(), 0).ToUTF8())); boss::Plugin plugin(string(pluginText->GetLabelText().ToUTF8())); @@ -408,24 +398,12 @@ void MiniEditor::OnRowSelect(wxListEvent& event) { } } -void MiniEditor::OnAddRowToggle(wxCommandEvent& event) { - //This exists because drag 'n' drop selects plugins when they're clicked on, and we - //don't want them being loaded. - if (event.IsChecked() || filterCheckbox->IsChecked()) - Unbind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); - else - Bind(wxEVT_LIST_ITEM_SELECTED, &MiniEditor::OnPluginSelect, this, LIST_Plugins); -} - void MiniEditor::OnRemoveRow(wxCommandEvent& event) { loadAfterList->DeleteItem(loadAfterList->GetFirstSelected()); removeBtn->Enable(false); } void MiniEditor::OnDragStart(wxListEvent& event) { - // if (!lastSelected.empty()) - // pluginList->Select(pluginList->FindItem(-1, lastSelected)); - wxTextDataObject data(pluginList->GetItemText(event.GetItem())); wxDropSource dropSource(pluginList); dropSource.SetData(data); diff --git a/src/gui/editor.h b/src/gui/editor.h index a05bcc7c..116c9b18 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -86,7 +86,6 @@ public: void OnPluginSelect(wxListEvent& event); void OnRowSelect(wxListEvent& event); void OnRemoveRow(wxCommandEvent& event); - void OnAddRowToggle(wxCommandEvent& event); void OnFilterToggle(wxCommandEvent& event); void OnDragStart(wxListEvent& event); void OnApply(wxCommandEvent& event); @@ -95,15 +94,12 @@ public: private: wxListView * pluginList; wxPanel * editingPanel; - wxToggleButton * addBtn; wxButton * removeBtn; wxListView * loadAfterList; wxCheckBox * filterCheckbox; wxSpinCtrl * prioritySpin; wxStaticText * pluginText; - wxString lastSelected; - boss::Plugin GetNewData(const wxString& plugin) const; }; From bb06bb5fd0fcaac614ed44df4a9200c81f53f759 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 18:16:57 +0000 Subject: [PATCH 18/22] Mini editor drag 'n' drop improvements. Now doesn't do anything if you try to add the selected plugin or a plugin that's already listed in the box. --- src/gui/editor.cpp | 10 +++++----- src/gui/editor.h | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index add38b47..661c29a3 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -38,11 +38,11 @@ using namespace std; // TextDropTarget class ////////////////////////////// -TextDropTarget::TextDropTarget(wxListView * owner) { - targetOwner = owner; -} +TextDropTarget::TextDropTarget(wxListView * owner, wxStaticText * name) : targetOwner(owner), targetName(name) {} bool TextDropTarget::OnDropText(wxCoord x, wxCoord y, const wxString &data) { + if (data == targetName->GetLabelText() || targetOwner->FindItem(-1, data) != wxNOT_FOUND) + return false; targetOwner->InsertItem(targetOwner->GetItemCount(), data); return true; } @@ -160,7 +160,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< pluginList = new wxListView(this, LIST_Plugins, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); loadAfterList = new wxListView(editingPanel, LIST_LoadAfter, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); - loadAfterList->SetDropTarget(new TextDropTarget(loadAfterList)); + loadAfterList->SetDropTarget(new TextDropTarget(loadAfterList, pluginText)); removeBtn = new wxButton(editingPanel, BUTTON_RemoveRow, translate("Remove Plugin")); @@ -334,7 +334,7 @@ void MiniEditor::OnFilterToggle(wxCommandEvent& event) { for (list::const_iterator it = plugins.begin(); it != plugins.end(); ++it) { //Want to filter to show only those the selected plugin can load after validly, and which also either conflict with it, //or which load a BSA (if the selected plugin loads a BSA). - if (!it->MustLoadAfter(*pos) && (pos->DoFormIDsOverlap(*it) || (loadsBSA && it->LoadsBSA(_game)))) { + if (*it == *pos || !it->MustLoadAfter(*pos) && (pos->DoFormIDsOverlap(*it) || (loadsBSA && it->LoadsBSA(_game)))) { pluginList->InsertItem(i, FromUTF8(it->Name())); pluginList->SetItem(i, 1, FromUTF8(boss::IntToString(it->Priority()))); if (it->FormIDs().empty()) { diff --git a/src/gui/editor.h b/src/gui/editor.h index 116c9b18..1ef7eae5 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -53,10 +53,11 @@ class TextDropTarget : public wxTextDropTarget { //Class to override virtual functions. public: - TextDropTarget(wxListView * owner); + TextDropTarget(wxListView * owner, wxStaticText * name); virtual bool OnDropText(wxCoord x, wxCoord y, const wxString &data); private: - wxListView *targetOwner; + wxListView * targetOwner; + wxStaticText * targetName; }; class CommonEditor { From e134e442189932921e742950875044eb6108463c Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 19:17:07 +0000 Subject: [PATCH 19/22] Updated text in mini editor. --- src/gui/editor.cpp | 5 +++-- src/gui/editor.h | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 661c29a3..b592053c 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -154,6 +154,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< //Initialise controls. pluginText = new wxStaticText(editingPanel, wxID_ANY, ""); + descText = new wxStaticText(this, wxID_ANY, translate("Please submit any edits made for reasons other than personal preference \nto the BOSS team so that they may be included in the masterlist.")); prioritySpin = new wxSpinCtrl(editingPanel, wxID_ANY, "0"); prioritySpin->SetRange(std::numeric_limits::min(), std::numeric_limits::max()); filterCheckbox = new wxCheckBox(editingPanel, wxID_ANY, translate("Show only conflicting plugins.")); @@ -212,11 +213,11 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); - hBox->Add(pluginList, 0, wxEXPAND | wxALL, 10); + hBox->Add(pluginList, 1, wxEXPAND | wxALL, 10); hBox->Add(editingPanel, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); bigBox->Add(hBox, 1, wxEXPAND | wxALL, 5); - bigBox->Add(new wxStaticText(this, wxID_ANY, translate("Do you want to apply the load order given above?")), 0, wxLEFT|wxRIGHT|wxBOTTOM, 15); + bigBox->Add(descText, 0, wxEXPAND|wxLEFT|wxRIGHT|wxBOTTOM, 15); //Need to add 'Yes' and 'No' buttons. wxSizer * sizer = CreateSeparatedButtonSizer(wxAPPLY | wxCANCEL); diff --git a/src/gui/editor.h b/src/gui/editor.h index 1ef7eae5..71fb1bf8 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -100,6 +100,7 @@ private: wxCheckBox * filterCheckbox; wxSpinCtrl * prioritySpin; wxStaticText * pluginText; + wxStaticText * descText; boss::Plugin GetNewData(const wxString& plugin) const; }; From ac3bd7ed552f71f3c6f93f158599064b4ecd0069 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 10 Feb 2014 23:35:23 +0000 Subject: [PATCH 20/22] Attempting to fix the layout of the mini editor. Made the editor non-resizable to keep things simple, initially wrapping dialog text to width of plugin list. Selecting a plugin with a long name seems to resize the editingPanel right, but it then gets clipped by the dialog window, which doesn't resize correctly. --- src/gui/editor.cpp | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index b592053c..c3980a50 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -148,13 +148,13 @@ boss::PluginDirtyInfo CommonEditor::RowToPluginDirtyInfo(wxListView * list, long /////////////////////////////////// -MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), CommonEditor(plugins, game) { +MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list& plugins, const boss::Game& game) : wxDialog(parent, wxID_ANY, title), CommonEditor(plugins, game) { //Initialise editing panel. editingPanel = new wxPanel(this, wxID_ANY); //Initialise controls. pluginText = new wxStaticText(editingPanel, wxID_ANY, ""); - descText = new wxStaticText(this, wxID_ANY, translate("Please submit any edits made for reasons other than personal preference \nto the BOSS team so that they may be included in the masterlist.")); + descText = new wxStaticText(this, wxID_ANY, translate("Please submit any edits made for reasons other than personal preference to the BOSS team so that they may be included in the masterlist.")); prioritySpin = new wxSpinCtrl(editingPanel, wxID_ANY, "0"); prioritySpin->SetRange(std::numeric_limits::min(), std::numeric_limits::max()); filterCheckbox = new wxCheckBox(editingPanel, wxID_ANY, translate("Show only conflicting plugins.")); @@ -197,14 +197,14 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< mainBox->Add(filterCheckbox, 0, wxTOP | wxBOTTOM, 10); wxBoxSizer * hbox2 = new wxBoxSizer(wxHORIZONTAL); - hbox2->Add(new wxStaticText(editingPanel, wxID_ANY, translate("Priority: ")), 0, wxALIGN_RIGHT | wxLEFT | wxRIGHT, 5); - hbox2->Add(prioritySpin, 0, wxALIGN_RIGHT); - mainBox->Add(hbox2, 0, wxEXPAND | wxALIGN_RIGHT | wxBOTTOM, 10); + hbox2->Add(new wxStaticText(editingPanel, wxID_ANY, translate("Priority: ")), 0, wxRIGHT, 5); + hbox2->Add(prioritySpin); + mainBox->Add(hbox2, 0, wxEXPAND | wxBOTTOM, 10); wxStaticBoxSizer * staticBox = new wxStaticBoxSizer(wxVERTICAL, editingPanel, translate("Load After")); - staticBox->Add(loadAfterList, 1, wxEXPAND); + staticBox->Add(loadAfterList, 0, wxEXPAND); staticBox->Add(removeBtn, 0, wxEXPAND | wxALIGN_RIGHT | wxTOP, 5); - mainBox->Add(staticBox, 1, wxEXPAND); + mainBox->Add(staticBox, 0, wxEXPAND); editingPanel->SetSizerAndFit(mainBox); editingPanel->Layout(); @@ -213,9 +213,9 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< wxBoxSizer * bigBox = new wxBoxSizer(wxVERTICAL); wxBoxSizer * hBox = new wxBoxSizer(wxHORIZONTAL); - hBox->Add(pluginList, 1, wxEXPAND | wxALL, 10); - hBox->Add(editingPanel, 1, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); - bigBox->Add(hBox, 1, wxEXPAND | wxALL, 5); + hBox->Add(pluginList, 0, wxEXPAND | wxALL, 10); + hBox->Add(editingPanel, 0, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, 10); + bigBox->Add(hBox, 0, wxEXPAND | wxALL, 5); bigBox->Add(descText, 0, wxEXPAND|wxLEFT|wxRIGHT|wxBOTTOM, 15); @@ -241,6 +241,7 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< } pluginList->SetColumnWidth(0, wxLIST_AUTOSIZE); pluginList->SetColumnWidth(1, 0); + descText->Wrap(pluginList->GetClientSize().GetWidth()); SetBackgroundColour(wxColour(255, 255, 255)); SetIcon(wxIconLocation("BOSS.exe")); @@ -251,13 +252,6 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< } void MiniEditor::OnPluginSelect(wxListEvent& event) { - //Change layout. - pluginList->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); - pluginList->InvalidateBestSize(); - if (!editingPanel->IsShown()) - editingPanel->Show(); - Refresh(); - //Create Plugin object for selected plugin. wxString selectedPlugin = pluginList->GetItemText(event.GetIndex()); wxString currentPlugin = pluginText->GetLabelText(); @@ -300,10 +294,24 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { filterCheckbox->Enable(true); removeBtn->Enable(false); } - editingPanel->InvalidateBestSize(); + + //Change layout. + if (pluginList->GetColumnWidth(1) == 0) + pluginList->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); + if (!editingPanel->IsShown()) + editingPanel->Show(); + + //Do all the horrible fitting. Still haven't managed to set a min size for the editing controls. + pluginList->InvalidateBestSize(); //Makes the priority column visible without scrolling. + //pluginText->InvalidateBestSize(); + //pluginText->Layout(); editingPanel->Fit(); - editingPanel->SetMinSize(editingPanel->GetSize()); + InvalidateBestSize(); + //descText->Wrap(descText->GetSize().GetWidth()); Fit(); + //Layout(); + // int width = GetClientSize().GetWidth(); + //descText->SetSize(width, descText->GetSize().GetHeight()); } void MiniEditor::OnFilterToggle(wxCommandEvent& event) { From 6a7c30857137ac06c2245d7f28a682b0ad8dd342 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Tue, 11 Feb 2014 00:24:07 +0000 Subject: [PATCH 21/22] Fixed mini editor layout. --- src/gui/editor.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index c3980a50..f47d0c63 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -248,7 +248,6 @@ MiniEditor::MiniEditor(wxWindow *parent, const wxString& title, const std::list< editingPanel->Hide(); SetSizerAndFit(bigBox); - Layout(); } void MiniEditor::OnPluginSelect(wxListEvent& event) { @@ -302,16 +301,15 @@ void MiniEditor::OnPluginSelect(wxListEvent& event) { editingPanel->Show(); //Do all the horrible fitting. Still haven't managed to set a min size for the editing controls. + //Layout() doesn't do anything extra, it automatically gets called by the windows. + //InvalidateBestSize() doesn't do anything useful for the panel or the window. pluginList->InvalidateBestSize(); //Makes the priority column visible without scrolling. - //pluginText->InvalidateBestSize(); - //pluginText->Layout(); - editingPanel->Fit(); - InvalidateBestSize(); - //descText->Wrap(descText->GetSize().GetWidth()); + editingPanel->Fit(); //Fits the editing panel to the pluginText length. + editingPanel->SetMinSize(editingPanel->GetSize()); //Makes sure that the editing panel is not cut off when Fit() is called below. Fit(); - //Layout(); - // int width = GetClientSize().GetWidth(); - //descText->SetSize(width, descText->GetSize().GetHeight()); + int width = descText->GetSize().GetWidth(); + descText->SetLabel(translate("Please submit any edits made for reasons other than personal preference to the BOSS team so that they may be included in the masterlist.")); + descText->Wrap(width); } void MiniEditor::OnFilterToggle(wxCommandEvent& event) { From 5b08cf04707a0fdda5573406cdc27538a9b81ec0 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Tue, 11 Feb 2014 09:19:28 +0000 Subject: [PATCH 22/22] Readme updates. --- docs/BOSS Readme.html | 16 ++++++++++------ docs/images/confirm-1.png | Bin 0 -> 10656 bytes docs/images/confirm-2.png | Bin 0 -> 27877 bytes docs/images/confirm.png | Bin 11853 -> 0 bytes 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 docs/images/confirm-1.png create mode 100644 docs/images/confirm-2.png delete mode 100644 docs/images/confirm.png diff --git a/docs/BOSS Readme.html b/docs/BOSS Readme.html index ef19087e..cd79c51b 100644 --- a/docs/BOSS Readme.html +++ b/docs/BOSS Readme.html @@ -153,16 +153,19 @@ Visual C++ Redistributable for Visual Studio 2013 (x86). BOSS can be install

The Help menu provides a link to this readme and an about page that contains the version of BOSS being run, some legal information and a link to the project homepage.

Sorting Plugins

-
- -
Editing BOSS's calculated load order.
+
+ BOSS confirmation dialog + BOSS confirmation dialog with editing panel visible +
Viewing & editing BOSS's calculated load order.

Clicking the Sort Plugins button in the main window will begin the sorting process.

Provided that masterlist updating is enabled and that a masterlist repository URL is set, BOSS checks for updates to its masterlist and downloads any that are available. If there are any syntax errors in the latest masterlist (due to human error by its maintainers), the updater rolls back one version at a time until it finds a version that works. If any other errors are encountered, updating is cancelled.

During the sorting process, BOSS extracts information from each of the plugins installed in the game's Data folder and combines this information with any relevant metadata provided by the masterlist and userlist. It then uses the combined data to build a directed graph of all the interactions between all the installed plugins. A topological sort is then carried out on the graph to produce a list of plugins in their optimal load order, according to BOSS's criteria. If a cycle is detected (eg. A depends on B depends on A), then sorting is cancelled as it becomes impossible to generate a load order that satisfies all conditions. -

If successful, the calculated load order is then displayed in the dialog shown above, which allows the user to edit the load order before it is applied. Multiple plugins may be selected and moved at once. Plugins which are empty (ie. only contain a header record) are displayed in grey text, and plugins that are not empty and load BSAs are displayed in blue text. If OK button is clicked, it is then applied, and any edits made are recorded in the userlist for re-application when BOSS next sorts the game's plugins. If the Cancel button is clicked, then the load order and any edits made are discarded. +

If successful, the calculated load order is then displayed in the dialog shown above. Plugins which are empty (ie. only contain a header record) are displayed in grey text, and plugins that are not empty and load BSAs are displayed in blue text. +Clicking on a plugin in the list will display the editing panel, where its load order related metadata can be edited. The Show only conflicting plugins checkbox filters the plugin list so that only plugins which can be validly added to the Load After list and either contain conflicting records or (if the selected plugin loads a BSA) load BSAs that may contain conflicting resources. The checkbox must be ticked before plugins can be added to the Load After list, which is done by drag 'n' dropping them from the plugin list, and it must be unticked to select another plugin for editing. The priority value is explained below. +

If the Apply button is clicked, BOSS does one of two things: if no edits were made, then it immediately applies the load order, but if edits were made, then it repeats the sorting process with the new edits applied. If the Cancel button is clicked, then the load order and any edits made are discarded.

BOSS then generates a report and displays it either in a new window or in your default browser, according to the value of the relevant setting. Any errors encountered during the sorting process will be displayed in this report, including those due to graph cycles, missing dependencies or installed incompatible plugins. See the next section for more information on the report.

BOSS is able to sort plugins ghosted by Wrye *ash, and can extract Bash Tags and version numbers from plugin descriptions. Provided that they have the Filter Bash Tag present in their description, BOSS can recognise filter patches and so avoid displaying unnecessary error messages for any of their masters that may be missing.

While reading very large plugins, such as the game's main master file, BOSS may appear to stop responding: this is not an issue. The time it takes for BOSS to sort your load order depends primarily on the size of the masterlist being used, the total size of the files in your load order, and the number of plugins in your load order. The first run is likely to be longer than subsequent runs as BOSS needs to download the full masterlist, rather than just the changes made by updates. In testing, it was found that sorting ~90 plugins with a total size of ~350 MB and a 5 MB masterlist takes around 15 seconds, though performance will vary with computer hardware. @@ -176,7 +179,7 @@ Visual C++ Redistributable for Visual Studio 2013 (x86). BOSS can be install

BOSS's results report can be displayed either in BOSS's own report viewer, or in your default web browser, according to the value of the relevant setting. -

The report is comprised of three main sections: +

The report is comprised of two sections:

  • The Summary tab gives information on the versions of BOSS and the masterlist used, whether masterlist updating is enabled, and breaks down the numbers of each message type in the Summary and Details tabs. It also notifies you if there have been no changes in the Details tab since you last ran BOSS for the same game. Finally, the Summary tab is also where any global messages supplied in the masterlist and any errors encountered during sorting are displayed.
  • The Details tab lists the plugins BOSS sorted in their new load order, along with any messages BOSS has provided for them. BOSS will also display the plugin's CRC value and extract its version if found in the plugin's description field. If BOSS suggests any Bash Tags to be added, they will be displayed in green text, while any Bash Tags to be removed will be displayed in red text. @@ -210,7 +213,7 @@ Visual C++ Redistributable for Visual Studio 2013 (x86). BOSS can be install FieldDescription Enable User ChangesIf this is checked, BOSS will use the metadata you supplied when sorting the current plugin. Otherwise, your metadata will be ignored. - PriorityThis is a very rough control for determining how far up or down the load order a plugin should go. Plugins with equal priority will be compared as normal by the sorting algorithm, but plugins with a larger priority value will load later in the load order than plugins with a smaller priority value, unless the former depends on the latter or the latter is explicity told to load after the former. Plugins have a default priority of 0. + PriorityThis is a very rough control for determining how far up or down the load order a plugin should go. Plugins with equal priority will be compared as normal by the sorting algorithm, but plugins with a larger priority value will load later in the load order than plugins with a smaller priority value, unless the former depends on the latter or the latter is explicity told to load after the former. Plugins have a default priority of 0. RequirementsThis is a list of files that are required by the current plugin for it to function correctly. BOSS will display an error message if any of the listed files are missing.

    Any file, not just plugins, can be listed here, and each file has three sub-fields:

      @@ -370,6 +373,7 @@ To translate the BOSS application:
    • Ysne58: For keeping the thread links on BOSS's Nexus site pages up to date.
    • Łukasz Niemczyk: For the Polish translation of v3.
    • bluesky404: For the Chinese translation of v3. +
    • Kaos: For the French translation of v3.
    • For beta testing: Surazal, myk002, Hickory, Ysne58, the Beta Testing & Analysis Guild at TES Alliance, AndalayBay, dAb, dj2005, Dubiousness, hyno111, Invader, matter2003, NightStar, olminator, saebel, Telyn, wiz0floyd, wormheart, Zanderat, zone22 and many more.

    BOSS is written in C++ and makes use of the Alphanum, Boost, libespm, libgit2, libloadorder, PugiXML, wxWidgets and yaml-cpp libraries. BOSS's reports are written in HTML5/CSS3/Javascript and make use of Polyfill.js to provide Internet Explorer 8 compatibility. Copyright license information for all these may be found here. diff --git a/docs/images/confirm-1.png b/docs/images/confirm-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9ecb038563da1f8af4776aa5db6e723ef4510a56 GIT binary patch literal 10656 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 Jmvv4FO#sBI^uPcB literal 0 HcmV?d00001 diff --git a/docs/images/confirm-2.png b/docs/images/confirm-2.png new file mode 100644 index 0000000000000000000000000000000000000000..88ecde28af4832c5959ab25bbeaeb96d4eef3bb7 GIT binary patch literal 27877 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 Ba=QQk literal 0 HcmV?d00001 diff --git a/docs/images/confirm.png b/docs/images/confirm.png deleted file mode 100644 index 83b16e4af3f36e22f8b0d837c12f155673a0aa31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11853 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