Implement file picker load/save normal
This commit is contained in:
9
rsrc/dialogs/confirm-overwrite.xml
Normal file
9
rsrc/dialogs/confirm-overwrite.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
|
||||
<!-- NOTE: This file should be updated to use relative positioning the next time it changes. -->
|
||||
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
|
||||
<dialog defbtn='cancel'>
|
||||
<pict type='dlog' num='23' top='9' left='9'/>
|
||||
<text name='prompt' top='5' left='51' width='255' height='32'>Are you sure you want to overwrite {File}?</text>
|
||||
<button name='save' type='regular' top='46' left='178'>Save</button>
|
||||
<button name='cancel' type='regular' def-key='esc' top='46' left='248'>Cancel</button>
|
||||
</dialog>
|
@@ -33,7 +33,7 @@ bool mac_is_intel(){
|
||||
}
|
||||
return _mac_is_intel;
|
||||
}
|
||||
fs::path progDir, tempDir, scenDir, replayDir;
|
||||
fs::path progDir, tempDir, scenDir, replayDir, saveDir;
|
||||
|
||||
// This is here to avoid unnecessarily duplicating it in platform-specific files.
|
||||
cursor_type Cursor::current = sword_curs;
|
||||
@@ -81,6 +81,9 @@ void init_directories(const char* exec_path) {
|
||||
replayDir = tempDir/"Replays";
|
||||
fs::create_directories(replayDir);
|
||||
|
||||
saveDir = tempDir/"Saves";
|
||||
fs::create_directories(saveDir);
|
||||
|
||||
add_resmgr_paths(tempDir/"data");
|
||||
tempDir /= "Temporary Files";
|
||||
|
||||
|
@@ -21,6 +21,7 @@
|
||||
#include "fileio/tagfile.hpp"
|
||||
#include "fileio/tarball.hpp"
|
||||
#include "replay.hpp"
|
||||
#include "game/boe.dlgutil.hpp"
|
||||
|
||||
extern bool mac_is_intel();
|
||||
extern fs::path progDir, tempDir;
|
||||
@@ -39,6 +40,8 @@ fs::path nav_get_or_decode_party() {
|
||||
decode_file(next_action.GetText(), tempDir / "temp.exg");
|
||||
return tempDir / "temp.exg";
|
||||
}else{
|
||||
// TODO if the save is not in the saves folder, prompt about moving it in,
|
||||
// and return the moved path?
|
||||
return nav_get_party();
|
||||
}
|
||||
}
|
||||
@@ -304,7 +307,6 @@ bool load_party_v1(fs::path file_to_load, cUniverse& real_univ, bool town_restor
|
||||
return true;
|
||||
}
|
||||
|
||||
extern fs::path scenDir;
|
||||
bool load_party_v2(fs::path file_to_load, cUniverse& real_univ){
|
||||
igzstream zin(file_to_load.string().c_str());
|
||||
tarball partyIn;
|
||||
@@ -546,7 +548,7 @@ bool save_party(cUniverse& univ, bool save_as) {
|
||||
// univ.file can be empty for prefab parties, so a file browser might be needed
|
||||
// even for a regular save.
|
||||
if(save_as || univ.file.empty()){
|
||||
univ.file = nav_put_or_temp_party();
|
||||
univ.file = run_file_picker(true);
|
||||
}
|
||||
// A file wasn't chosen
|
||||
if(univ.file.empty()) return false;
|
||||
|
@@ -2907,6 +2907,7 @@ bool handle_scroll(const sf::Event& event) {
|
||||
}
|
||||
|
||||
void do_load() {
|
||||
// TODO this needs to be changed/moved because a picker dialog opens now!!!
|
||||
// Edge case: Replay can be cut off before a file is chosen,
|
||||
// or party selection can be canceled, and this will cause
|
||||
// a crash trying to decode a party
|
||||
@@ -2914,7 +2915,7 @@ void do_load() {
|
||||
return;
|
||||
}
|
||||
|
||||
fs::path file_to_load = nav_get_or_decode_party();
|
||||
fs::path file_to_load = run_file_picker(false);
|
||||
if(file_to_load.empty()) return;
|
||||
if(!load_party(file_to_load, univ))
|
||||
return;
|
||||
@@ -3428,7 +3429,7 @@ void handle_death() {
|
||||
return;
|
||||
}
|
||||
else if(choice == "load") {
|
||||
fs::path file_to_load = nav_get_or_decode_party();
|
||||
fs::path file_to_load = run_file_picker(false);
|
||||
if(!file_to_load.empty()){
|
||||
if(load_party(file_to_load, univ)){
|
||||
finish_load_party();
|
||||
|
@@ -1,5 +1,9 @@
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <string>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
|
||||
#include "boe.global.hpp"
|
||||
|
||||
@@ -1716,3 +1720,280 @@ public:
|
||||
scen_header_type pick_a_scen() {
|
||||
return cChooseScenario().run();
|
||||
}
|
||||
|
||||
extern fs::path saveDir;
|
||||
|
||||
class cFilePicker {
|
||||
const int SLOTS_PER_PAGE = 4;
|
||||
int parties_per_page;
|
||||
|
||||
fs::path save_folder;
|
||||
bool picking_auto;
|
||||
bool saving;
|
||||
cDialog me{*ResMgr::dialogs.get("pick-save")};
|
||||
cStack& get_stack() { return dynamic_cast<cStack&>(me["list"]); }
|
||||
std::string template_info_str;
|
||||
|
||||
std::vector<std::pair<fs::path, std::time_t>> save_file_mtimes;
|
||||
// We have to load the parties to get PC graphics, average level, location, etc.
|
||||
// But we shouldn't load them all at once, because the amount is unlimited.
|
||||
std::vector<cUniverse> save_files;
|
||||
int pages_populated = 0;
|
||||
int saves_loaded = 0;
|
||||
|
||||
void init_pages() {
|
||||
save_file_mtimes = sorted_file_mtimes(save_folder);
|
||||
save_files.resize(save_file_mtimes.size());
|
||||
|
||||
cStack& stk = get_stack();
|
||||
int num_pages = ceil((float)save_file_mtimes.size() / parties_per_page);
|
||||
stk.setPageCount(num_pages);
|
||||
}
|
||||
|
||||
void empty_slot(int idx) {
|
||||
std::string suffix = std::to_string(idx+1);
|
||||
me["file" + suffix].setText("");
|
||||
me["pc" + suffix + "a"].hide();
|
||||
me["pc" + suffix + "b"].hide();
|
||||
me["pc" + suffix + "c"].hide();
|
||||
me["pc" + suffix + "d"].hide();
|
||||
me["pc" + suffix + "e"].hide();
|
||||
me["pc" + suffix + "f"].hide();
|
||||
me["info" + suffix].hide();
|
||||
me["load" + suffix].hide();
|
||||
me["auto" + suffix].hide();
|
||||
me["auto" + suffix + "-more-recent"].hide();
|
||||
}
|
||||
|
||||
void populate_slot(int idx, fs::path file, std::time_t mtime, cUniverse& party_univ) {
|
||||
std::string suffix = std::to_string(idx+1);
|
||||
me["file" + suffix].setText(file.filename().string());
|
||||
|
||||
// Populate PC graphics
|
||||
for(int i = 0; i < 6; ++i){
|
||||
std::string key = "pc" + suffix;
|
||||
key.push_back((char)('a' + i));
|
||||
cPict& pict = dynamic_cast<cPict&>(me[key]);
|
||||
if(party_univ.party[i].main_status != eMainStatus::ABSENT) {
|
||||
pic_num_t pic = party_univ.party[i].which_graphic;
|
||||
// TODO Apparently PCs are supposed to be able to have custom graphics and monster graphics,
|
||||
// but the special node to Create PC only allows choosing preset PC pictures, so I don't
|
||||
// think it's even possible to get a non-preset-PC graphic into a party without directly
|
||||
// editing the tagfile. For now, I'm only rendering preset PCs.
|
||||
if(pic >= 1000){
|
||||
}else if(pic >= 100){
|
||||
}else{
|
||||
pict.setPict(pic, PIC_PC);
|
||||
}
|
||||
pict.show();
|
||||
}else{
|
||||
pict.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Populate party info
|
||||
std::string party_info = template_info_str;
|
||||
|
||||
short level_sum = 0;
|
||||
short num_pc = 0;
|
||||
for(int i = 0; i < 6; ++i){
|
||||
if(party_univ.party[i].main_status != eMainStatus::ABSENT) {
|
||||
level_sum += party_univ.party[i].level;
|
||||
++num_pc;
|
||||
}
|
||||
}
|
||||
short avg_level = round((float)(level_sum / num_pc));
|
||||
boost::replace_first(party_info, "{Lv}", std::to_string(avg_level));
|
||||
if(num_pc == 1){
|
||||
boost::replace_first(party_info, "Avg. ", "");
|
||||
}
|
||||
|
||||
auto local_time = *std::localtime(&mtime);
|
||||
std::stringstream last_modified;
|
||||
last_modified << std::put_time(&local_time, "%h %d, %Y %I:%M %p");
|
||||
boost::replace_first(party_info, "{LastSaved}", last_modified.str());
|
||||
|
||||
if(!party_univ.party.scen_name.empty()){
|
||||
boost::replace_first(party_info, "{Scenario}", party_univ.scenario.scen_name);
|
||||
boost::replace_first(party_info, "{Location}", get_location(&party_univ));
|
||||
}else{
|
||||
boost::replace_first(party_info, "{Scenario}||{Location}", "");
|
||||
}
|
||||
|
||||
me["info" + suffix].setText(party_info);
|
||||
|
||||
// Set up buttons
|
||||
if(saving){
|
||||
me["file1"].setText(""); // Keep the frame
|
||||
me["auto" + suffix].hide();
|
||||
me["auto" + suffix + "-more-recent"].hide();
|
||||
me["save" + suffix].attachClickHandler(std::bind(&cFilePicker::doSave, this, file));
|
||||
}else{
|
||||
me["load" + suffix].attachClickHandler(std::bind(&cFilePicker::doLoad, this, file));
|
||||
// TODO check if a newer autosave exists
|
||||
me["auto" + suffix + "-more-recent"].hide();
|
||||
}
|
||||
}
|
||||
|
||||
void populate_page(int page) {
|
||||
int parties_needed = min(save_file_mtimes.size(), (page+1) * parties_per_page);
|
||||
while(saves_loaded < parties_needed){
|
||||
fs::path next_file = save_file_mtimes[saves_loaded].first;
|
||||
cUniverse party_univ;
|
||||
if(!load_party(next_file, save_files[saves_loaded])){
|
||||
// TODO show error, fatal? Show corrupted party?
|
||||
}
|
||||
saves_loaded++;
|
||||
}
|
||||
|
||||
if(saving){
|
||||
time_t now;
|
||||
std::time(&now);
|
||||
// Populate the first slot with the actual current party
|
||||
populate_slot(0, "", now, univ);
|
||||
}
|
||||
|
||||
int start_idx = page * parties_per_page;
|
||||
for(int party_idx = start_idx; party_idx < start_idx + parties_per_page; ++party_idx){
|
||||
int slot_idx = party_idx - start_idx;
|
||||
if(saving) slot_idx++;
|
||||
if(party_idx < parties_needed)
|
||||
populate_slot(slot_idx, save_file_mtimes[party_idx].first, save_file_mtimes[party_idx].second, save_files[party_idx]);
|
||||
else
|
||||
empty_slot(party_idx - start_idx);
|
||||
}
|
||||
|
||||
++pages_populated;
|
||||
}
|
||||
|
||||
bool doLoad(fs::path selected_file) {
|
||||
me.setResult(selected_file);
|
||||
me.toast(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool confirm_overwrite(fs::path selected_file) {
|
||||
cChoiceDlog dlog("confirm-overwrite", {"save","cancel"}, &me);
|
||||
cDialog& inner = *(dlog.operator->());
|
||||
std::string prompt = inner["prompt"].getText();
|
||||
boost::replace_first(prompt, "{File}", selected_file.filename().string());
|
||||
inner["prompt"].setText(prompt);
|
||||
std::string choice = dlog.show();
|
||||
return choice == "save";
|
||||
}
|
||||
|
||||
bool doSave(fs::path selected_file) {
|
||||
if(selected_file.empty()){
|
||||
selected_file = save_folder / me["file1-field"].getText();
|
||||
selected_file += ".exg";
|
||||
}
|
||||
if(!fs::exists(selected_file) || confirm_overwrite(selected_file)){
|
||||
me.setResult(selected_file);
|
||||
me.toast(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doCancel() {
|
||||
me.toast(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doSelectPage(int dir) {
|
||||
auto& stk = dynamic_cast<cStack&>(me["list"]);
|
||||
// This stack doesn't loop. It's easier to implement loading the files one page at a time
|
||||
// if I know we're not gonna jump from page 0 to the last page, leaving a gap in the vector.
|
||||
stk.doSelectPage(dir);
|
||||
me["prev"].show();
|
||||
me["next"].show();
|
||||
if(stk.getPage() == 0){
|
||||
me["prev"].hide();
|
||||
}
|
||||
if(stk.getPage() == stk.getPageCount() - 1){
|
||||
me["next"].hide();
|
||||
}
|
||||
|
||||
populate_page(stk.getPage());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doFileBrowser() {
|
||||
fs::path from_browser = nav_get_party();
|
||||
if(!from_browser.empty()){
|
||||
me.setResult(from_browser);
|
||||
me.toast(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
cFilePicker(fs::path save_folder, bool saving, bool picking_auto = false) :
|
||||
save_folder(save_folder),
|
||||
picking_auto(picking_auto),
|
||||
saving(saving),
|
||||
parties_per_page(saving ? SLOTS_PER_PAGE - 1 : SLOTS_PER_PAGE) {}
|
||||
|
||||
fs::path run() {
|
||||
template_info_str = me["info1"].getText();
|
||||
|
||||
if(saving){
|
||||
me["title-load"].hide();
|
||||
me["file1"].setText(""); // Keep the frame
|
||||
for(int i = 0; i < SLOTS_PER_PAGE; ++i){
|
||||
me["load" + std::to_string(i+1)].hide();
|
||||
}
|
||||
}else{
|
||||
me["title-save"].hide();
|
||||
me["file1-field"].hide();
|
||||
me["file1-extension-label"].hide();
|
||||
for(int i = 0; i < SLOTS_PER_PAGE; ++i){
|
||||
me["save" + std::to_string(i+1)].hide();
|
||||
}
|
||||
}
|
||||
|
||||
me["cancel"].attachClickHandler(std::bind(&cFilePicker::doCancel, this));
|
||||
// Since it would be crazy to record and replay the metadata shown on a player's save picker
|
||||
// dialog (which is what we do for the scenario picker),
|
||||
// when replaying, basically make every part of the picker no-op except cancel and view autosaves.
|
||||
// Load buttons should do the same thing as cancel.
|
||||
if(!replaying){
|
||||
me["next"].attachClickHandler(std::bind(&cFilePicker::doSelectPage, this, 1));
|
||||
me["prev"].attachClickHandler(std::bind(&cFilePicker::doSelectPage, this, -1));
|
||||
init_pages();
|
||||
me["find"].attachClickHandler(std::bind(&cFilePicker::doFileBrowser, this));
|
||||
}else{
|
||||
me["load"].attachClickHandler(std::bind(&cFilePicker::doCancel, this));
|
||||
}
|
||||
|
||||
// Hide the prev button and populate the first page
|
||||
doSelectPage(0);
|
||||
|
||||
me.run();
|
||||
if(!me.hasResult()) return "";
|
||||
fs::path file = me.getResult<fs::path>();
|
||||
return file;
|
||||
}
|
||||
};
|
||||
|
||||
static fs::path run_file_picker(fs::path save_folder, bool saving) {
|
||||
return cFilePicker(save_folder, saving).run();
|
||||
}
|
||||
|
||||
fs::path run_autosave_picker(fs::path save_file) {
|
||||
return "";
|
||||
}
|
||||
|
||||
fs::path run_file_picker(bool saving) {
|
||||
if(!has_feature_flag("file-picker-dialog", "V1") /* TODO check file picker preference */){
|
||||
if(saving)
|
||||
return nav_put_or_temp_party();
|
||||
else
|
||||
return nav_get_or_decode_party();
|
||||
}
|
||||
|
||||
// TODO this is set up to be configurable, but not yet exposed in preferences.
|
||||
fs::path save_folder = get_string_pref("SaveFolder", saveDir.string());
|
||||
|
||||
return run_file_picker(save_folder, saving);
|
||||
}
|
||||
|
||||
|
@@ -25,5 +25,8 @@ void pick_preferences(bool record = true);
|
||||
void save_prefs();
|
||||
void tip_of_day();
|
||||
struct scen_header_type pick_a_scen();
|
||||
fs::path run_file_picker(bool saving);
|
||||
// Pick from the autosaves made while playing in a given save file
|
||||
fs::path run_autosave_picker(fs::path save_file);
|
||||
|
||||
#endif
|
||||
|
@@ -341,6 +341,8 @@ void draw_startup_stats() {
|
||||
to_rect = party_to;
|
||||
to_rect.offset(pc_rect.left,pc_rect.top);
|
||||
pic_num_t pic = univ.party[i].which_graphic;
|
||||
// TODO This doesn't make sense. If we're in the startup menu, there are no scenario custom graphics.
|
||||
// Doesn't this need to find it saved in the party?
|
||||
if(pic >= 1000) {
|
||||
std::shared_ptr<const sf::Texture> gw;
|
||||
graf_pos_ref(gw, from_rect) = spec_scen_g.find_graphic(pic % 1000, pic >= 10000);
|
||||
|
@@ -99,7 +99,9 @@ std::map<std::string,std::vector<std::string>> feature_flags = {
|
||||
// Legacy behavior of the T debug action (used by some replays)
|
||||
// does not change the party's outdoors location
|
||||
{"debug-enter-town", {"move-outdoors"}},
|
||||
{"target-lock", {"V1"}}
|
||||
{"target-lock", {"V1"}},
|
||||
// New in-game save file picker
|
||||
{"file-picker-dialog", {"V1"}}
|
||||
};
|
||||
|
||||
struct cParseEntrance {
|
||||
|
@@ -133,6 +133,7 @@ void setWindowFloating(sf::Window& win, bool floating) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this check is only required when trying nav_get_* specifically, now that there's a save file picker
|
||||
void init_fileio(){
|
||||
// if init_fileio() is called more than once, only check once
|
||||
static bool checked_zenity = false;
|
||||
|
Reference in New Issue
Block a user