Skip to content

Commit

Permalink
Refactor Control::save() and Control::saveAs()
Browse files Browse the repository at this point in the history
        Those functions are now non-blocking and (possibly) call dialogs that are compatible with gtk4.
  • Loading branch information
bhennion committed Apr 20, 2024
1 parent e0c092e commit 4b7bdfa
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 234 deletions.
165 changes: 53 additions & 112 deletions src/core/control/Control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
#include "gui/dialog/SettingsDialog.h" // for Sett...
#include "gui/dialog/ToolbarManageDialog.h" // for Tool...
#include "gui/dialog/XojOpenDlg.h" // for XojO...
#include "gui/dialog/XojSaveDlg.h" // for XojS...
#include "gui/dialog/toolbarCustomize/ToolbarDragDropHandler.h" // for Tool...
#include "gui/inputdevices/CompassInputHandler.h" // for Comp...
#include "gui/inputdevices/GeometryToolInputHandler.h" // for Geom...
Expand Down Expand Up @@ -1703,83 +1704,6 @@ void Control::setCurrentState(size_t state) {
});
}

auto Control::save(bool synchron) -> bool {
// clear selection before saving
clearSelectionEndText();

this->doc->lock();
fs::path filepath = this->doc->getFilepath();
this->doc->unlock();

if (filepath.empty()) {
if (!showSaveDialog()) {
return false;
}
}

auto* job = new SaveJob(this);
bool result = true;
if (synchron) {
result = job->save();
unblock();
this->resetSavedStatus();
} else {
this->scheduler->addJob(job, JOB_PRIORITY_URGENT);
}
job->unref();

return result;
}

auto Control::showSaveDialog() -> bool {
GtkWidget* dialog =
gtk_file_chooser_dialog_new(_("Save File"), getGtkWindow(), GTK_FILE_CHOOSER_ACTION_SAVE, _("_Cancel"),
GTK_RESPONSE_CANCEL, _("_Save"), GTK_RESPONSE_OK, nullptr);

GtkFileFilter* filterXoj = gtk_file_filter_new();
gtk_file_filter_set_name(filterXoj, _("Xournal++ files"));
gtk_file_filter_add_mime_type(filterXoj, "application/x-xopp");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filterXoj);

this->doc->lock();
auto suggested_folder = this->doc->createSaveFolder(this->settings->getLastSavePath());
auto suggested_name = this->doc->createSaveFilename(Document::XOPP, this->settings->getDefaultSaveName());
this->doc->unlock();

gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), Util::toGFile(suggested_folder).get(), nullptr);
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), Util::toGFilename(suggested_name).c_str());
gtk_file_chooser_add_shortcut_folder(GTK_FILE_CHOOSER(dialog),
Util::toGFile(this->settings->getLastOpenPath()).get(), nullptr);

while (true) {
if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_OK) {
gtk_widget_destroy(dialog);
return false;
}

auto fileTmp = Util::fromGFile(
xoj::util::GObjectSPtr<GFile>(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog)), xoj::util::adopt)
.get());
Util::clearExtensions(fileTmp);
fileTmp += ".xopp";
// Since we add the extension after the OK button, we have to check manually on existing files
if (askToReplace(fileTmp, GTK_WINDOW(dialog))) {
break;
}
}

auto file = Util::fromGFile(
xoj::util::GObjectSPtr<GFile>(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog)), xoj::util::adopt).get());
settings->setLastSavePath(file.parent_path());
gtk_widget_destroy(dialog);

this->doc->lock();
this->doc->setFilepath(file);
this->doc->unlock();

return true;
}

void Control::showFontDialog() {
this->actionDB->enableAction(Action::SELECT_FONT, false); // Only one dialog
auto* dlg = gtk_font_chooser_dialog_new(_("Select font"), GTK_WINDOW(this->win->getWindow()));
Expand Down Expand Up @@ -1880,21 +1804,47 @@ void Control::exportAs() {
job->showDialogAndRun();
}

auto Control::saveAs(bool synchron) -> bool {
if (!showSaveDialog()) {
return false;
}
void Control::save(std::function<void(bool)> callback) { saveImpl(false, std::move(callback)); }

void Control::saveAs(std::function<void(bool)> callback) { saveImpl(true, std::move(callback)); }

void Control::saveImpl(bool saveAs, std::function<void(bool)> callback) {
// clear selection before saving
clearSelectionEndText();

this->doc->lock();
auto filepath = doc->getFilepath();
fs::path filepath = this->doc->getFilepath();
this->doc->unlock();

if (filepath.empty()) {
return false;
}
auto doSave = [ctrl = this, cb = std::move(callback)]() {
// clear selection before saving
ctrl->clearSelectionEndText();

auto* job = new SaveJob(ctrl, std::move(cb));
ctrl->scheduler->addJob(job, JOB_PRIORITY_URGENT);
job->unref();
};

// no lock needed, this is an uncritical operation
this->doc->setCreateBackupOnSave(false);
return save(synchron);
if (saveAs || filepath.empty()) {
// No need to backup the old saved file, as there is none
this->doc->lock();
this->doc->setCreateBackupOnSave(false);
auto suggestedPath = this->doc->createSaveFolder(this->settings->getLastSavePath());
suggestedPath /= this->doc->createSaveFilename(Document::XOPP, this->settings->getDefaultSaveName());
this->doc->unlock();
xoj::SaveExportDialog::showSaveFileDialog(getGtkWindow(), settings, std::move(suggestedPath),
[doSave = std::move(doSave), ctrl = this](std::optional<fs::path> p) {
if (p && !p->empty()) {
ctrl->settings->setLastSavePath(p->parent_path());
ctrl->doc->lock();
ctrl->doc->setFilepath(std::move(p.value()));
ctrl->doc->unlock();
doSave();
}
});
} else {
doSave();
}
}

void Control::resetSavedStatus() {
Expand Down Expand Up @@ -1940,30 +1890,31 @@ void Control::close(std::function<void(bool)> callback, const bool allowDestroy,

bool safeToClose = !undoRedo->isChanged();
if (!safeToClose) {
const bool fileRemoved = !doc->getFilepath().empty() && !fs::exists(this->doc->getFilepath());
fs::path path = doc->getFilepath();
const bool fileRemoved = !path.empty() && !fs::exists(path);
const auto message = fileRemoved ? _("Document file was removed.") : _("This document is not saved yet.");
const auto saveLabel = fileRemoved ? _("Save As...") : _("Save");
const bool saveAs = fileRemoved || path.empty();
const auto saveLabel = saveAs ? _("Save As...") : _("Save");

enum { SAVE = 1, DISCARD, CANCEL };
std::vector<XojMsgBox::Button> buttons = {{saveLabel, SAVE}, {_("Discard"), DISCARD}};
if (allowCancel) {
buttons.emplace_back(_("Cancel"), CANCEL);
}
XojMsgBox::askQuestion(getGtkWindow(), message, std::string(), std::move(buttons),
[ctrl = this, fileRemoved, allowDestroy, callback = std::move(callback)](int response) {
bool safeToClose = true;
[ctrl = this, saveAs, allowDestroy, callback = std::move(callback)](int response) {
auto execAfter = [allowDestroy, ctrl, cb = std::move(callback)](bool saved) {
if (saved && allowDestroy) {
ctrl->closeDocument();
}
cb(saved);
};
if (response == SAVE) {
safeToClose = fileRemoved ? ctrl->saveAs(true) : ctrl->save(true);
} else if (response == DISCARD) {
safeToClose = true;
} else {
safeToClose = false;
}

if (safeToClose && allowDestroy) {
ctrl->closeDocument();
ctrl->saveImpl(saveAs, std::move(execAfter));
return;
}
callback(safeToClose);
bool safeToClose = response == DISCARD;
execAfter(safeToClose);
});
} else {
if (allowDestroy) {
Expand Down Expand Up @@ -1994,16 +1945,6 @@ void Control::initButtonTool() {
}
}

auto Control::askToReplace(fs::path const& filepath, GtkWindow* parent) const -> bool {
if (fs::exists(filepath)) {
std::string msg = FS(FORMAT_STR("The file {1} already exists! Do you want to replace it?") %
filepath.filename().u8string());
int res = XojMsgBox::replaceFileQuestion(parent, msg);
return res == GTK_RESPONSE_OK;
}
return true;
}

void Control::showAbout() {
auto popup = xoj::popup::PopupWindowWrapper<xoj::popup::AboutDialog>(this->gladeSearchPath);
popup.show(GTK_WINDOW(this->win->getWindow()));
Expand Down
21 changes: 9 additions & 12 deletions src/core/control/Control.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,14 @@ class Control:
void quit(bool allowCancel = true);

/**
* Save the current document.
*
* @param synchron Whether the save should be run synchronously or asynchronously.
* @brief Asynchronously saves the document and calls callback afterwards with boolean parameter true on success.
* May ask the user for a place to save if necessary.
*/
void save(std::function<void(bool)> callback = [](bool) {});
/**
* @brief Asks the user for a new location, asynchronously saves the document there and calls callback afterwards.
*/
bool save(bool synchron = false);
bool saveAs(bool synchron = false);
void saveAs(std::function<void(bool)> callback = [](bool) {});

/**
* Marks the current document as saved if it is currently marked as unsaved.
Expand All @@ -148,13 +150,6 @@ class Control:
*/
void close(std::function<void(bool)> callback, bool allowDestroy = false, bool allowCancel = true);

/**
* @brief Asks user to replace an existing file when saving / exporting, since we add the extension
* after the OK, we need to check manually
* @param parent the dialog which wants to override something
*/
bool askToReplace(fs::path const& filepath, GtkWindow* parent) const;

// Menu edit
void showSettings();

Expand Down Expand Up @@ -390,6 +385,8 @@ class Control:
bool loadXoptTemplate(fs::path const& filepath);
bool loadPdf(fs::path const& filepath, int scrollToPage);

void saveImpl(bool saveAs, std::function<void(bool)> callback);

private:
/**
* @brief Creates the specified geometric tool if it's not on the current page yet. Deletes it if it already exists.
Expand Down
96 changes: 28 additions & 68 deletions src/core/control/jobs/BaseExportJob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "control/jobs/BlockingJob.h" // for BlockingJob
#include "control/settings/Settings.h" // for Settings
#include "gui/MainWindow.h" // for MainWindow
#include "gui/dialog/XojSaveDlg.h"
#include "model/Document.h" // for Document, Document::PDF
#include "util/PathUtil.h" // for toGFilename, clearExtensions
#include "util/PopupWindowWrapper.h" // for PopupWindowWrapper
Expand All @@ -28,104 +29,63 @@ void BaseExportJob::addFileFilterToDialog(GtkFileChooser* dialog, const std::str

auto BaseExportJob::checkOverwriteBackgroundPDF(fs::path const& file) const -> bool {
auto backgroundPDF = control->getDocument()->getPdfFilepath();
// If there is no background, we can return
try {
if (!fs::exists(backgroundPDF)) {
if (backgroundPDF.empty() || !fs::exists(backgroundPDF)) {
// If there is no background, we can return
return true;
}
// If the new file name (with the selected extension) is the previously selected pdf, warn the user

if (fs::weakly_canonical(file) == fs::weakly_canonical(backgroundPDF)) {
// If the new file name (with the selected extension) is the previously selected pdf, warn the user
std::string msg = _("Do not overwrite the background PDF! This will cause errors!");
XojMsgBox::showErrorToUser(control->getGtkWindow(), msg);
return false;
}
} catch (const fs::filesystem_error& fe) {
g_warning("%s", fe.what());
auto msg = std::string(_("The check for overwriting the background failed with:\n")) + fe.what() +
_("\n Do you want to continue?");
return XojMsgBox::replaceFileQuestion(control->getGtkWindow(), msg) == GTK_RESPONSE_OK;
auto msg = std::string(_("The check for overwriting the background failed with:\n")) + fe.what();
XojMsgBox::showErrorToUser(control->getGtkWindow(), msg);
return false;
}
return true;
}

void BaseExportJob::showFileChooser(std::function<void()> onFileSelected, std::function<void()> onCancel) {
auto* dialog =
gtk_file_chooser_dialog_new(_("Export"), control->getGtkWindow(), GTK_FILE_CHOOSER_ACTION_SAVE,
_("_Cancel"), GTK_RESPONSE_CANCEL, _("_Export"), GTK_RESPONSE_OK, nullptr);
addFilterToDialog(GTK_FILE_CHOOSER(dialog));

Settings* settings = control->getSettings();
Document* doc = control->getDocument();
doc->lock();
fs::path folder = doc->createSaveFolder(settings->getLastSavePath());
fs::path name = doc->createSaveFilename(Document::PDF, settings->getDefaultSaveName(), settings->getDefaultPdfExportName());
fs::path suggestedPath = doc->createSaveFolder(settings->getLastSavePath());
suggestedPath /=
doc->createSaveFilename(Document::PDF, settings->getDefaultSaveName(), settings->getDefaultPdfExportName());
doc->unlock();

gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), Util::toGFile(folder).get(), nullptr);
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), Util::toGFilename(name).c_str());

class FileDlg final {
public:
FileDlg(GtkDialog* dialog, BaseExportJob* job, std::function<void()> onFileSelected,
std::function<void()> onCancel):
window(GTK_WINDOW(dialog)),
job(job),
onFileSelected(std::move(onFileSelected)),
onCancel(std::move(onCancel)) {
this->signalId = g_signal_connect(
dialog, "response", G_CALLBACK((+[](GtkDialog* dialog, int response, gpointer data) {
FileDlg* self = static_cast<FileDlg*>(data);
auto* job = self->job;
if (response == GTK_RESPONSE_OK) {
auto file = Util::fromGFile(
xoj::util::GObjectSPtr<GFile>(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog)),
xoj::util::adopt)
.get());
Util::clearExtensions(file);
// Since we add the extension after the OK button, we have to check manually on existing
// files
const char* filterName =
gtk_file_filter_get_name(gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog)));
if (job->testAndSetFilepath(std::move(file), filterName)) {
auto doExport = [self, dialog](const fs::path& file) {
// Closing the window causes another "response" signal, which we want to ignore
g_signal_handler_disconnect(dialog, self->signalId);
gtk_window_close(GTK_WINDOW(dialog));
self->job->control->getSettings()->setLastSavePath(file.parent_path());
self->onFileSelected();
};
XojMsgBox::replaceFileQuestion(GTK_WINDOW(dialog), job->filepath, std::move(doExport));
} // else the dialog stays on until a suitable destination is found or cancel is hit.
} else {
self->onCancel();
// Closing the window causes another "response" signal, which we want to ignore
g_signal_handler_disconnect(dialog, self->signalId);
gtk_window_close(GTK_WINDOW(dialog)); // Deletes self, don't do anything after this
}
})),
this);
auto pathValidation = [job = this](fs::path& p, const char* filterName) {
return job->testAndSetFilepath(p, filterName);
};

auto callback = [settings, onFileSelected = std::move(onFileSelected),
onCancel = std::move(onCancel)](std::optional<fs::path> p) {
if (p && !p->empty()) {
settings->setLastSavePath(p->parent_path());
onFileSelected();
} else {
onCancel();
}
~FileDlg() = default;
};

inline GtkWindow* getWindow() const { return window.get(); }
auto popup = xoj::popup::PopupWindowWrapper<xoj::SaveExportDialog>(control->getSettings(), std::move(suggestedPath),
_("Export File"), _("Export"),
std::move(pathValidation), std::move(callback));

private:
xoj::util::GtkWindowUPtr window;
BaseExportJob* job;
std::function<void()> onFileSelected;
std::function<void()> onCancel;
gulong signalId;
};
auto* fc = GTK_FILE_CHOOSER(popup.getPopup()->getWindow());
addFilterToDialog(fc);

auto popup = xoj::popup::PopupWindowWrapper<FileDlg>(GTK_DIALOG(dialog), this, std::move(onFileSelected),
std::move(onCancel));
popup.show(GTK_WINDOW(this->control->getWindow()->getWindow()));
}

auto BaseExportJob::testAndSetFilepath(const fs::path& file, const char* /*filterName*/) -> bool {
try {
if (fs::is_directory(file.parent_path())) {
if (!file.empty() && fs::is_directory(file.parent_path())) {
this->filepath = file;
return true;
}
Expand Down
Loading

0 comments on commit 4b7bdfa

Please sign in to comment.