#include "Control.h" #include #include #include #include #include "enums/ActionType.enum.h" #include "gui/TextEditor.h" #include "gui/XournalView.h" #include "gui/XournalppCursor.h" #include "gui/dialog/AboutDialog.h" #include "gui/dialog/FillOpacityDialog.h" #include "gui/dialog/FormatDialog.h" #include "gui/dialog/GotoDialog.h" #include "gui/dialog/PageTemplateDialog.h" #include "gui/dialog/SelectBackgroundColorDialog.h" #include "gui/dialog/SettingsDialog.h" #include "gui/dialog/ToolbarManageDialog.h" #include "gui/dialog/toolbarCustomize/ToolbarDragDropHandler.h" #include "gui/inputdevices/HandRecognition.h" #include "gui/toolbarMenubar/model/ToolbarData.h" #include "gui/toolbarMenubar/model/ToolbarModel.h" #include "jobs/AutosaveJob.h" #include "jobs/CustomExportJob.h" #include "jobs/PdfExportJob.h" #include "jobs/SaveJob.h" #include "layer/LayerController.h" #include "model/StrokeStyle.h" #include "pagetype/PageTypeHandler.h" #include "plugin/PluginController.h" #include "serializing/ObjectInputStream.h" #include "settings/ButtonConfig.h" #include "settings/SettingsEnums.h" #include "stockdlg/XojOpenDlg.h" #include "undo/AddUndoAction.h" #include "undo/DeleteUndoAction.h" #include "undo/InsertDeletePageUndoAction.h" #include "undo/InsertUndoAction.h" #include "view/TextView.h" #include "xojfile/LoadHandler.h" #include "CrashHandler.h" #include "FullscreenHandler.h" #include "LatexController.h" #include "PageBackgroundChangeController.h" #include "PathUtil.h" #include "PrintHandler.h" #include "Stacktrace.h" #include "StringUtils.h" #include "UndoRedoController.h" #include "Util.h" #include "XojMsgBox.h" #include "config-dev.h" #include "config.h" #include "i18n.h" Control::Control(GApplication* gtkApp, GladeSearchpath* gladeSearchPath): gtkApp(gtkApp) { this->recent = new RecentManager(); this->undoRedo = new UndoRedoHandler(this); this->recent->addListener(this); this->undoRedo->addUndoRedoListener(this); this->isBlocking = false; this->gladeSearchPath = gladeSearchPath; this->metadata = new MetadataManager(); this->cursor = new XournalppCursor(this); this->lastAction = ACTION_NONE; this->lastGroup = GROUP_NOGROUP; this->lastEnabled = false; auto name = Util::getConfigFile(SETTINGS_XML_FILE); this->settings = new Settings(std::move(name)); this->settings->load(); this->applyPreferredLanguage(); TextView::setDpi(settings->getDisplayDpi()); this->pageTypes = new PageTypeHandler(gladeSearchPath); this->newPageType = std::make_unique(this->pageTypes, settings, true, true); this->audioController = new AudioController(this->settings, this); this->scrollHandler = new ScrollHandler(this); this->scheduler = new XournalScheduler(); this->doc = new Document(this); // for crashhandling setEmergencyDocument(this->doc); this->zoom = new ZoomControl(); this->zoom->setZoomStep(this->settings->getZoomStep() / 100.0); this->zoom->setZoomStepScroll(this->settings->getZoomStepScroll() / 100.0); this->zoom->setZoom100Value(this->settings->getDisplayDpi() / Util::DPI_NORMALIZATION_FACTOR); this->toolHandler = new ToolHandler(this, this, this->settings); this->toolHandler->loadSettings(); this->initButtonTool(); /** * This is needed to update the previews */ this->changeTimout = g_timeout_add_seconds(5, reinterpret_cast(checkChangedDocument), this); this->pageBackgroundChangeController = new PageBackgroundChangeController(this); this->layerController = new LayerController(this); this->layerController->registerListener(this); this->fullscreenHandler = new FullscreenHandler(settings); this->pluginController = new PluginController(this); this->pluginController->registerToolbar(); } Control::~Control() { g_source_remove(this->changeTimout); this->enableAutosave(false); deleteLastAutosaveFile(""); this->scheduler->stop(); this->changedPages.clear(); // can be removed, will be done by implicit destructor delete this->pluginController; this->pluginController = nullptr; delete this->clipboardHandler; this->clipboardHandler = nullptr; delete this->recent; this->recent = nullptr; delete this->undoRedo; this->undoRedo = nullptr; delete this->settings; this->settings = nullptr; delete this->toolHandler; this->toolHandler = nullptr; delete this->sidebar; this->sidebar = nullptr; delete this->doc; this->doc = nullptr; delete this->searchBar; this->searchBar = nullptr; delete this->scrollHandler; this->scrollHandler = nullptr; delete this->pageTypes; this->pageTypes = nullptr; delete this->metadata; this->metadata = nullptr; delete this->cursor; this->cursor = nullptr; delete this->zoom; this->zoom = nullptr; delete this->scheduler; this->scheduler = nullptr; delete this->dragDropHandler; this->dragDropHandler = nullptr; delete this->audioController; this->audioController = nullptr; delete this->pageBackgroundChangeController; this->pageBackgroundChangeController = nullptr; delete this->layerController; this->layerController = nullptr; delete this->fullscreenHandler; this->fullscreenHandler = nullptr; } void Control::renameLastAutosaveFile() { if (this->lastAutosaveFilename.empty()) { return; } auto const& filename = this->lastAutosaveFilename; auto renamed = Util::getAutosaveFilepath(); Util::clearExtensions(renamed); if (!filename.empty() && filename.string().front() != '.') { // This file must be a fresh, unsaved document. Since this file is // already in ~/.xournalpp/autosave/, we need to change the renamed filename. renamed += ".old.autosave.xopp"; } else { // The file is a saved document with the form "..autosave.xopp" renamed += filename.filename(); } g_message("%s", FS(_F("Autosave renamed from {1} to {2}") % this->lastAutosaveFilename.string() % renamed.string()) .c_str()); if (!fs::exists(filename)) { this->save(false); } std::vector errors; try { Util::safeRenameFile(filename, renamed); } catch (fs::filesystem_error const& e) { auto fmtstr = _F("Could not rename autosave file from \"{1}\" to \"{2}\": {3}"); errors.emplace_back(FS(fmtstr % filename.u8string() % renamed.u8string() % e.what())); } if (!errors.empty()) { string error = std::accumulate(errors.begin() + 1, errors.end(), *errors.begin(), [](const string& e1, const string& e2) { return e1 + "\n" + e2; }); Util::execInUiThread([=]() { string msg = FS(_F("Autosave failed with an error: {1}") % error); XojMsgBox::showErrorToUser(getGtkWindow(), msg); }); } } void Control::setLastAutosaveFile(fs::path newAutosaveFile) { this->lastAutosaveFilename = std::move(newAutosaveFile); } void Control::deleteLastAutosaveFile(fs::path newAutosaveFile) { fs::remove(this->lastAutosaveFilename); this->lastAutosaveFilename = std::move(newAutosaveFile); } auto Control::checkChangedDocument(Control* control) -> bool { if (!control->doc->tryLock()) { // call again later return true; } for (auto const& page: control->changedPages) { int p = control->doc->indexOf(page); if (p != -1) { control->firePageChanged(p); } } control->changedPages.clear(); control->doc->unlock(); // Call again return true; } void Control::saveSettings() { this->toolHandler->saveSettings(); gint width = 0; gint height = 0; gtk_window_get_size(getGtkWindow(), &width, &height); if (!this->win->isMaximized()) { this->settings->setMainWndSize(width, height); } this->settings->setMainWndMaximized(this->win->isMaximized()); this->sidebar->saveSize(); } void Control::initWindow(MainWindow* win) { win->setRecentMenu(recent->getMenu()); selectTool(toolHandler->getToolType()); this->win = win; this->sidebar = new Sidebar(win, this); XojMsgBox::setDefaultWindow(getGtkWindow()); updatePageNumbers(0, npos); toolHandler->eraserTypeChanged(); this->searchBar = new SearchBar(this); // Disable undo buttons undoRedoChanged(); if (settings->isPresentationMode()) { setViewPresentationMode(true); } else if (settings->isViewFixedRows()) { setViewRows(settings->getViewRows()); } else { setViewColumns(settings->getViewColumns()); } setViewLayoutVert(settings->getViewLayoutVert()); setViewLayoutR2L(settings->getViewLayoutR2L()); setViewLayoutB2T(settings->getViewLayoutB2T()); setViewPairedPages(settings->isShowPairedPages()); penSizeChanged(); eraserSizeChanged(); highlighterSizeChanged(); updateDeletePageButton(); toolFillChanged(); toolLineStyleChanged(); this->clipboardHandler = new ClipboardHandler(this, win->getXournal()->getWidget()); this->enableAutosave(settings->isAutosaveEnabled()); win->setFontButtonFont(settings->getFont()); this->pluginController->registerMenu(); win->rebindMenubarAccelerators(); fireActionSelected(GROUP_SNAPPING, settings->isSnapRotation() ? ACTION_ROTATION_SNAPPING : ACTION_NONE); fireActionSelected(GROUP_GRID_SNAPPING, settings->isSnapGrid() ? ACTION_GRID_SNAPPING : ACTION_NONE); } auto Control::autosaveCallback(Control* control) -> bool { if (!control->undoRedo->isChangedAutosave()) { // do nothing, nothing changed return true; } g_message("Info: autosave document..."); auto* job = new AutosaveJob(control); control->scheduler->addJob(job, JOB_PRIORITY_NONE); job->unref(); return true; } void Control::enableAutosave(bool enable) { if (this->autosaveTimeout) { g_source_remove(this->autosaveTimeout); this->autosaveTimeout = 0; } if (enable) { int timeout = settings->getAutosaveTimeout() * 60; this->autosaveTimeout = g_timeout_add_seconds(timeout, reinterpret_cast(autosaveCallback), this); } } void Control::updatePageNumbers(size_t page, size_t pdfPage) { if (this->win == nullptr) { return; } this->win->updatePageNumbers(page, this->doc->getPageCount(), pdfPage); this->sidebar->selectPageNr(page, pdfPage); this->metadata->storeMetadata(this->doc->getEvMetadataFilename(), page, getZoomControl()->getZoomReal()); int current = getCurrentPageNo(); int count = this->doc->getPageCount(); fireEnableAction(ACTION_GOTO_FIRST, current != 0); fireEnableAction(ACTION_GOTO_BACK, current != 0); fireEnableAction(ACTION_GOTO_PREVIOUS_ANNOTATED_PAGE, current != 0); fireEnableAction(ACTION_GOTO_PAGE, count > 1); fireEnableAction(ACTION_GOTO_NEXT, current < count - 1); fireEnableAction(ACTION_GOTO_LAST, current < count - 1); fireEnableAction(ACTION_GOTO_NEXT_ANNOTATED_PAGE, current < count - 1); } void Control::actionPerformed(ActionType type, ActionGroup group, GdkEvent* event, GtkMenuItem* menuitem, GtkToolButton* toolbutton, bool enabled) { if (layerController->actionPerformed(type)) { return; } switch (type) { // Menu File case ACTION_NEW: clearSelectionEndText(); newFile(); break; case ACTION_OPEN: openFile(); break; case ACTION_ANNOTATE_PDF: clearSelectionEndText(); annotatePdf("", false, false); break; case ACTION_SAVE: save(); break; case ACTION_SAVE_AS: saveAs(); break; case ACTION_EXPORT_AS_PDF: exportAsPdf(); break; case ACTION_EXPORT_AS: exportAs(); break; case ACTION_PRINT: print(); break; case ACTION_QUIT: quit(); break; // Menu Edit case ACTION_UNDO: UndoRedoController::undo(this); break; case ACTION_REDO: UndoRedoController::redo(this); break; case ACTION_CUT: cut(); break; case ACTION_COPY: copy(); break; case ACTION_PASTE: paste(); break; case ACTION_SEARCH: clearSelectionEndText(); searchBar->showSearchBar(true); break; case ACTION_DELETE: if (!win->getXournal()->actionDelete()) { deleteSelection(); } break; case ACTION_SETTINGS: showSettings(); break; case ACTION_ARRANGE_BRING_TO_FRONT: case ACTION_ARRANGE_BRING_FORWARD: case ACTION_ARRANGE_SEND_BACKWARD: case ACTION_ARRANGE_SEND_TO_BACK: this->reorderSelection(type); break; // Menu Navigation case ACTION_GOTO_FIRST: scrollHandler->scrollToPage(0); break; case ACTION_GOTO_BACK: scrollHandler->goToPreviousPage(); break; case ACTION_GOTO_PAGE: gotoPage(); break; case ACTION_GOTO_NEXT: scrollHandler->goToNextPage(); break; case ACTION_GOTO_LAST: scrollHandler->scrollToPage(this->doc->getPageCount() - 1); break; case ACTION_GOTO_NEXT_ANNOTATED_PAGE: scrollHandler->scrollToAnnotatedPage(true); break; case ACTION_GOTO_PREVIOUS_ANNOTATED_PAGE: scrollHandler->scrollToAnnotatedPage(false); break; // Menu Journal case ACTION_NEW_PAGE_BEFORE: insertNewPage(getCurrentPageNo()); break; case ACTION_NEW_PAGE_AFTER: insertNewPage(getCurrentPageNo() + 1); break; case ACTION_APPEND_NEW_PDF_PAGES: appendNewPdfPages(); break; case ACTION_NEW_PAGE_AT_END: insertNewPage(this->doc->getPageCount()); break; case ACTION_DELETE_PAGE: deletePage(); break; case ACTION_PAPER_FORMAT: paperFormat(); break; case ACTION_CONFIGURE_PAGE_TEMPLATE: paperTemplate(); break; case ACTION_PAPER_BACKGROUND_COLOR: changePageBackgroundColor(); break; // Menu Tools case ACTION_TOOL_PEN: clearSelection(); if (enabled) { selectTool(TOOL_PEN); } break; case ACTION_TOOL_ERASER: clearSelection(); if (enabled) { selectTool(TOOL_ERASER); } break; case ACTION_TOOL_ERASER_STANDARD: if (enabled) { toolHandler->setEraserType(ERASER_TYPE_DEFAULT); } break; case ACTION_TOOL_ERASER_DELETE_STROKE: if (enabled) { toolHandler->setEraserType(ERASER_TYPE_DELETE_STROKE); } break; case ACTION_TOOL_ERASER_WHITEOUT: if (enabled) { toolHandler->setEraserType(ERASER_TYPE_WHITEOUT); } break; case ACTION_TOOL_HIGHLIGHTER: clearSelection(); if (enabled) { selectTool(TOOL_HIGHLIGHTER); } break; case ACTION_TOOL_TEXT: clearSelection(); if (enabled) { selectTool(TOOL_TEXT); } break; case ACTION_TOOL_IMAGE: clearSelection(); if (enabled) { selectTool(TOOL_IMAGE); } break; case ACTION_TOOL_SELECT_RECT: if (enabled) { selectTool(TOOL_SELECT_RECT); } break; case ACTION_TOOL_SELECT_REGION: if (enabled) { selectTool(TOOL_SELECT_REGION); } break; case ACTION_TOOL_SELECT_OBJECT: if (enabled) { selectTool(TOOL_SELECT_OBJECT); } break; case ACTION_TOOL_PLAY_OBJECT: if (enabled) { selectTool(TOOL_PLAY_OBJECT); } break; case ACTION_TOOL_VERTICAL_SPACE: clearSelection(); if (enabled) { selectTool(TOOL_VERTICAL_SPACE); } break; case ACTION_TOOL_HAND: if (enabled) { selectTool(TOOL_HAND); } break; case ACTION_TOOL_FLOATING_TOOLBOX: if (enabled) { selectTool(TOOL_FLOATING_TOOLBOX); } break; case ACTION_TOOL_DRAW_RECT: case ACTION_TOOL_DRAW_ELLIPSE: case ACTION_TOOL_DRAW_ARROW: case ACTION_TOOL_DRAW_COORDINATE_SYSTEM: case ACTION_RULER: case ACTION_TOOL_DRAW_SPLINE: case ACTION_SHAPE_RECOGNIZER: setShapeTool(type, enabled); break; case ACTION_TOOL_DEFAULT: if (enabled) { selectDefaultTool(); } break; case ACTION_TOOL_FILL: setFill(enabled); break; case ACTION_SIZE_VERY_FINE: if (enabled) { setToolSize(TOOL_SIZE_VERY_FINE); } break; case ACTION_SIZE_FINE: if (enabled) { setToolSize(TOOL_SIZE_FINE); } break; case ACTION_SIZE_MEDIUM: if (enabled) { setToolSize(TOOL_SIZE_MEDIUM); } break; case ACTION_SIZE_THICK: if (enabled) { setToolSize(TOOL_SIZE_THICK); } break; case ACTION_SIZE_VERY_THICK: if (enabled) { setToolSize(TOOL_SIZE_VERY_THICK); } break; case ACTION_TOOL_LINE_STYLE_PLAIN: setLineStyle("plain"); break; case ACTION_TOOL_LINE_STYLE_DASH: setLineStyle("dash"); break; case ACTION_TOOL_LINE_STYLE_DASH_DOT: setLineStyle("dashdot"); break; case ACTION_TOOL_LINE_STYLE_DOT: setLineStyle("dot"); break; case ACTION_TOOL_ERASER_SIZE_VERY_FINE: if (enabled) { this->toolHandler->setEraserSize(TOOL_SIZE_VERY_FINE); eraserSizeChanged(); } break; case ACTION_TOOL_ERASER_SIZE_FINE: if (enabled) { this->toolHandler->setEraserSize(TOOL_SIZE_FINE); eraserSizeChanged(); } break; case ACTION_TOOL_ERASER_SIZE_MEDIUM: if (enabled) { this->toolHandler->setEraserSize(TOOL_SIZE_MEDIUM); eraserSizeChanged(); } break; case ACTION_TOOL_ERASER_SIZE_THICK: if (enabled) { this->toolHandler->setEraserSize(TOOL_SIZE_THICK); eraserSizeChanged(); } break; case ACTION_TOOL_ERASER_SIZE_VERY_THICK: if (enabled) { this->toolHandler->setEraserSize(TOOL_SIZE_VERY_THICK); eraserSizeChanged(); } break; case ACTION_TOOL_PEN_SIZE_VERY_FINE: if (enabled) { this->toolHandler->setPenSize(TOOL_SIZE_VERY_FINE); penSizeChanged(); } break; case ACTION_TOOL_PEN_SIZE_FINE: if (enabled) { this->toolHandler->setPenSize(TOOL_SIZE_FINE); penSizeChanged(); } break; case ACTION_TOOL_PEN_SIZE_MEDIUM: if (enabled) { this->toolHandler->setPenSize(TOOL_SIZE_MEDIUM); penSizeChanged(); } break; case ACTION_TOOL_PEN_SIZE_THICK: if (enabled) { this->toolHandler->setPenSize(TOOL_SIZE_THICK); penSizeChanged(); } break; case ACTION_TOOL_PEN_SIZE_VERY_THICK: if (enabled) { this->toolHandler->setPenSize(TOOL_SIZE_VERY_THICK); penSizeChanged(); } break; case ACTION_TOOL_PEN_FILL: this->toolHandler->setPenFillEnabled(enabled); break; case ACTION_TOOL_PEN_FILL_OPACITY: selectFillAlpha(true); break; case ACTION_TOOL_HIGHLIGHTER_SIZE_VERY_FINE: if (enabled) { this->toolHandler->setHighlighterSize(TOOL_SIZE_VERY_FINE); highlighterSizeChanged(); } break; case ACTION_TOOL_HIGHLIGHTER_SIZE_FINE: if (enabled) { this->toolHandler->setHighlighterSize(TOOL_SIZE_FINE); highlighterSizeChanged(); } break; case ACTION_TOOL_HIGHLIGHTER_SIZE_MEDIUM: if (enabled) { this->toolHandler->setHighlighterSize(TOOL_SIZE_MEDIUM); highlighterSizeChanged(); } break; case ACTION_TOOL_HIGHLIGHTER_SIZE_THICK: if (enabled) { this->toolHandler->setHighlighterSize(TOOL_SIZE_THICK); highlighterSizeChanged(); } break; case ACTION_TOOL_HIGHLIGHTER_SIZE_VERY_THICK: if (enabled) { this->toolHandler->setHighlighterSize(TOOL_SIZE_VERY_THICK); highlighterSizeChanged(); } break; case ACTION_TOOL_HIGHLIGHTER_FILL: this->toolHandler->setHighlighterFillEnabled(enabled); break; case ACTION_TOOL_HIGHLIGHTER_FILL_OPACITY: selectFillAlpha(false); break; case ACTION_FONT_BUTTON_CHANGED: fontChanged(); break; case ACTION_SELECT_FONT: if (win) { win->getToolMenuHandler()->showFontSelectionDlg(); } break; // Used for all colors case ACTION_SELECT_COLOR: case ACTION_SELECT_COLOR_CUSTOM: // nothing to do here, the color toolbar item handles the color break; case ACTION_TEX: runLatex(); break; // Menu View case ACTION_ZOOM_100: case ACTION_ZOOM_FIT: case ACTION_ZOOM_IN: case ACTION_ZOOM_OUT: Util::execInUiThread([=]() { zoomCallback(type, enabled); }); break; case ACTION_VIEW_PAIRED_PAGES: setViewPairedPages(enabled); break; case ACTION_VIEW_PRESENTATION_MODE: setViewPresentationMode(enabled); break; case ACTION_MANAGE_TOOLBAR: manageToolbars(); break; case ACTION_CUSTOMIZE_TOOLBAR: customizeToolbars(); break; case ACTION_FULLSCREEN: setFullscreen(enabled); break; case ACTION_SET_COLUMNS_1: setViewColumns(1); break; case ACTION_SET_COLUMNS_2: setViewColumns(2); break; case ACTION_SET_COLUMNS_3: setViewColumns(3); break; case ACTION_SET_COLUMNS_4: setViewColumns(4); break; case ACTION_SET_COLUMNS_5: setViewColumns(5); break; case ACTION_SET_COLUMNS_6: setViewColumns(6); break; case ACTION_SET_COLUMNS_7: setViewColumns(7); break; case ACTION_SET_COLUMNS_8: setViewColumns(8); break; case ACTION_SET_ROWS_1: setViewRows(1); break; case ACTION_SET_ROWS_2: setViewRows(2); break; case ACTION_SET_ROWS_3: setViewRows(3); break; case ACTION_SET_ROWS_4: setViewRows(4); break; case ACTION_SET_ROWS_5: setViewRows(5); break; case ACTION_SET_ROWS_6: setViewRows(6); break; case ACTION_SET_ROWS_7: setViewRows(7); break; case ACTION_SET_ROWS_8: setViewRows(8); break; case ACTION_SET_LAYOUT_HORIZONTAL: setViewLayoutVert(false); break; case ACTION_SET_LAYOUT_VERTICAL: setViewLayoutVert(true); break; case ACTION_SET_LAYOUT_L2R: setViewLayoutR2L(false); break; case ACTION_SET_LAYOUT_R2L: setViewLayoutR2L(true); break; case ACTION_SET_LAYOUT_T2B: setViewLayoutB2T(false); break; case ACTION_SET_LAYOUT_B2T: setViewLayoutB2T(true); break; case ACTION_AUDIO_RECORD: { bool result = false; if (enabled) { result = audioController->startRecording(); } else { result = audioController->stopRecording(); } if (!result) { Util::execInUiThread([=]() { gtk_toggle_tool_button_set_active(reinterpret_cast(toolbutton), !enabled); string msg = _("Recorder could not be started."); g_warning("%s", msg.c_str()); XojMsgBox::showErrorToUser(Control::getGtkWindow(), msg); }); } break; } case ACTION_AUDIO_PAUSE_PLAYBACK: if (enabled) { this->getAudioController()->pausePlayback(); } else { this->getAudioController()->continuePlayback(); } break; case ACTION_AUDIO_SEEK_FORWARDS: this->getAudioController()->seekForwards(); break; case ACTION_AUDIO_SEEK_BACKWARDS: this->getAudioController()->seekBackwards(); break; case ACTION_AUDIO_STOP_PLAYBACK: this->getAudioController()->stopPlayback(); break; case ACTION_ROTATION_SNAPPING: rotationSnappingToggle(); break; case ACTION_GRID_SNAPPING: gridSnappingToggle(); break; // Footer, not really an action, but need an identifier to case ACTION_FOOTER_PAGESPIN: case ACTION_FOOTER_ZOOM_SLIDER: // nothing to do here break; // Plugin menu case ACTION_PLUGIN_MANAGER: this->pluginController->showPluginManager(); break; // Menu Help case ACTION_HELP: XojMsgBox::showHelp(getGtkWindow()); break; case ACTION_ABOUT: showAbout(); break; case ACTION_NONE: // do nothing break; default: g_warning("Unhandled action event: %s / %s (%i / %i)", ActionType_toString(type).c_str(), ActionGroup_toString(group).c_str(), type, group); Stacktrace::printStracktrace(); } if (type >= ACTION_TOOL_PEN && type <= ACTION_TOOL_HAND) { auto at = static_cast(toolHandler->getToolType() - TOOL_PEN + ACTION_TOOL_PEN); if (type == at && !enabled) { fireActionSelected(GROUP_TOOL, at); } } } auto Control::copy() -> bool { if (this->win && this->win->getXournal()->copy()) { return true; } return this->clipboardHandler->copy(); } auto Control::cut() -> bool { if (this->win && this->win->getXournal()->cut()) { return true; } return this->clipboardHandler->cut(); } auto Control::paste() -> bool { if (this->win && this->win->getXournal()->paste()) { return true; } return this->clipboardHandler->paste(); } void Control::selectFillAlpha(bool pen) { int alpha = 0; if (pen) { alpha = toolHandler->getPenFill(); } else { alpha = toolHandler->getHighlighterFill(); } FillOpacityDialog dlg(gladeSearchPath, alpha); dlg.show(getGtkWindow()); if (dlg.getResultAlpha() == -1) { return; } alpha = dlg.getResultAlpha(); if (pen) { toolHandler->setPenFill(alpha); } else { toolHandler->setHighlighterFill(alpha); } } void Control::clearSelectionEndText() { clearSelection(); if (win) { win->getXournal()->endTextAllPages(); } } void Control::reorderSelection(const ActionType type) { EditSelection* sel = win->getXournal()->getSelection(); if (!sel) return; EditSelection::OrderChange change; switch (type) { case ACTION_ARRANGE_BRING_TO_FRONT: change = EditSelection::OrderChange::BringToFront; break; case ACTION_ARRANGE_BRING_FORWARD: change = EditSelection::OrderChange::BringForward; break; case ACTION_ARRANGE_SEND_BACKWARD: change = EditSelection::OrderChange::SendBackward; break; case ACTION_ARRANGE_SEND_TO_BACK: change = EditSelection::OrderChange::SendToBack; break; default: // Unknown selection order, do nothing. return; } auto undoAction = sel->rearrangeInsertOrder(change); this->undoRedo->addUndoAction(std::move(undoAction)); } /** * Fire page selected, but first check if the page Number is valid * * @return the page ID or size_t_npos if the page is not found */ auto Control::firePageSelected(const PageRef& page) -> size_t { this->doc->lock(); size_t pageId = this->doc->indexOf(page); this->doc->unlock(); if (pageId == npos) { return npos; } DocumentHandler::firePageSelected(pageId); return pageId; } void Control::firePageSelected(size_t page) { DocumentHandler::firePageSelected(page); } void Control::manageToolbars() { ToolbarManageDialog dlg(this->gladeSearchPath, this->win->getToolbarModel()); dlg.show(GTK_WINDOW(this->win->getWindow())); this->win->updateToolbarMenu(); auto filepath = Util::getConfigFile(TOOLBAR_CONFIG); this->win->getToolbarModel()->save(filepath); } void Control::customizeToolbars() { g_return_if_fail(this->win != nullptr); if (this->win->getSelectedToolbar()->isPredefined()) { GtkWidget* dialog = gtk_message_dialog_new(getGtkWindow(), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "%s", FC(_F("The Toolbarconfiguration \"{1}\" is predefined, " "would you create a copy to edit?") % this->win->getSelectedToolbar()->getName())); gtk_window_set_transient_for(GTK_WINDOW(dialog), getGtkWindow()); int res = gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); if (res == -8) // Yes { auto* data = new ToolbarData(*this->win->getSelectedToolbar()); ToolbarModel* model = this->win->getToolbarModel(); model->initCopyNameId(data); model->add(data); this->win->toolbarSelected(data); this->win->updateToolbarMenu(); } else { return; } } if (!this->dragDropHandler) { this->dragDropHandler = new ToolbarDragDropHandler(this); } this->dragDropHandler->configure(); } void Control::endDragDropToolbar() { if (!this->dragDropHandler) { return; } this->dragDropHandler->clearToolbarsFromDragAndDrop(); } void Control::startDragDropToolbar() { if (!this->dragDropHandler) { return; } this->dragDropHandler->prepareToolbarsForDragAndDrop(); } auto Control::isInDragAndDropToolbar() -> bool { if (!this->dragDropHandler) { return false; } return this->dragDropHandler->isInDragAndDrop(); } void Control::setShapeTool(ActionType type, bool enabled) { if (!enabled) { // Disable all entries this->toolHandler->setDrawingType(DRAWING_TYPE_DEFAULT); // fire disabled and return fireActionSelected(GROUP_RULER, ACTION_NONE); return; } // Check for nothing changed, and return in this case if ((this->toolHandler->getDrawingType() == DRAWING_TYPE_LINE && type == ACTION_RULER) || (this->toolHandler->getDrawingType() == DRAWING_TYPE_RECTANGLE && type == ACTION_TOOL_DRAW_RECT) || (this->toolHandler->getDrawingType() == DRAWING_TYPE_ARROW && type == ACTION_TOOL_DRAW_ARROW) || (this->toolHandler->getDrawingType() == DRAWING_TYPE_COORDINATE_SYSTEM && type == ACTION_TOOL_DRAW_COORDINATE_SYSTEM) || (this->toolHandler->getDrawingType() == DRAWING_TYPE_ELLIPSE && type == ACTION_TOOL_DRAW_ELLIPSE) || (this->toolHandler->getDrawingType() == DRAWING_TYPE_SPLINE && type == ACTION_TOOL_DRAW_SPLINE) || (this->toolHandler->getDrawingType() == DRAWING_TYPE_STROKE_RECOGNIZER && type == ACTION_SHAPE_RECOGNIZER)) { return; } switch (type) { case ACTION_TOOL_DRAW_RECT: this->toolHandler->setDrawingType(DRAWING_TYPE_RECTANGLE); break; case ACTION_TOOL_DRAW_ELLIPSE: this->toolHandler->setDrawingType(DRAWING_TYPE_ELLIPSE); break; case ACTION_TOOL_DRAW_ARROW: this->toolHandler->setDrawingType(DRAWING_TYPE_ARROW); break; case ACTION_TOOL_DRAW_COORDINATE_SYSTEM: this->toolHandler->setDrawingType(DRAWING_TYPE_COORDINATE_SYSTEM); break; case ACTION_RULER: this->toolHandler->setDrawingType(DRAWING_TYPE_LINE); break; case ACTION_TOOL_DRAW_SPLINE: this->toolHandler->setDrawingType(DRAWING_TYPE_SPLINE); break; case ACTION_SHAPE_RECOGNIZER: this->toolHandler->setDrawingType(DRAWING_TYPE_STROKE_RECOGNIZER); this->resetShapeRecognizer(); break; default: g_warning("Invalid type for setShapeTool: %i", type); break; } fireActionSelected(GROUP_RULER, type); } void Control::setFullscreen(bool enabled) { fullscreenHandler->setFullscreen(win, enabled); fireActionSelected(GROUP_FULLSCREEN, enabled ? ACTION_FULLSCREEN : ACTION_NONE); } void Control::disableSidebarTmp(bool disabled) { this->sidebar->setTmpDisabled(disabled); } void Control::addDefaultPage(string pageTemplate) { if (pageTemplate.empty()) { pageTemplate = settings->getPageTemplate(); } PageTemplateSettings model; model.parse(pageTemplate); auto page = std::make_shared(model.getPageWidth(), model.getPageHeight()); page->setBackgroundColor(model.getBackgroundColor()); page->setBackgroundType(model.getBackgroundType()); this->doc->lock(); this->doc->addPage(std::move(page)); this->doc->unlock(); updateDeletePageButton(); } void Control::updateDeletePageButton() { if (this->win) { GtkWidget* w = this->win->get("menuDeletePage"); gtk_widget_set_sensitive(w, this->doc->getPageCount() > 1); } } void Control::deletePage() { clearSelectionEndText(); // don't allow delete pages if we have less than 2 pages, // so we can be (more or less) sure there is at least one page. if (this->doc->getPageCount() < 2) { return; } size_t pNr = getCurrentPageNo(); if (pNr == npos || pNr > this->doc->getPageCount()) { // something went wrong... return; } this->doc->lock(); PageRef page = doc->getPage(pNr); this->doc->unlock(); // first send event, then delete page... firePageDeleted(pNr); this->doc->lock(); doc->deletePage(pNr); this->doc->unlock(); updateDeletePageButton(); this->undoRedo->addUndoAction(std::make_unique(page, pNr, false)); if (pNr >= this->doc->getPageCount()) { pNr = this->doc->getPageCount() - 1; } scrollHandler->scrollToPage(pNr, 0); this->win->getXournal()->forceUpdatePagenumbers(); } void Control::insertNewPage(size_t position) { pageBackgroundChangeController->insertNewPage(position); } void Control::appendNewPdfPages() { auto pageCount = this->doc->getPageCount(); // find last page with pdf background and get its pdf page number auto currentPdfPageCount = [&]() { for (size_t i = pageCount; i != 0; --i) { if (auto page = doc->getPage(i - 1); page && page->getBackgroundType().isPdfPage()) { return page->getPdfPageNr() + 1; } } return size_t{0U}; }(); auto pdfPageCount = this->doc->getPdfPageCount(); auto insertCount = pdfPageCount - currentPdfPageCount; if (insertCount == 0) { string msg = FS(_F("No pdf pages available to append. You may need to reopen the document first.")); XojMsgBox::showErrorToUser(getGtkWindow(), msg); } for (size_t i = 0; i != insertCount; ++i) { doc->lock(); XojPdfPageSPtr pdf = doc->getPdfPage(currentPdfPageCount + i); doc->unlock(); if (pdf) { auto newPage = std::make_shared(pdf->getWidth(), pdf->getHeight()); newPage->setBackgroundPdfPageNr(currentPdfPageCount + i); insertPage(newPage, pageCount + i); } else { string msg = FS(_F("Unable to retrieve pdf page.")); // should not happen XojMsgBox::showErrorToUser(getGtkWindow(), msg); } } } void Control::insertPage(const PageRef& page, size_t position) { this->doc->lock(); this->doc->insertPage(page, position); // insert the new page to the document and update page numbers this->doc->unlock(); // notify document listeners about the inserted page; this creates the new XojViewPage, recalculates the layout // and creates a preview page in the sidebar firePageInserted(position); getCursor()->updateCursor(); // make the inserted page fully visible (or at least as much from the top which fits on the screen), // and make the page appear selected scrollHandler->scrollToPage(position); firePageSelected(position); updateDeletePageButton(); undoRedo->addUndoAction(std::make_unique(page, position, true)); } void Control::gotoPage() { auto* dlg = new GotoDialog(this->gladeSearchPath, this->doc->getPageCount()); dlg->show(GTK_WINDOW(this->win->getWindow())); int page = dlg->getSelectedPage(); if (page != -1) { this->scrollHandler->scrollToPage(page - 1, 0); } delete dlg; } void Control::updateBackgroundSizeButton() { if (this->win == nullptr) { return; } // Update paper color button auto const& p = getCurrentPage(); if (!p || this->win == nullptr) { return; } GtkWidget* paperColor = win->get("menuJournalPaperColor"); GtkWidget* pageSize = win->get("menuJournalPaperFormat"); PageType bg = p->getBackgroundType(); gtk_widget_set_sensitive(paperColor, !bg.isSpecial()); // PDF page size is defined, you cannot change it gtk_widget_set_sensitive(pageSize, !bg.isPdfPage()); } void Control::paperTemplate() { auto dlg = std::make_unique(this->gladeSearchPath, settings, pageTypes); dlg->show(GTK_WINDOW(this->win->getWindow())); if (dlg->isSaved()) { newPageType->loadDefaultPage(); } } void Control::paperFormat() { auto const& page = getCurrentPage(); if (!page || page->getBackgroundType().isPdfPage()) { return; } clearSelectionEndText(); auto* dlg = new FormatDialog(this->gladeSearchPath, settings, page->getWidth(), page->getHeight()); dlg->show(GTK_WINDOW(this->win->getWindow())); double width = dlg->getWidth(); double height = dlg->getHeight(); if (width > 0) { this->doc->lock(); Document::setPageSize(page, width, height); this->doc->unlock(); } size_t pageNo = doc->indexOf(page); if (pageNo != npos && pageNo < doc->getPageCount()) { this->firePageSizeChanged(pageNo); } delete dlg; } void Control::changePageBackgroundColor() { int pNr = getCurrentPageNo(); this->doc->lock(); auto const& p = this->doc->getPage(pNr); this->doc->unlock(); if (!p) { return; } clearSelectionEndText(); PageType bg = p->getBackgroundType(); if (bg.isSpecial()) { return; } SelectBackgroundColorDialog dlg(this); dlg.show(GTK_WINDOW(this->win->getWindow())); if (auto optColor = dlg.getSelectedColor(); optColor) { p->setBackgroundColor(*optColor); firePageChanged(pNr); } } void Control::setViewPairedPages(bool enabled) { settings->setShowPairedPages(enabled); fireActionSelected(GROUP_PAIRED_PAGES, enabled ? ACTION_VIEW_PAIRED_PAGES : ACTION_NOT_SELECTED); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setViewPresentationMode(bool enabled) { if (enabled) { bool success = zoom->updateZoomPresentationValue(); if (!success) { g_warning("Error calculating zoom value"); fireActionSelected(GROUP_PRESENTATION_MODE, ACTION_NOT_SELECTED); return; } } else { if (settings->isViewFixedRows()) { setViewRows(settings->getViewRows()); } else { setViewColumns(settings->getViewColumns()); } setViewLayoutVert(settings->getViewLayoutVert()); setViewLayoutR2L(settings->getViewLayoutR2L()); setViewLayoutB2T(settings->getViewLayoutB2T()); } zoom->setZoomPresentationMode(enabled); settings->setPresentationMode(enabled); // Disable Zoom fireEnableAction(ACTION_ZOOM_IN, !enabled); fireEnableAction(ACTION_ZOOM_OUT, !enabled); fireEnableAction(ACTION_ZOOM_FIT, !enabled); fireEnableAction(ACTION_ZOOM_100, !enabled); fireEnableAction(ACTION_FOOTER_ZOOM_SLIDER, !enabled); gtk_widget_set_sensitive(win->get("menuitemLayout"), !enabled); gtk_widget_set_sensitive(win->get("menuitemViewDimensions"), !enabled); // disable selection of scroll hand tool fireEnableAction(ACTION_TOOL_HAND, !enabled); fireActionSelected(GROUP_PRESENTATION_MODE, enabled ? ACTION_VIEW_PRESENTATION_MODE : ACTION_NOT_SELECTED); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setPairsOffset(int numOffset) { settings->setPairsOffset(numOffset); fireActionSelected(GROUP_PAIRED_PAGES, numOffset ? ACTION_SET_PAIRS_OFFSET : ACTION_NOT_SELECTED); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setViewColumns(int numColumns) { settings->setViewColumns(numColumns); settings->setViewFixedRows(false); ActionType action{}; switch (numColumns) { case 1: action = ACTION_SET_COLUMNS_1; break; case 2: action = ACTION_SET_COLUMNS_2; break; case 3: action = ACTION_SET_COLUMNS_3; break; case 4: action = ACTION_SET_COLUMNS_4; break; case 5: action = ACTION_SET_COLUMNS_5; break; case 6: action = ACTION_SET_COLUMNS_6; break; case 7: action = ACTION_SET_COLUMNS_7; break; case 8: action = ACTION_SET_COLUMNS_8; break; default: action = ACTION_SET_COLUMNS; } fireActionSelected(GROUP_FIXED_ROW_OR_COLS, action); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setViewRows(int numRows) { settings->setViewRows(numRows); settings->setViewFixedRows(true); ActionType action{}; switch (numRows) { case 1: action = ACTION_SET_ROWS_1; break; case 2: action = ACTION_SET_ROWS_2; break; case 3: action = ACTION_SET_ROWS_3; break; case 4: action = ACTION_SET_ROWS_4; break; case 5: action = ACTION_SET_ROWS_5; break; case 6: action = ACTION_SET_ROWS_6; break; case 7: action = ACTION_SET_ROWS_7; break; case 8: action = ACTION_SET_ROWS_8; break; default: action = ACTION_SET_ROWS; } fireActionSelected(GROUP_FIXED_ROW_OR_COLS, action); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setViewLayoutVert(bool vert) { settings->setViewLayoutVert(vert); ActionType action{}; if (vert) { action = ACTION_SET_LAYOUT_VERTICAL; } else { action = ACTION_SET_LAYOUT_HORIZONTAL; } fireActionSelected(GROUP_LAYOUT_HORIZONTAL, action); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setViewLayoutR2L(bool r2l) { settings->setViewLayoutR2L(r2l); ActionType action{}; if (r2l) { action = ACTION_SET_LAYOUT_R2L; } else { action = ACTION_SET_LAYOUT_L2R; } fireActionSelected(GROUP_LAYOUT_LR, action); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } void Control::setViewLayoutB2T(bool b2t) { settings->setViewLayoutB2T(b2t); ActionType action{}; if (b2t) { action = ACTION_SET_LAYOUT_B2T; } else { action = ACTION_SET_LAYOUT_T2B; } fireActionSelected(GROUP_LAYOUT_TB, action); int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } /** * This callback is used by used to be called later in the UI Thread * On slower machine this feels more fluent, therefore this will not * be removed */ void Control::zoomCallback(ActionType type, bool enabled) { switch (type) { case ACTION_ZOOM_100: zoom->zoom100(); break; case ACTION_ZOOM_FIT: if (enabled) { zoom->updateZoomFitValue(); } // enable/disable ZoomFit zoom->setZoomFitMode(enabled); break; case ACTION_ZOOM_IN: zoom->zoomOneStep(ZOOM_IN); break; case ACTION_ZOOM_OUT: zoom->zoomOneStep(ZOOM_OUT); break; default: break; } } auto Control::getCurrentPageNo() -> size_t { if (this->win) { return this->win->getXournal()->getCurrentPage(); } return 0; } auto Control::searchTextOnPage(string text, int p, int* occures, double* top) -> bool { return getWindow()->getXournal()->searchTextOnPage(std::move(text), p, occures, top); } auto Control::getCurrentPage() -> PageRef { this->doc->lock(); PageRef p = this->doc->getPage(getCurrentPageNo()); this->doc->unlock(); return p; } void Control::fileOpened(fs::path const& path) { openFile(path); } void Control::undoRedoChanged() { fireEnableAction(ACTION_UNDO, undoRedo->canUndo()); fireEnableAction(ACTION_REDO, undoRedo->canRedo()); win->setUndoDescription(undoRedo->undoDescription()); win->setRedoDescription(undoRedo->redoDescription()); updateWindowTitle(); } void Control::undoRedoPageChanged(PageRef page) { if (std::find(begin(this->changedPages), end(this->changedPages), page) == end(this->changedPages)) { this->changedPages.emplace_back(std::move(page)); } } void Control::selectTool(ToolType type) { toolHandler->selectTool(type); toolHandler->fireToolChanged(); if (win) { (win->getXournal()->getViewFor(getCurrentPageNo()))->rerenderPage(); } } void Control::selectDefaultTool() { ButtonConfig* cfg = settings->getButtonConfig(Button::BUTTON_DEFAULT); cfg->applyConfigToToolbarTool(toolHandler); } void Control::toolChanged() { ToolType type = toolHandler->getToolType(); // Convert enum values, enums has to be in the same order! auto at = static_cast(type - TOOL_PEN + ACTION_TOOL_PEN); fireActionSelected(GROUP_TOOL, at); fireEnableAction(ACTION_SELECT_COLOR, toolHandler->hasCapability(TOOL_CAP_COLOR)); fireEnableAction(ACTION_SELECT_COLOR_CUSTOM, toolHandler->hasCapability(TOOL_CAP_COLOR)); fireEnableAction(ACTION_RULER, toolHandler->hasCapability(TOOL_CAP_RULER)); fireEnableAction(ACTION_TOOL_DRAW_RECT, toolHandler->hasCapability(TOOL_CAP_RECTANGLE)); fireEnableAction(ACTION_TOOL_DRAW_ELLIPSE, toolHandler->hasCapability(TOOL_CAP_ELLIPSE)); fireEnableAction(ACTION_TOOL_DRAW_ARROW, toolHandler->hasCapability(TOOL_CAP_ARROW)); fireEnableAction(ACTION_TOOL_DRAW_COORDINATE_SYSTEM, toolHandler->hasCapability(TOOL_CAP_ARROW)); fireEnableAction(ACTION_TOOL_DRAW_SPLINE, toolHandler->hasCapability(TOOL_CAP_SPLINE)); fireEnableAction(ACTION_SHAPE_RECOGNIZER, toolHandler->hasCapability(TOOL_CAP_RECOGNIZER)); bool enableSize = toolHandler->hasCapability(TOOL_CAP_SIZE); fireEnableAction(ACTION_SIZE_MEDIUM, enableSize); fireEnableAction(ACTION_SIZE_THICK, enableSize); fireEnableAction(ACTION_SIZE_FINE, enableSize); fireEnableAction(ACTION_SIZE_VERY_THICK, enableSize); fireEnableAction(ACTION_SIZE_VERY_FINE, enableSize); if (enableSize) { toolSizeChanged(); } bool enableFill = toolHandler->hasCapability(TOOL_CAP_FILL); fireEnableAction(ACTION_TOOL_FILL, enableFill); if (enableFill) { toolFillChanged(); } // Update color if (toolHandler->hasCapability(TOOL_CAP_COLOR)) { toolColorChanged(); } ActionType rulerAction = ACTION_NOT_SELECTED; if (toolHandler->getDrawingType() == DRAWING_TYPE_STROKE_RECOGNIZER) { rulerAction = ACTION_SHAPE_RECOGNIZER; } else if (toolHandler->getDrawingType() == DRAWING_TYPE_LINE) { rulerAction = ACTION_RULER; } else if (toolHandler->getDrawingType() == DRAWING_TYPE_RECTANGLE) { rulerAction = ACTION_TOOL_DRAW_RECT; } else if (toolHandler->getDrawingType() == DRAWING_TYPE_ELLIPSE) { rulerAction = ACTION_TOOL_DRAW_ELLIPSE; } else if (toolHandler->getDrawingType() == DRAWING_TYPE_ARROW) { rulerAction = ACTION_TOOL_DRAW_ARROW; } else if (toolHandler->getDrawingType() == DRAWING_TYPE_COORDINATE_SYSTEM) { rulerAction = ACTION_TOOL_DRAW_COORDINATE_SYSTEM; } else if (toolHandler->getDrawingType() == DRAWING_TYPE_SPLINE) { rulerAction = ACTION_TOOL_DRAW_SPLINE; } fireActionSelected(GROUP_RULER, rulerAction); getCursor()->updateCursor(); if (type != TOOL_TEXT) { if (win) { win->getXournal()->endTextAllPages(); } } } void Control::eraserSizeChanged() { switch (toolHandler->getEraserSize()) { case TOOL_SIZE_VERY_FINE: fireActionSelected(GROUP_ERASER_SIZE, ACTION_TOOL_ERASER_SIZE_VERY_FINE); break; case TOOL_SIZE_FINE: fireActionSelected(GROUP_ERASER_SIZE, ACTION_TOOL_ERASER_SIZE_FINE); break; case TOOL_SIZE_MEDIUM: fireActionSelected(GROUP_ERASER_SIZE, ACTION_TOOL_ERASER_SIZE_MEDIUM); break; case TOOL_SIZE_THICK: fireActionSelected(GROUP_ERASER_SIZE, ACTION_TOOL_ERASER_SIZE_THICK); break; case TOOL_SIZE_VERY_THICK: fireActionSelected(GROUP_ERASER_SIZE, ACTION_TOOL_ERASER_SIZE_VERY_THICK); break; default: break; } } void Control::penSizeChanged() { switch (toolHandler->getPenSize()) { case TOOL_SIZE_VERY_FINE: fireActionSelected(GROUP_PEN_SIZE, ACTION_TOOL_PEN_SIZE_VERY_FINE); break; case TOOL_SIZE_FINE: fireActionSelected(GROUP_PEN_SIZE, ACTION_TOOL_PEN_SIZE_FINE); break; case TOOL_SIZE_MEDIUM: fireActionSelected(GROUP_PEN_SIZE, ACTION_TOOL_PEN_SIZE_MEDIUM); break; case TOOL_SIZE_THICK: fireActionSelected(GROUP_PEN_SIZE, ACTION_TOOL_PEN_SIZE_THICK); break; case TOOL_SIZE_VERY_THICK: fireActionSelected(GROUP_PEN_SIZE, ACTION_TOOL_PEN_SIZE_VERY_THICK); break; default: break; } } void Control::highlighterSizeChanged() { switch (toolHandler->getHighlighterSize()) { case TOOL_SIZE_VERY_FINE: fireActionSelected(GROUP_HIGHLIGHTER_SIZE, ACTION_TOOL_HIGHLIGHTER_SIZE_VERY_FINE); break; case TOOL_SIZE_FINE: fireActionSelected(GROUP_HIGHLIGHTER_SIZE, ACTION_TOOL_HIGHLIGHTER_SIZE_FINE); break; case TOOL_SIZE_MEDIUM: fireActionSelected(GROUP_HIGHLIGHTER_SIZE, ACTION_TOOL_HIGHLIGHTER_SIZE_MEDIUM); break; case TOOL_SIZE_THICK: fireActionSelected(GROUP_HIGHLIGHTER_SIZE, ACTION_TOOL_HIGHLIGHTER_SIZE_THICK); break; case TOOL_SIZE_VERY_THICK: fireActionSelected(GROUP_HIGHLIGHTER_SIZE, ACTION_TOOL_HIGHLIGHTER_SIZE_VERY_THICK); break; default: break; } } void Control::toolSizeChanged() { if (toolHandler->getToolType() == TOOL_PEN) { penSizeChanged(); } else if (toolHandler->getToolType() == TOOL_ERASER) { eraserSizeChanged(); } else if (toolHandler->getToolType() == TOOL_HIGHLIGHTER) { highlighterSizeChanged(); } switch (toolHandler->getSize()) { case TOOL_SIZE_NONE: fireActionSelected(GROUP_SIZE, ACTION_NONE); break; case TOOL_SIZE_VERY_FINE: fireActionSelected(GROUP_SIZE, ACTION_SIZE_VERY_FINE); break; case TOOL_SIZE_FINE: fireActionSelected(GROUP_SIZE, ACTION_SIZE_FINE); break; case TOOL_SIZE_MEDIUM: fireActionSelected(GROUP_SIZE, ACTION_SIZE_MEDIUM); break; case TOOL_SIZE_THICK: fireActionSelected(GROUP_SIZE, ACTION_SIZE_THICK); break; case TOOL_SIZE_VERY_THICK: fireActionSelected(GROUP_SIZE, ACTION_SIZE_VERY_THICK); break; } getCursor()->updateCursor(); } void Control::toolFillChanged() { fireActionSelected(GROUP_FILL, toolHandler->getFill() != -1 ? ACTION_TOOL_FILL : ACTION_NONE); fireActionSelected(GROUP_PEN_FILL, toolHandler->getPenFillEnabled() ? ACTION_TOOL_PEN_FILL : ACTION_NONE); fireActionSelected(GROUP_HIGHLIGHTER_FILL, toolHandler->getHighlighterFillEnabled() ? ACTION_TOOL_HIGHLIGHTER_FILL : ACTION_NONE); } void Control::toolLineStyleChanged() { const LineStyle& lineStyle = toolHandler->getTool(TOOL_PEN).getLineStyle(); string style = StrokeStyle::formatStyle(lineStyle); if (style == "dash") { fireActionSelected(GROUP_LINE_STYLE, ACTION_TOOL_LINE_STYLE_DASH); } else if (style == "dashdot") { fireActionSelected(GROUP_LINE_STYLE, ACTION_TOOL_LINE_STYLE_DASH_DOT); } else if (style == "dot") { fireActionSelected(GROUP_LINE_STYLE, ACTION_TOOL_LINE_STYLE_DOT); } else { fireActionSelected(GROUP_LINE_STYLE, ACTION_TOOL_LINE_STYLE_PLAIN); } } void Control::toolColorChanged() { fireActionSelected(GROUP_COLOR, ACTION_SELECT_COLOR); getCursor()->updateCursor(); } void Control::changeColorOfSelection() { if (this->win && toolHandler->hasCapability(TOOL_CAP_COLOR)) { EditSelection* sel = this->win->getXournal()->getSelection(); if (sel) { UndoAction* undo = sel->setColor(toolHandler->getColor()); // move into selection undoRedo->addUndoAction(UndoActionPtr(undo)); } TextEditor* edit = getTextEditor(); if (this->toolHandler->getToolType() == TOOL_TEXT && edit != nullptr) { // Todo move into selection undoRedo->addUndoAction(UndoActionPtr(edit->setColor(toolHandler->getColor()))); } } } void Control::setCustomColorSelected() { fireActionSelected(GROUP_COLOR, ACTION_SELECT_COLOR_CUSTOM); } void Control::showSettings() { // take note of some settings before to compare with after auto selectionColor = settings->getBorderColor(); bool verticalSpace = settings->getAddVerticalSpace(); int verticalSpaceAmount = settings->getAddVerticalSpaceAmount(); bool horizontalSpace = settings->getAddHorizontalSpace(); int horizontalSpaceAmount = settings->getAddHorizontalSpaceAmount(); StylusCursorType stylusCursorType = settings->getStylusCursorType(); bool highlightPosition = settings->isHighlightPosition(); auto* dlg = new SettingsDialog(this->gladeSearchPath, settings, this); dlg->show(GTK_WINDOW(this->win->getWindow())); // note which settings have changed and act accordingly if (selectionColor != settings->getBorderColor()) { win->getXournal()->forceUpdatePagenumbers(); } if (verticalSpace != settings->getAddVerticalSpace() || horizontalSpace != settings->getAddHorizontalSpace() || verticalSpaceAmount != settings->getAddVerticalSpaceAmount() || horizontalSpaceAmount != settings->getAddHorizontalSpaceAmount()) { int currentPage = getCurrentPageNo(); win->getXournal()->layoutPages(); scrollHandler->scrollToPage(currentPage); } if (stylusCursorType != settings->getStylusCursorType() || highlightPosition != settings->isHighlightPosition()) { getCursor()->updateCursor(); } win->updateScrollbarSidebarPosition(); enableAutosave(settings->isAutosaveEnabled()); this->zoom->setZoomStep(settings->getZoomStep() / 100.0); this->zoom->setZoomStepScroll(settings->getZoomStepScroll() / 100.0); this->zoom->setZoom100Value(settings->getDisplayDpi() / Util::DPI_NORMALIZATION_FACTOR); getWindow()->getXournal()->getHandRecognition()->reload(); getWindow()->updateColorscheme(); TextView::setDpi(settings->getDisplayDpi()); delete dlg; } auto Control::newFile(string pageTemplate, fs::path filepath) -> bool { if (!this->close(true)) { return false; } Document newDoc(this); this->doc->lock(); *doc = newDoc; if (!filepath.empty()) { this->doc->setFilepath(std::move(filepath)); } this->doc->unlock(); addDefaultPage(std::move(pageTemplate)); fireDocumentChanged(DOCUMENT_CHANGE_COMPLETE); fileLoaded(); return true; } /** * Check if this is an autosave file, return false in this case and display a user instruction */ auto Control::shouldFileOpen(fs::path const& filepath) const -> bool { auto basePath = Util::getConfigSubfolder(""); auto isChild = Util::isChildOrEquivalent(filepath, basePath); if (isChild) { string msg = FS(_F("Do not open Autosave files. They may will be overwritten!\n" "Copy the files to another folder.\n" "Files from Folder {1} cannot be opened.") % basePath.u8string()); XojMsgBox::showErrorToUser(getGtkWindow(), msg); } return !isChild; } auto Control::openFile(fs::path filepath, int scrollToPage, bool forceOpen) -> bool { if (filepath.empty()) { bool attachPdf = false; XojOpenDlg dlg(getGtkWindow(), this->settings); filepath = dlg.showOpenDialog(false, attachPdf); g_message("%s", (_F("file: {1}") % filepath.string()).c_str()); } if (filepath.empty() || (!forceOpen && !shouldFileOpen(filepath))) { return false; } if (!this->close(false)) { return false; } // Read template file if (filepath.extension() == ".xopt") { return loadXoptTemplate(filepath); } if (filepath.extension() == ".pdf") { return loadPdf(filepath, scrollToPage); } LoadHandler loadHandler; Document* loadedDocument = loadHandler.loadDocument(filepath); if ((loadedDocument != nullptr && loadHandler.isAttachedPdfMissing()) || !loadHandler.getMissingPdfFilename().empty()) { // give the user a second chance to select a new PDF filepath, or to discard the PDF GtkWidget* dialog = gtk_message_dialog_new( getGtkWindow(), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", loadHandler.isAttachedPdfMissing() ? _("The attached background PDF could not be found.") : _("The background PDF could not be found.")); gtk_dialog_add_button(GTK_DIALOG(dialog), _("Select another PDF"), 1); gtk_dialog_add_button(GTK_DIALOG(dialog), _("Remove PDF Background"), 2); gtk_dialog_add_button(GTK_DIALOG(dialog), _("Cancel"), 3); gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(this->getWindow()->getWindow())); int res = gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); if (res == 2) // remove PDF background { loadHandler.removePdfBackground(); loadedDocument = loadHandler.loadDocument(filepath); } else if (res == 1) // select another PDF background { bool attachToDocument = false; XojOpenDlg dlg(getGtkWindow(), this->settings); auto pdfFilename = dlg.showOpenDialog(true, attachToDocument); if (!pdfFilename.empty()) { loadHandler.setPdfReplacement(pdfFilename, attachToDocument); loadedDocument = loadHandler.loadDocument(filepath); } } } if (!loadedDocument) { string msg = FS(_F("Error opening file \"{1}\"") % filepath.u8string()) + "\n" + loadHandler.getLastError(); XojMsgBox::showErrorToUser(getGtkWindow(), msg); fileLoaded(scrollToPage); return false; } else if (loadHandler.getFileVersion() > FILE_FORMAT_VERSION) { GtkWidget* dialog = gtk_message_dialog_new( getGtkWindow(), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, "%s", _("The file being loaded has a file format version newer than the one currently supported by this " "version of Xournal++, so it may not load properly. Open anyways?")); int response = gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); if (response != GTK_RESPONSE_YES) { loadedDocument->clearDocument(); return false; } } this->closeDocument(); this->doc->lock(); this->doc->clearDocument(); *this->doc = *loadedDocument; this->doc->unlock(); // Set folder as last save path, so the next save will be at the current document location // This is important because of the new .xopp format, where Xournal .xoj handled as import, // not as file to load settings->setLastSavePath(filepath.parent_path()); fileLoaded(scrollToPage); return true; } auto Control::loadPdf(const fs::path& filepath, int scrollToPage) -> bool { LoadHandler loadHandler; if (settings->isAutloadPdfXoj()) { fs::path f = filepath; Util::clearExtensions(f); f += ".xopp"; Document* tmp = loadHandler.loadDocument(f); if (tmp == nullptr) { f = filepath; Util::clearExtensions(f); f += ".xoj"; tmp = loadHandler.loadDocument(f); } if (tmp) { this->doc->lock(); this->doc->clearDocument(); *this->doc = *tmp; this->doc->unlock(); fileLoaded(scrollToPage); return true; } } bool an = annotatePdf(filepath, false, false); fileLoaded(scrollToPage); return an; } auto Control::loadXoptTemplate(fs::path const& filepath) -> bool { auto contents = Util::readString(filepath); if (!contents.has_value()) { return false; } newFile(*contents); return true; } void Control::fileLoaded(int scrollToPage) { this->doc->lock(); auto filepath = this->doc->getEvMetadataFilename(); this->doc->unlock(); if (!filepath.empty()) { MetadataEntry md = MetadataManager::getForFile(filepath); if (!md.valid) { md.zoom = -1; md.page = 0; } if (scrollToPage >= 0) { md.page = scrollToPage; } loadMetadata(md); RecentManager::addRecentFileFilename(filepath); } else { zoom->updateZoomFitValue(); zoom->setZoomFitMode(true); } updateWindowTitle(); win->getXournal()->forceUpdatePagenumbers(); getCursor()->updateCursor(); updateDeletePageButton(); } class MetadataCallbackData { public: Control* ctrl{}; MetadataEntry md; }; /** * Load the data after processing the document... */ auto Control::loadMetadataCallback(MetadataCallbackData* data) -> bool { if (!data->md.valid) { delete data; return false; } ZoomControl* zoom = data->ctrl->zoom; if (zoom->isZoomPresentationMode()) { data->ctrl->setViewPresentationMode(true); } else if (zoom->isZoomFitMode()) { zoom->updateZoomFitValue(); zoom->setZoomFitMode(true); } else { zoom->setZoomFitMode(false); zoom->setZoom(data->md.zoom * zoom->getZoom100Value()); } data->ctrl->scrollHandler->scrollToPage(data->md.page); delete data; // Do not call again! return false; } void Control::loadMetadata(MetadataEntry md) { auto* data = new MetadataCallbackData(); data->md = std::move(md); data->ctrl = this; g_idle_add(reinterpret_cast(loadMetadataCallback), data); } auto Control::annotatePdf(fs::path filepath, bool /*attachPdf*/, bool attachToDocument) -> bool { if (!this->close(false)) { return false; } if (filepath.empty()) { XojOpenDlg dlg(getGtkWindow(), this->settings); filepath = dlg.showOpenDialog(true, attachToDocument); if (filepath.empty()) { return false; } } this->closeDocument(); getCursor()->setCursorBusy(true); this->doc->setFilepath(""); bool res = this->doc->readPdf(filepath, true, attachToDocument); if (res) { RecentManager::addRecentFileFilename(filepath.c_str()); this->doc->lock(); auto filepath = this->doc->getEvMetadataFilename(); this->doc->unlock(); MetadataEntry md = MetadataManager::getForFile(filepath); loadMetadata(md); } else { this->doc->lock(); string errMsg = doc->getLastErrorMsg(); this->doc->unlock(); string msg = FS(_F("Error annotate PDF file \"{1}\"\n{2}") % filepath.u8string() % errMsg); XojMsgBox::showErrorToUser(getGtkWindow(), msg); } getCursor()->setCursorBusy(false); fireDocumentChanged(DOCUMENT_CHANGE_COMPLETE); getCursor()->updateCursor(); return true; } void Control::print() { this->doc->lock(); PrintHandler::print(this->doc, getCurrentPageNo(), this->getGtkWindow()); this->doc->unlock(); } void Control::block(const string& name) { if (this->isBlocking) { return; } // Disable all gui Control, to get full control over the application win->setControlTmpDisabled(true); getCursor()->setCursorBusy(true); disableSidebarTmp(true); this->statusbar = this->win->get("statusbar"); this->lbState = GTK_LABEL(this->win->get("lbState")); this->pgState = GTK_PROGRESS_BAR(this->win->get("pgState")); gtk_label_set_text(this->lbState, name.c_str()); gtk_widget_show(this->statusbar); this->maxState = 100; this->isBlocking = true; } void Control::unblock() { if (!this->isBlocking) { return; } this->win->setControlTmpDisabled(false); getCursor()->setCursorBusy(false); disableSidebarTmp(false); gtk_widget_hide(this->statusbar); this->isBlocking = false; } void Control::setMaximumState(int max) { this->maxState = max; } void Control::setCurrentState(int state) { Util::execInUiThread([=]() { gtk_progress_bar_set_fraction(this->pgState, gdouble(state) / this->maxState); }); } 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); gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dialog), true); GtkFileFilter* filterXoj = gtk_file_filter_new(); gtk_file_filter_set_name(filterXoj, _("Xournal++ files")); gtk_file_filter_add_pattern(filterXoj, "*.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::toGFilename(suggested_folder).c_str()); 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::toGFilename(this->settings->getLastOpenPath()).c_str(), nullptr); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), false); // handled below gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(this->getWindow()->getWindow())); while (true) { if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_OK) { gtk_widget_destroy(dialog); return false; } auto fileTmp = Util::fromGFilename(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog))); 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)) { break; } } auto filename = Util::fromGFilename(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog))); settings->setLastSavePath(filename.parent_path()); gtk_widget_destroy(dialog); this->doc->lock(); this->doc->setFilepath(filename); this->doc->unlock(); return true; } void Control::updateWindowTitle() { string title{}; this->doc->lock(); if (doc->getFilepath().empty()) { if (doc->getPdfFilepath().empty()) { title = _("Unsaved Document"); } else { if (undoRedo->isChanged()) { title += "*"; } title += doc->getPdfFilepath().filename().u8string(); } } else { if (undoRedo->isChanged()) { title += "*"; } title += doc->getFilepath().filename().u8string(); } this->doc->unlock(); title += " - Xournal++"; gtk_window_set_title(getGtkWindow(), title.c_str()); } void Control::exportAsPdf() { this->clearSelectionEndText(); exportBase(new PdfExportJob(this)); } void Control::exportAs() { this->clearSelectionEndText(); exportBase(new CustomExportJob(this)); } void Control::exportBase(BaseExportJob* job) { if (job->showFilechooser()) { this->scheduler->addJob(job, JOB_PRIORITY_NONE); } else { // The job blocked, so we have to unblock, because the job unblocks only after run unblock(); } job->unref(); } auto Control::saveAs() -> bool { if (!showSaveDialog()) { return false; } this->doc->lock(); auto filepath = doc->getFilepath(); this->doc->unlock(); if (filepath.empty()) { return false; } // no lock needed, this is an uncritically operation this->doc->setCreateBackupOnSave(false); return save(); } void Control::resetSavedStatus() { this->doc->lock(); auto filepath = this->doc->getFilepath(); this->doc->unlock(); this->undoRedo->documentSaved(); RecentManager::addRecentFileFilename(filepath); this->updateWindowTitle(); } void Control::quit(bool allowCancel) { if (!this->close(false, allowCancel)) { if (!allowCancel) { // Cancel is not allowed, and the user close or did not save // This is probably called from macOS, where the Application // now will be killed - therefore do an emergency save. emergencySave(); } return; } this->closeDocument(); this->scheduler->lock(); audioController->stopRecording(); settings->save(); this->scheduler->removeAllJobs(); this->scheduler->unlock(); g_application_quit(G_APPLICATION(gtkApp)); } auto Control::close(const bool allowDestroy, const bool allowCancel) -> bool { clearSelectionEndText(); metadata->documentChanged(); bool discard = false; const bool fileRemoved = !doc->getFilepath().empty() && !fs::exists(this->doc->getFilepath()); if (undoRedo->isChanged()) { const auto message = fileRemoved ? _("Document file was removed.") : _("This document is not saved yet."); const auto saveLabel = fileRemoved ? _("Save As...") : _("Save"); GtkWidget* dialog = gtk_message_dialog_new(getGtkWindow(), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_NONE, "%s", message); gtk_dialog_add_button(GTK_DIALOG(dialog), saveLabel, GTK_RESPONSE_ACCEPT); gtk_dialog_add_button(GTK_DIALOG(dialog), _("Discard"), GTK_RESPONSE_REJECT); if (allowCancel) { gtk_dialog_add_button(GTK_DIALOG(dialog), _("Cancel"), GTK_RESPONSE_CANCEL); } gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(this->getWindow()->getWindow())); const auto dialogResponse = gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); switch (dialogResponse) { case GTK_RESPONSE_ACCEPT: if (fileRemoved) { return this->saveAs(); } else { return this->save(true); } break; case GTK_RESPONSE_REJECT: discard = true; break; default: return false; break; } } if (allowDestroy && discard) { this->closeDocument(); } return true; } void Control::closeDocument() { this->undoRedo->clearContents(); this->doc->lock(); this->doc->clearDocument(true); this->doc->unlock(); this->undoRedoChanged(); } void Control::applyPreferredLanguage() { #ifdef _WIN32 _putenv_s("LANGUAGE", this->settings->getPreferredLocale().c_str()); #else setenv("LANGUAGE", this->settings->getPreferredLocale().c_str(), 1); #endif } void Control::initButtonTool() { vector