You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
555 lines
18 KiB
555 lines
18 KiB
#include "XournalMain.h" |
|
|
|
#include <memory> |
|
|
|
#include <glib/gstdio.h> |
|
#include <gtk/gtk.h> |
|
#include <libintl.h> |
|
|
|
#include "control/jobs/ImageExport.h" |
|
#include "control/jobs/ProgressListener.h" |
|
#include "gui/GladeSearchpath.h" |
|
#include "gui/MainWindow.h" |
|
#include "gui/XournalView.h" |
|
#include "pdf/base/XojPdfExport.h" |
|
#include "pdf/base/XojPdfExportFactory.h" |
|
#include "undo/EmergencySaveRestore.h" |
|
#include "xojfile/LoadHandler.h" |
|
|
|
#include "Control.h" |
|
#include "Stacktrace.h" |
|
#include "StringUtils.h" |
|
#include "XojMsgBox.h" |
|
#include "config-dev.h" |
|
#include "config-paths.h" |
|
#include "config.h" |
|
#include "filesystem.h" |
|
#include "i18n.h" |
|
|
|
#if __linux__ |
|
#include <libgen.h> |
|
#endif |
|
|
|
#include <algorithm> // std::sort |
|
|
|
|
|
XournalMain::XournalMain() = default; |
|
|
|
XournalMain::~XournalMain() = default; |
|
|
|
void XournalMain::initLocalisation() { |
|
#ifdef ENABLE_NLS |
|
|
|
#ifdef _WIN32 |
|
#undef PACKAGE_LOCALE_DIR |
|
#define PACKAGE_LOCALE_DIR "../share/locale/" |
|
#endif |
|
|
|
#ifdef __APPLE__ |
|
#undef PACKAGE_LOCALE_DIR |
|
fs::path p = Stacktrace::getExePath(); |
|
p /= "../Resources/share/locale/"; |
|
const char* PACKAGE_LOCALE_DIR = p.c_str(); |
|
#endif |
|
bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR); |
|
textdomain(GETTEXT_PACKAGE); |
|
|
|
#ifdef _WIN32 |
|
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); |
|
#endif |
|
|
|
#endif // ENABLE_NLS |
|
|
|
// Not working on Windows! Working on Linux, but not sure if it's needed |
|
#ifndef _WIN32 |
|
try { |
|
std::locale::global(std::locale("")); // "" - system default locale |
|
} catch (std::runtime_error& e) { |
|
g_warning("XournalMain: System default locale could not be set.\nCaused by: %s", e.what()); |
|
} |
|
#endif |
|
std::cout.imbue(std::locale()); |
|
} |
|
|
|
XournalMain::MigrateResult XournalMain::migrateSettings() { |
|
fs::path newConfigPath = Util::getConfigFolder(); |
|
|
|
if (!fs::exists(newConfigPath)) { |
|
std::array<fs::path, 1> oldPaths = { |
|
fs::u8path(g_get_home_dir()) /= ".xournalpp", |
|
}; |
|
for (auto const& oldPath: oldPaths) { |
|
if (!fs::is_directory(oldPath)) { |
|
continue; |
|
} |
|
g_message("Migrating configuration from %s to %s", oldPath.c_str(), newConfigPath.c_str()); |
|
Util::ensureFolderExists(newConfigPath.parent_path()); |
|
try { |
|
fs::copy(oldPath, newConfigPath, fs::copy_options::recursive); |
|
constexpr auto msg = "Due to a recent update, Xournal++ has changed where it's configuration files are " |
|
"stored.\nThey have been automatically copied from\n\t{1}\nto\n\t{2}"; |
|
return {MigrateStatus::Success, |
|
FS(_F(msg) % oldPath.u8string().c_str() % newConfigPath.u8string().c_str())}; |
|
} catch (fs::filesystem_error const& except) { |
|
constexpr auto msg = |
|
"Due to a recent update, Xournal++ has changed where it's configuration files are " |
|
"stored.\nHowever, when attempting to copy\n\t{1}\nto\n\t{2}\nmigration failed:\n{3}"; |
|
g_message("Migration failed: %s", except.what()); |
|
return {MigrateStatus::Failure, |
|
FS(_F(msg) % oldPath.u8string().c_str() % newConfigPath.u8string().c_str() % except.what())}; |
|
} |
|
} |
|
} |
|
return {MigrateStatus::NotNeeded, ""}; |
|
} |
|
|
|
void XournalMain::checkForErrorlog() { |
|
fs::path errorDir = Util::getCacheSubfolder(ERRORLOG_DIR); |
|
if (!fs::exists(errorDir)) { |
|
return; |
|
} |
|
|
|
vector<fs::path> errorList; |
|
for (auto const& f: fs::directory_iterator(errorDir)) { |
|
if (f.is_regular_file() && f.path().stem() == "errorlog") { |
|
errorList.emplace_back(f); |
|
} |
|
} |
|
|
|
if (errorList.empty()) { |
|
return; |
|
} |
|
|
|
std::sort(errorList.begin(), errorList.end()); |
|
string msg = |
|
errorList.size() == 1 ? |
|
_("There is an errorlogfile from Xournal++. Please send a Bugreport, so the bug may be fixed.") : |
|
_("There are errorlogfiles from Xournal++. Please send a Bugreport, so the bug may be fixed."); |
|
msg += "\n"; |
|
#if defined(GIT_BRANCH) && defined(GIT_REPO_OWNER) |
|
msg += FS(_F("You're using {1}/{2} branch. Send Bugreport will direct you to this repo's issue tracker.") % |
|
GIT_REPO_OWNER % GIT_BRANCH); |
|
msg += "\n"; |
|
#endif |
|
msg += FS(_F("The most recent log file name: {1}") % errorList[0].string()); |
|
|
|
GtkWidget* dialog = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", |
|
msg.c_str()); |
|
|
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Send Bugreport"), 1); |
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Open Logfile"), 2); |
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Open Logfile directory"), 3); |
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Delete Logfile"), 4); |
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Cancel"), 5); |
|
|
|
int res = gtk_dialog_run(GTK_DIALOG(dialog)); |
|
|
|
auto const& errorlogPath = Util::getCacheSubfolder(ERRORLOG_DIR) / errorList[0]; |
|
if (res == 1) // Send Bugreport |
|
{ |
|
Util::openFileWithDefaultApplication(PROJECT_BUGREPORT); |
|
Util::openFileWithDefaultApplication(errorlogPath); |
|
} else if (res == 2) // Open Logfile |
|
{ |
|
Util::openFileWithDefaultApplication(errorlogPath); |
|
} else if (res == 3) // Open Logfile directory |
|
{ |
|
Util::openFileWithFilebrowser(errorlogPath.parent_path()); |
|
} else if (res == 4) // Delete Logfile |
|
{ |
|
if (!fs::exists(errorlogPath)) { |
|
string msg = FS(_F("Errorlog cannot be deleted. You have to do it manually.\nLogfile: {1}") % |
|
errorlogPath.u8string()); |
|
XojMsgBox::showErrorToUser(nullptr, msg); |
|
} |
|
} else if (res == 5) // Cancel |
|
{ |
|
// Nothing to do |
|
} |
|
|
|
gtk_widget_destroy(dialog); |
|
} |
|
|
|
void XournalMain::checkForEmergencySave(Control* control) { |
|
auto file = Util::getConfigFile("emergencysave.xopp"); |
|
|
|
if (!fs::exists(file)) { |
|
return; |
|
} |
|
|
|
string msg = _("Xournal++ crashed last time. Would you like to restore the last edited file?"); |
|
|
|
GtkWidget* dialog = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", |
|
msg.c_str()); |
|
|
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Delete file"), 1); |
|
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Restore file"), 2); |
|
|
|
int res = gtk_dialog_run(GTK_DIALOG(dialog)); |
|
|
|
if (res == 1) // Delete file |
|
{ |
|
fs::remove(file); |
|
} else if (res == 2) // Open File |
|
{ |
|
if (control->openFile(file, -1, true)) { |
|
control->getDocument()->setFilepath(""); |
|
|
|
// Make sure the document is changed, there is a question to ask for save |
|
control->getUndoRedoHandler()->addUndoAction(std::make_unique<EmergencySaveRestore>()); |
|
control->updateWindowTitle(); |
|
fs::remove(file); |
|
} |
|
} |
|
|
|
gtk_widget_destroy(dialog); |
|
} |
|
|
|
auto XournalMain::exportImg(const char* input, const char* output) -> int { |
|
LoadHandler loader; |
|
|
|
Document* doc = loader.loadDocument(input); |
|
if (doc == nullptr) { |
|
g_error("%s", loader.getLastError().c_str()); |
|
return -2; |
|
} |
|
|
|
fs::path const path(output); |
|
|
|
ExportGraphicsFormat format = EXPORT_GRAPHICS_PNG; |
|
|
|
if (path.extension() == ".svg") { |
|
format = EXPORT_GRAPHICS_SVG; |
|
} |
|
|
|
PageRangeVector exportRange; |
|
exportRange.push_back(new PageRangeEntry(0, doc->getPageCount() - 1)); |
|
DummyProgressListener progress; |
|
|
|
ImageExport imgExport(doc, path, format, false, exportRange); |
|
imgExport.exportGraphics(&progress); |
|
|
|
for (PageRangeEntry* e: exportRange) { |
|
delete e; |
|
} |
|
exportRange.clear(); |
|
|
|
string errorMsg = imgExport.getLastErrorMsg(); |
|
if (!errorMsg.empty()) { |
|
g_message("Error exporting image: %s\n", errorMsg.c_str()); |
|
return -3; |
|
} |
|
|
|
g_message("%s", _("Image file successfully created")); |
|
|
|
return 0; // no error |
|
} |
|
|
|
auto XournalMain::exportPdf(const char* input, const char* output) -> int { |
|
LoadHandler loader; |
|
|
|
Document* doc = loader.loadDocument(input); |
|
if (doc == nullptr) { |
|
g_error("%s", loader.getLastError().c_str()); |
|
return -2; |
|
} |
|
|
|
GFile* file = g_file_new_for_commandline_arg(output); |
|
|
|
XojPdfExport* pdfe = XojPdfExportFactory::createExport(doc, nullptr); |
|
char* cpath = g_file_get_path(file); |
|
string path = cpath; |
|
g_free(cpath); |
|
g_object_unref(file); |
|
|
|
if (!pdfe->createPdf(path)) { |
|
g_error("%s", pdfe->getLastError().c_str()); |
|
|
|
delete pdfe; |
|
return -3; |
|
} |
|
delete pdfe; |
|
|
|
g_message("%s", _("PDF file successfully created")); |
|
|
|
return 0; // no error |
|
} |
|
|
|
auto XournalMain::run(int argc, char* argv[]) -> int { |
|
g_set_prgname("com.github.xournalpp.xournalpp"); |
|
this->initLocalisation(); |
|
MigrateResult migrateResult = this->migrateSettings(); |
|
|
|
GError* error = nullptr; |
|
GOptionContext* context = g_option_context_new("FILE"); |
|
|
|
gchar** optFilename = nullptr; |
|
gchar* pdfFilename = nullptr; |
|
gchar* imgFilename = nullptr; |
|
gboolean showVersion = false; |
|
int openAtPageNumber = -1; |
|
|
|
string create_pdf = _("PDF output filename"); |
|
string create_img = _("Image output filename (.png / .svg)"); |
|
string page_jump = _("Jump to Page (first Page: 1)"); |
|
string audio_folder = _("Absolute path for the audio files playback"); |
|
string version = _("Get version of xournalpp"); |
|
GOptionEntry options[] = {{"create-pdf", 'p', 0, G_OPTION_ARG_FILENAME, &pdfFilename, create_pdf.c_str(), nullptr}, |
|
{"create-img", 'i', 0, G_OPTION_ARG_FILENAME, &imgFilename, create_img.c_str(), nullptr}, |
|
{"page", 'n', 0, G_OPTION_ARG_INT, &openAtPageNumber, page_jump.c_str(), "N"}, |
|
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &optFilename, "<input>", nullptr}, |
|
{"version", 0, 0, G_OPTION_ARG_NONE, &showVersion, version.c_str(), nullptr}, |
|
{nullptr}}; |
|
|
|
g_option_context_add_main_entries(context, options, GETTEXT_PACKAGE); |
|
// parse options, so we don't need gtk_init, but don't init display (so we have a commandline mode) |
|
g_option_context_add_group(context, gtk_get_option_group(false)); |
|
if (!g_option_context_parse(context, &argc, &argv, &error)) { |
|
g_error("%s", error->message); |
|
g_error_free(error); |
|
gchar* help = g_option_context_get_help(context, true, nullptr); |
|
g_message("%s", help); |
|
g_free(help); |
|
error = nullptr; |
|
} |
|
g_option_context_free(context); |
|
|
|
if (pdfFilename && optFilename && *optFilename) { |
|
return exportPdf(*optFilename, pdfFilename); |
|
} |
|
if (imgFilename && optFilename && *optFilename) { |
|
return exportImg(*optFilename, imgFilename); |
|
} |
|
|
|
if (showVersion) { |
|
g_printf("%s %s \n", PROJECT_NAME, PROJECT_VERSION); |
|
g_printf("└──%s: %d.%d.%d \n", "libgtk", gtk_get_major_version(), gtk_get_minor_version(), |
|
gtk_get_micro_version()); |
|
return 0; |
|
} |
|
|
|
// Checks for input method compatibility |
|
|
|
const char* imModule = g_getenv("GTK_IM_MODULE"); |
|
if (imModule != nullptr && strcmp(imModule, "xim") == 0) { |
|
g_setenv("GTK_IM_MODULE", "ibus", true); |
|
g_warning("Unsupported input method: xim, changed to: ibus"); |
|
} |
|
|
|
// Init GTK Display |
|
gtk_init(&argc, &argv); |
|
|
|
auto* gladePath = new GladeSearchpath(); |
|
initResourcePath(gladePath, "ui/about.glade"); |
|
initResourcePath(gladePath, "ui/xournalpp.css", |
|
false); // will notify user if file not present. Path ui/ already added above. |
|
|
|
|
|
auto* control = new Control(gladePath); |
|
|
|
auto icon = gladePath->getFirstSearchPath() / "icons"; |
|
gtk_icon_theme_prepend_search_path(gtk_icon_theme_get_default(), icon.u8string().c_str()); |
|
|
|
if (control->getSettings()->isDarkTheme()) { |
|
auto icon = gladePath->getFirstSearchPath() / "iconsDark"; |
|
gtk_icon_theme_prepend_search_path(gtk_icon_theme_get_default(), icon.u8string().c_str()); |
|
} |
|
|
|
auto& globalLatexTemplatePath = control->getSettings()->latexSettings.globalTemplatePath; |
|
if (globalLatexTemplatePath.empty()) { |
|
globalLatexTemplatePath = findResourcePath("resources/") / "default_template.tex"; |
|
g_message("Using default latex template in %s", globalLatexTemplatePath.string().c_str()); |
|
control->getSettings()->save(); |
|
} |
|
|
|
auto* win = new MainWindow(gladePath, control); |
|
control->initWindow(win); |
|
|
|
win->show(nullptr); |
|
bool opened = false; |
|
if (optFilename) { |
|
if (g_strv_length(optFilename) != 1) { |
|
string msg = _("Sorry, Xournal++ can only open one file at once.\n" |
|
"Others are ignored."); |
|
XojMsgBox::showErrorToUser(static_cast<GtkWindow*>(*win), msg); |
|
} |
|
|
|
fs::path p = Util::fromGFilename(optFilename[0], false); |
|
|
|
try { |
|
if (fs::exists(p)) { |
|
opened = control->openFile(p, openAtPageNumber); |
|
} else { |
|
opened = control->newFile("", p); |
|
} |
|
} catch (fs::filesystem_error const& e) { |
|
string msg = FS(_F("Sorry, Xournal++ cannot open remote files at the moment.\n" |
|
"You have to copy the file to a local directory.") % |
|
p.u8string().c_str() % e.what()); |
|
XojMsgBox::showErrorToUser(static_cast<GtkWindow*>(*win), msg); |
|
opened = control->newFile("", p); |
|
} |
|
} |
|
|
|
control->getScheduler()->start(); |
|
|
|
if (!opened) { |
|
control->newFile(); |
|
} |
|
|
|
checkForErrorlog(); |
|
checkForEmergencySave(control); |
|
|
|
// There is a timing issue with the layout |
|
// This fixes it, see #405 |
|
Util::execInUiThread([=]() { control->getWindow()->getXournal()->layoutPages(); }); |
|
|
|
if (migrateResult.status != MigrateStatus::NotNeeded) { |
|
Util::execInUiThread([=]() { XojMsgBox::showErrorToUser(control->getGtkWindow(), migrateResult.message); }); |
|
} |
|
|
|
gtk_main(); |
|
|
|
control->saveSettings(); |
|
|
|
win->getXournal()->clearSelection(); |
|
|
|
control->getScheduler()->stop(); |
|
|
|
delete win; |
|
delete control; |
|
delete gladePath; |
|
|
|
return 0; |
|
} |
|
|
|
/** |
|
* Find a file in a resource folder, and return the resource folder path |
|
* Return an empty string, if the folder was not found |
|
*/ |
|
auto XournalMain::findResourcePath(const string& searchFile) -> fs::path { |
|
// First check if the files are available relative to the path |
|
// So a "portable" installation will be possible |
|
fs::path relative1 = searchFile; |
|
|
|
if (fs::exists(relative1)) { |
|
return relative1.parent_path(); |
|
} |
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
// Check if we are in the "build" directory, and therefore the resources |
|
// are installed two folders back |
|
fs::path relative2 = "../.."; |
|
relative2 /= searchFile; |
|
|
|
if (fs::exists(relative2)) { |
|
return relative2.parent_path(); |
|
} |
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
fs::path executableDir = Stacktrace::getExePath(); |
|
executableDir = executableDir.parent_path(); |
|
|
|
// First check if the files are available relative to the executable |
|
// So a "portable" installation will be possible |
|
fs::path relative3 = executableDir; |
|
relative3 /= searchFile; |
|
|
|
if (fs::exists(relative3)) { |
|
return relative3.parent_path(); |
|
} |
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
// Check one folder back, for windows portable |
|
fs::path relative4 = executableDir; |
|
relative4 /= ".."; |
|
relative4 /= searchFile; |
|
|
|
if (fs::exists(relative4)) { |
|
return relative4.parent_path(); |
|
} |
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
// Check if we are in the "build" directory, and therefore the resources |
|
// are installed two folders back |
|
fs::path relative5 = executableDir; |
|
relative5 /= "../.."; |
|
relative5 /= searchFile; |
|
|
|
if (fs::exists(relative5)) { |
|
return relative5.parent_path(); |
|
} |
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
// Check for .../share resources directory relative to binary to support |
|
// relocatable installations (such as e.g., AppImages) |
|
fs::path relative6 = executableDir; |
|
relative6 /= "../share/xournalpp/"; |
|
relative6 /= searchFile; |
|
|
|
if (fs::exists(relative6)) { |
|
return relative6.parent_path(); |
|
} |
|
|
|
// Not found |
|
return {}; |
|
} |
|
|
|
void XournalMain::initResourcePath(GladeSearchpath* gladePath, const gchar* relativePathAndFile, bool failIfNotFound) { |
|
auto uiPath = findResourcePath(relativePathAndFile); // i.e. relativePathAndFile = "ui/about.glade" |
|
|
|
if (!uiPath.empty()) { |
|
gladePath->addSearchDirectory(uiPath); |
|
return; |
|
} |
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
#ifdef __APPLE__ |
|
fs::path p = Stacktrace::getExePath(); |
|
p /= "../Resources"; |
|
p /= relativePathAndFile; |
|
|
|
if (fs::exists(p)) { |
|
gladePath->addSearchDirectory(p.parent_path()); |
|
return; |
|
} |
|
|
|
string msg = FS(_F("Missing the needed UI file:\n{1}\n .app corrupted?\nPath: {2}") % relativePathAndFile % |
|
p.u8string()); |
|
|
|
if (!failIfNotFound) { |
|
msg += _("\nWill now attempt to run without this file."); |
|
} |
|
XojMsgBox::showErrorToUser(nullptr, msg); |
|
#else |
|
// Check at the target installation directory |
|
fs::path absolute = PACKAGE_DATA_DIR; |
|
absolute /= PROJECT_PACKAGE; |
|
absolute /= relativePathAndFile; |
|
|
|
if (fs::exists(absolute)) { |
|
gladePath->addSearchDirectory(absolute.parent_path()); |
|
return; |
|
} |
|
|
|
|
|
string msg = FS(_F("<span foreground='red' size='x-large'>Missing the needed UI file:\n<b>{1}</b></span>\nCould " |
|
"not find them at any location.\n Not relative\n Not in the Working Path\n Not in {2}") % |
|
relativePathAndFile % PACKAGE_DATA_DIR); |
|
|
|
if (!failIfNotFound) { |
|
msg += _("\n\nWill now attempt to run without this file."); |
|
} |
|
XojMsgBox::showErrorToUser(nullptr, msg); |
|
#endif |
|
|
|
if (failIfNotFound) { |
|
exit(12); |
|
} |
|
}
|
|
|