mirror of
https://gitcode.com/shufeng2012/ohos-xournalpp.git
synced 2026-11-08 02:11:11 +08:00
🚚 移动源码
This commit is contained in:
1 parent
bfeaad1899
commit
1ae3987fab
830 files changed
+95931
No files matched your search
@@ -0,0 +1,174 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
cmake_policy(VERSION 3.13)
|
||||||
|
|
||||||
|
# Todo: add to target by generator expression
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# xoj default option target
|
||||||
|
##############################
|
||||||
|
|
||||||
|
add_library(defaults INTERFACE)
|
||||||
|
target_include_directories(defaults INTERFACE ${PROJECT_BINARY_DIR}/src)
|
||||||
|
add_library(xoj::defaults ALIAS defaults)
|
||||||
|
if (MSVC) # clang-cl.exe, cl.exe
|
||||||
|
target_compile_definitions(defaults INTERFACE NOMINMAX WIN32_LEAN_AND_MEAN)
|
||||||
|
target_compile_options(defaults INTERFACE -utf-8)
|
||||||
|
else(MSVC) # gnu clang with gnu driver,
|
||||||
|
target_compile_options(defaults INTERFACE -Wall -Wreturn-type -Wuninitialized -Wunused-value -Wunused-variable -Wconversion)
|
||||||
|
endif ()
|
||||||
|
# Todo (support-v3.10): guard this variable with cmake_dependent_option for CMake 3.10, and NOT(GCC)
|
||||||
|
# then downgrade cmake_minimum_required to CMake 3.10
|
||||||
|
if (DEV_ENABLE_GCOV)
|
||||||
|
target_compile_options(defaults INTERFACE --coverage)
|
||||||
|
target_link_options(defaults INTERFACE --coverage) #cmake v3.13
|
||||||
|
endif (DEV_ENABLE_GCOV)
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# util
|
||||||
|
##############################
|
||||||
|
|
||||||
|
add_subdirectory(util)
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# xournalpp-core
|
||||||
|
##############################
|
||||||
|
|
||||||
|
add_subdirectory(core)
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# xournalpp-wrapper: catches crashes
|
||||||
|
##############################
|
||||||
|
|
||||||
|
# The crash dialog's ui file is stored as gresource to make sure the wrapper is standalone
|
||||||
|
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
|
||||||
|
|
||||||
|
set(XOURNAL_WRAPPER_GRESOURCE_C xournalpp-wrapper.gresource.c)
|
||||||
|
set(XOURNAL_WRAPPER_GRESOURCE_XML exe/xournalpp-wrapper.gresource.xml)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${XOURNAL_WRAPPER_GRESOURCE_C}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
COMMAND ${GLIB_COMPILE_RESOURCES}
|
||||||
|
ARGS
|
||||||
|
--generate-source
|
||||||
|
--sourcedir=${CMAKE_SOURCE_DIR}
|
||||||
|
--target=${CMAKE_CURRENT_BINARY_DIR}/${XOURNAL_WRAPPER_GRESOURCE_C}
|
||||||
|
${XOURNAL_WRAPPER_GRESOURCE_XML}
|
||||||
|
VERBATIM
|
||||||
|
MAIN_DEPENDENCY ${XOURNAL_WRAPPER_GRESOURCE_XML}
|
||||||
|
DEPENDS
|
||||||
|
${CMAKE_SOURCE_DIR}/ui/crashDialog.glade
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(
|
||||||
|
xournalpp-wrapper-resource
|
||||||
|
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${XOURNAL_WRAPPER_GRESOURCE_C}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(xournalpp-wrapper exe/Xournalpp-wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/${XOURNAL_WRAPPER_GRESOURCE_C})
|
||||||
|
set_source_files_properties(
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/${XOURNAL_WRAPPER_GRESOURCE_C}
|
||||||
|
PROPERTIES GENERATED TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
add_dependencies(xournalpp-wrapper xournalpp-wrapper-resource)
|
||||||
|
target_link_libraries(xournalpp-wrapper
|
||||||
|
PUBLIC xoj::defaults xoj::util xoj::external_modules)
|
||||||
|
set_target_properties(xournalpp-wrapper PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||||
|
MACOSX_BUNDLE $<IF:$<PLATFORM_ID:Darwin>,TRUE,${CMAKE_MACOSX_BUNDLE}>
|
||||||
|
WIN32_EXECUTABLE $<IF:$<PLATFORM_ID:Windows>,TRUE,${CMAKE_WIN32_EXECUTABLE}>
|
||||||
|
)
|
||||||
|
target_include_directories(xournalpp-wrapper PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# xournalpp main program
|
||||||
|
##############################
|
||||||
|
|
||||||
|
add_executable(xournalpp exe/Xournalpp.cpp)
|
||||||
|
target_link_libraries(xournalpp PRIVATE xoj::core xoj::util)
|
||||||
|
set_target_properties(xournalpp PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||||
|
MACOSX_BUNDLE $<IF:$<PLATFORM_ID:Darwin>,TRUE,${CMAKE_MACOSX_BUNDLE}>
|
||||||
|
WIN32_EXECUTABLE $<IF:$<PLATFORM_ID:Windows>,TRUE,${CMAKE_WIN32_EXECUTABLE}>
|
||||||
|
)
|
||||||
|
if (WIN32)
|
||||||
|
# Make sure Windows executable has all symbols
|
||||||
|
target_link_options(xournalpp PRIVATE "-Wl,--export-all-symbols")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
###############################
|
||||||
|
# Generate icons
|
||||||
|
# Todo(fabian): move this into an own windows specific cmake script
|
||||||
|
# e.g. exe/win32/CMakeLists.txt
|
||||||
|
###############################
|
||||||
|
|
||||||
|
###############################
|
||||||
|
# Add MacOS specific files
|
||||||
|
# Todo(Roland): consider moving this into an own MacOS specific cmake script
|
||||||
|
# e.g. exe/macos/CMakeLists.txt
|
||||||
|
##############################
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
# MacOS specific source files
|
||||||
|
file (GLOB xournalpp-sources-osx exe/osx/*.cpp exe/osx/*.h)
|
||||||
|
target_sources(xournalpp PRIVATE ${xournalpp-sources-osx})
|
||||||
|
target_include_directories(xournalpp PRIVATE exe)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
# generate .ico from .png
|
||||||
|
find_package(ImageMagick REQUIRED COMPONENTS magick)
|
||||||
|
set (ICON_SIZES 16 32 48 256)
|
||||||
|
foreach (SIZE IN LISTS ICON_SIZES)
|
||||||
|
set (RESIZED_ICON ${PROJECT_BINARY_DIR}/xournalpp-icon-${SIZE}.png)
|
||||||
|
add_custom_command (
|
||||||
|
OUTPUT ${RESIZED_ICON}
|
||||||
|
COMMAND rsvg-convert ${PROJECT_SOURCE_DIR}/ui/pixmaps/com.github.xournalpp.xournalpp.svg --width=${SIZE} --height=${SIZE} -o ${RESIZED_ICON}
|
||||||
|
)
|
||||||
|
set (ICON_DEPS ${ICON_DEPS} ${RESIZED_ICON})
|
||||||
|
unset (RESIZED_ICON)
|
||||||
|
endforeach()
|
||||||
|
add_custom_command (
|
||||||
|
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/win32/xournalpp.ico
|
||||||
|
COMMAND magick convert ${ICON_DEPS} ${CMAKE_CURRENT_BINARY_DIR}/win32/xournalpp.ico
|
||||||
|
DEPENDS ${ICON_DEPS}
|
||||||
|
)
|
||||||
|
unset(ICON_SIZES)
|
||||||
|
unset(ICON_DEPS)
|
||||||
|
add_custom_target(xournalpp_icon DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/win32/xournalpp.ico)
|
||||||
|
add_dependencies(xournalpp xournalpp_icon)
|
||||||
|
add_dependencies(xournalpp-wrapper xournalpp_icon)
|
||||||
|
|
||||||
|
# windows specific source files
|
||||||
|
target_sources(xournalpp PRIVATE exe/win32/console.cpp)
|
||||||
|
target_sources(xournalpp-wrapper PRIVATE exe/win32/console.cpp)
|
||||||
|
if (MSVC)
|
||||||
|
target_sources(xournalpp PRIVATE exe/win32/gui_main.cpp)
|
||||||
|
target_sources(xournalpp-wrapper PRIVATE exe/win32/gui_main.cpp)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# windows specific appinfo
|
||||||
|
if(NOT MSVC)
|
||||||
|
set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> -O coff -i <SOURCE> -o <OBJECT>")
|
||||||
|
set(CMAKE_RC_COMPILER_INIT windres)
|
||||||
|
endif()
|
||||||
|
enable_language(RC)
|
||||||
|
|
||||||
|
configure_file(exe/win32/xpp.rc.in ${CMAKE_CURRENT_BINARY_DIR}/win32/xpp.rc)
|
||||||
|
target_sources(xournalpp PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/win32/xpp.rc)
|
||||||
|
target_sources(xournalpp-wrapper PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/win32/xpp.rc)
|
||||||
|
endif ()
|
||||||
|
# Until here
|
||||||
|
################################
|
||||||
|
|
||||||
|
install(TARGETS xournalpp-wrapper
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
COMPONENT xournalpp-wrapper
|
||||||
|
)
|
||||||
|
install(TARGETS xournalpp
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
COMPONENT xournalpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_subdirectory(xoj-preview-extractor)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Static debugging definitions for Xournal++
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input debugging, e.g. eraser events etc.
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_INPUT
|
||||||
|
#cmakedefine DEBUG_INPUT_GDK_PRINT_EVENTS
|
||||||
|
#cmakedefine DEBUG_INPUT_PRINT_ALL_MOTION_EVENTS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape recognizer debug: output score etc.
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_RECOGNIZER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduler debug: show jobs etc.
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_SCHEDULER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a surrounding border to all elements
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_SHOW_ELEMENT_BOUNDS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a border around all repaint rects
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_SHOW_REPAINT_BOUNDS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a border around all painted rects
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_SHOW_PAINT_BOUNDS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the mask in StrokeView
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_MASKS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print info on draw requests and calls
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_DRAW_WIDGET
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show repainted areas when erasing an highlighter stroke
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_ERASABLE_STROKE_BOXES
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show eraser logging info when we get an inconsistent result
|
||||||
|
*/
|
||||||
|
#cmakedefine ENABLE_ERASER_DEBUG
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable some debugging output around the action database
|
||||||
|
*/
|
||||||
|
#cmakedefine DEBUG_ACTION_DB
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Static definitions for Xournal++
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolbar config file name
|
||||||
|
*/
|
||||||
|
#define TOOLBAR_CONFIG "@DEV_TOOLBAR_CONFIG@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings file name
|
||||||
|
*/
|
||||||
|
#define SETTINGS_XML_FILE "@DEV_SETTINGS_XML_FILE@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gimp Palette File
|
||||||
|
*/
|
||||||
|
#define DEFAULT_PALETTE_FILE "@DEV_DEFAULT_PALETTE_FILE@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print config file name
|
||||||
|
*/
|
||||||
|
#define PRINT_CONFIG_FILE "@DEV_PRINT_CONFIG_FILE@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata file name
|
||||||
|
*/
|
||||||
|
#define METADATA_FILE "@DEV_METADATA_FILE@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory where errorlogfiles will be placed
|
||||||
|
*/
|
||||||
|
#define ERRORLOG_DIR "@DEV_ERRORLOG_DIR@"
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Logging --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile with call log
|
||||||
|
*/
|
||||||
|
#cmakedefine DEV_CALL_LOG
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Additional features of Xournal++
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/* --- Stable features --- */
|
||||||
|
|
||||||
|
#cmakedefine ENABLE_PLUGINS
|
||||||
|
|
||||||
|
#cmakedefine ENABLE_GTK_SOURCEVIEW
|
||||||
|
|
||||||
|
#cmakedefine ENABLE_AUDIO
|
||||||
|
|
||||||
|
/* --- Testing features --- */
|
||||||
|
|
||||||
|
#cmakedefine ENABLE_QPDF
|
||||||
|
|
||||||
|
#cmakedefine ENABLE_CPPTRACE
|
||||||
|
|
||||||
|
// Example: #cmakedefine NAME_FROM_Cmake_list
|
||||||
|
// in CMakeFile.txt:
|
||||||
|
// option (NAME_FROM_Cmake_list "Description" OFF)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Current git repo info
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Current git repo info --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Origin URL of current repository
|
||||||
|
*/
|
||||||
|
constexpr auto GIT_ORIGIN_URL = "@GIT_ORIGIN_URL@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git origin repo owner
|
||||||
|
*/
|
||||||
|
constexpr auto GIT_ORIGIN_OWNER = "@GIT_ORIGIN_OWNER@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git origin repo name
|
||||||
|
*/
|
||||||
|
constexpr auto GIT_ORIGIN_REPO = "@GIT_ORIGIN_REPO@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current branch of git repo
|
||||||
|
*/
|
||||||
|
constexpr auto GIT_BRANCH = "@GIT_BRANCH@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current git commit id (with "-dirty" appended if dirty source tree)
|
||||||
|
*/
|
||||||
|
constexpr auto GIT_COMMIT_ID = "@RELEASE_IDENTIFIER@";
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Paths to resources
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to current source directory
|
||||||
|
*/
|
||||||
|
#define PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to install directory (for integration test)
|
||||||
|
*/
|
||||||
|
#define PROJECT_INSTALL_DIR "@PROJECT_INSTALL_DIR@"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Static project constants
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/* --- Project info --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full name of this project and package
|
||||||
|
*/
|
||||||
|
constexpr auto PROJECT_NAME = "@PROJECT_NAME@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project version number
|
||||||
|
*/
|
||||||
|
constexpr auto PROJECT_VERSION = "@PROJECT_VERSION@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address where crash reports for this project should be sent
|
||||||
|
*/
|
||||||
|
constexpr auto PROJECT_CRASHREPORT = "@PROJECT_CRASHREPORT@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full name and version of this project
|
||||||
|
*/
|
||||||
|
constexpr auto PROJECT_STRING = "@PROJECT_NAME@ @PROJECT_VERSION@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project URL
|
||||||
|
*/
|
||||||
|
constexpr auto PROJECT_HOMEPAGE_URL = "@PROJECT_HOMEPAGE_URL@";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File format version
|
||||||
|
*/
|
||||||
|
constexpr int FILE_FORMAT_VERSION = @DEV_FILE_FORMAT_VERSION@;
|
||||||
|
|
||||||
|
|
||||||
|
/* --- I18N --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable i18n
|
||||||
|
*/
|
||||||
|
#cmakedefine ENABLE_NLS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gettext package
|
||||||
|
*/
|
||||||
|
constexpr auto GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@";
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.12)
|
||||||
|
cmake_policy(VERSION 3.12)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE xournalpp_sources *.cpp *.h)
|
||||||
|
|
||||||
|
if(NOT ENABLE_AUDIO)
|
||||||
|
list(FILTER xournalpp_sources EXCLUDE REGEX "${PROJECT_SOURCE_DIR}/src/core/audio/.*" )
|
||||||
|
list(FILTER xournalpp_sources EXCLUDE REGEX "${PROJECT_SOURCE_DIR}/src/core/control/AudioController.cpp" )
|
||||||
|
endif()
|
||||||
|
set(xournalpp_include_dirs "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
|
add_library(xournalpp-core STATIC ${xournalpp_sources})
|
||||||
|
target_compile_features(xournalpp-core PUBLIC ${PROJECT_CXX_FEATURES})
|
||||||
|
target_compile_features(xournalpp-core PUBLIC cxx_std_20)
|
||||||
|
target_link_libraries(xournalpp-core
|
||||||
|
PRIVATE xoj::util ${xournalpp_LDFLAGS}
|
||||||
|
PUBLIC xoj::defaults xoj::external_modules
|
||||||
|
)
|
||||||
|
target_include_directories(xournalpp-core PUBLIC ${xournalpp_include_dirs})
|
||||||
|
add_library(xoj::core ALIAS xournalpp-core)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "AudioPlayer.h"
|
||||||
|
|
||||||
|
#include "audio/AudioQueue.h" // for AudioQueue
|
||||||
|
#include "audio/DeviceInfo.h" // for DeviceInfo
|
||||||
|
#include "audio/PortAudioConsumer.h" // for PortAudioConsumer
|
||||||
|
#include "audio/VorbisProducer.h" // for VorbisProducer
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/actions/ActionDatabase.h" // for ActionDatabase
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
|
||||||
|
AudioPlayer::AudioPlayer(Control& control, Settings& settings):
|
||||||
|
control(control),
|
||||||
|
settings(settings),
|
||||||
|
audioQueue(std::make_unique<AudioQueue<float>>()),
|
||||||
|
portAudioConsumer(std::make_unique<PortAudioConsumer>(*this, *audioQueue)),
|
||||||
|
vorbisProducer(std::make_unique<VorbisProducer>(*audioQueue)) {}
|
||||||
|
|
||||||
|
AudioPlayer::~AudioPlayer() { this->stop(); }
|
||||||
|
|
||||||
|
auto AudioPlayer::start(fs::path const& file, unsigned int timestamp) -> bool {
|
||||||
|
// Start the producer for reading the data
|
||||||
|
bool status = this->vorbisProducer->start(file, timestamp);
|
||||||
|
|
||||||
|
// Start playing
|
||||||
|
if (status) {
|
||||||
|
status = status && this->play();
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioPlayer::isPlaying() -> bool { return this->portAudioConsumer->isPlaying(); }
|
||||||
|
|
||||||
|
void AudioPlayer::pause() {
|
||||||
|
if (!this->portAudioConsumer->isPlaying()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop playing audio
|
||||||
|
this->portAudioConsumer->stopPlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioPlayer::play() -> bool {
|
||||||
|
if (this->portAudioConsumer->isPlaying()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->portAudioConsumer->startPlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioPlayer::disableAudioPlaybackButtons() {
|
||||||
|
if (this->audioQueue->hasStreamEnded()) {
|
||||||
|
auto* actionDB = this->control.getActionDatabase();
|
||||||
|
actionDB->enableAction(Action::AUDIO_PAUSE_PLAYBACK, false);
|
||||||
|
actionDB->enableAction(Action::AUDIO_STOP_PLAYBACK, false);
|
||||||
|
actionDB->enableAction(Action::AUDIO_SEEK_FORWARDS, false);
|
||||||
|
actionDB->enableAction(Action::AUDIO_SEEK_BACKWARDS, false);
|
||||||
|
actionDB->setActionState(Action::AUDIO_PAUSE_PLAYBACK, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioPlayer::stop() {
|
||||||
|
disableAudioPlaybackButtons();
|
||||||
|
// Stop playing audio
|
||||||
|
this->portAudioConsumer->stopPlaying();
|
||||||
|
|
||||||
|
this->audioQueue->signalEndOfStream();
|
||||||
|
|
||||||
|
// Abort libsox
|
||||||
|
this->vorbisProducer->abort();
|
||||||
|
|
||||||
|
// Reset the queue for the next playback
|
||||||
|
this->audioQueue->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioPlayer::seek(int seconds) {
|
||||||
|
// set seek flag here in vorbisProducer
|
||||||
|
this->vorbisProducer->seek(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioPlayer::getOutputDevices() -> std::vector<DeviceInfo> { return this->portAudioConsumer->getOutputDevices(); }
|
||||||
|
|
||||||
|
auto AudioPlayer::getSettings() -> Settings& { return this->settings; }
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class to play audio from a MP3-file
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for make_unique, unique_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue;
|
||||||
|
class Control;
|
||||||
|
class DeviceInfo;
|
||||||
|
class PortAudioConsumer;
|
||||||
|
class Settings;
|
||||||
|
class VorbisProducer;
|
||||||
|
|
||||||
|
|
||||||
|
class AudioPlayer final {
|
||||||
|
public:
|
||||||
|
explicit AudioPlayer(Control& control, Settings& settings);
|
||||||
|
AudioPlayer(AudioPlayer const&) = delete;
|
||||||
|
AudioPlayer(AudioPlayer&&) = delete;
|
||||||
|
auto operator=(AudioPlayer const&) -> AudioPlayer& = delete;
|
||||||
|
auto operator=(AudioPlayer&&) -> AudioPlayer& = delete;
|
||||||
|
~AudioPlayer();
|
||||||
|
|
||||||
|
bool start(fs::path const& file, unsigned int timestamp = 0);
|
||||||
|
bool isPlaying();
|
||||||
|
void stop();
|
||||||
|
bool play();
|
||||||
|
void pause();
|
||||||
|
void seek(int seconds);
|
||||||
|
|
||||||
|
std::vector<DeviceInfo> getOutputDevices();
|
||||||
|
|
||||||
|
Settings& getSettings();
|
||||||
|
void disableAudioPlaybackButtons();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Control& control;
|
||||||
|
Settings& settings;
|
||||||
|
|
||||||
|
std::unique_ptr<AudioQueue<float>> audioQueue;
|
||||||
|
std::unique_ptr<PortAudioConsumer> portAudioConsumer;
|
||||||
|
std::unique_ptr<VorbisProducer> vorbisProducer;
|
||||||
|
};
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Queue to connect an audio producer and an audio consumer
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
|
#include <limits>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "util/Assert.h"
|
||||||
|
#include "util/safe_casts.h" // for as_signed
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue {
|
||||||
|
public:
|
||||||
|
void reset() {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
this->popNotified = false;
|
||||||
|
this->pushNotified = false;
|
||||||
|
this->streamEnd = false;
|
||||||
|
internalQueue.clear();
|
||||||
|
|
||||||
|
this->sampleRate = -1;
|
||||||
|
this->channels = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
return internalQueue.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
return internalQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Iter>
|
||||||
|
void emplace(Iter begI, Iter endI) {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
std::move(begI, endI, std::front_inserter(internalQueue));
|
||||||
|
|
||||||
|
this->pushNotified = true;
|
||||||
|
this->pushLockCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename InsertIter>
|
||||||
|
InsertIter pop(InsertIter insertIter, size_t nSamples) {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
|
||||||
|
if (this->channels == 0) {
|
||||||
|
this->popNotified = true;
|
||||||
|
this->popLockCondition.notify_one();
|
||||||
|
return insertIter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto queueSize = internalQueue.size();
|
||||||
|
auto returnBufferLength = std::min<size_t>(nSamples, queueSize - queueSize % this->channels);
|
||||||
|
auto begI = rbegin(internalQueue);
|
||||||
|
auto endI = std::next(begI, as_signed(returnBufferLength));
|
||||||
|
|
||||||
|
auto ret = std::move(begI, endI, insertIter);
|
||||||
|
internalQueue.erase(endI.base(), begI.base());
|
||||||
|
|
||||||
|
this->popNotified = true;
|
||||||
|
this->popLockCondition.notify_one();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void signalEndOfStream() {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
this->streamEnd = true;
|
||||||
|
this->pushNotified = true;
|
||||||
|
this->popNotified = true;
|
||||||
|
this->pushLockCondition.notify_one();
|
||||||
|
this->popLockCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitForProducer(std::unique_lock<std::mutex>& lock) {
|
||||||
|
// static_assert(lock.mutex() == &this->queueLock);
|
||||||
|
xoj_assert(lock.mutex() == &this->queueLock);
|
||||||
|
while (!this->pushNotified && !hasStreamEnded()) { this->pushLockCondition.wait(lock); }
|
||||||
|
this->pushNotified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitForConsumer(std::unique_lock<std::mutex>& lock) {
|
||||||
|
// static_assert(lock.mutex() == &this->queueLock);
|
||||||
|
xoj_assert(lock.mutex() == &this->queueLock);
|
||||||
|
while (!this->popNotified && !hasStreamEnded()) { this->popLockCondition.wait(lock); }
|
||||||
|
this->popNotified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasStreamEnded() {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
return this->streamEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::unique_lock<std::mutex> acquire_lock() {
|
||||||
|
std::unique_lock retLock{this->queueLock, std::defer_lock};
|
||||||
|
std::lock(retLock, this->internalLock);
|
||||||
|
std::lock_guard lock{this->internalLock, std::adopt_lock};
|
||||||
|
return retLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAudioAttributes(double lSampleRate, unsigned int lChannels) {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
this->sampleRate = lSampleRate;
|
||||||
|
this->channels = lChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a pair of the sample rate and the channel count,
|
||||||
|
* std::pair<>::first is the sample rate and std::pair<>::second the channel count.
|
||||||
|
* Todo (readability, type-safety): create a struct AudioAttributes; remove this comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
[[nodiscard]] std::pair<double, int> getAudioAttributes() {
|
||||||
|
std::lock_guard<std::mutex> lock(internalLock);
|
||||||
|
return {this->sampleRate, this->channels};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex queueLock;
|
||||||
|
std::mutex internalLock;
|
||||||
|
|
||||||
|
std::deque<T> internalQueue;
|
||||||
|
|
||||||
|
std::condition_variable pushLockCondition;
|
||||||
|
std::condition_variable popLockCondition;
|
||||||
|
|
||||||
|
double sampleRate{std::numeric_limits<double>::quiet_NaN()};
|
||||||
|
uint32_t channels{0};
|
||||||
|
|
||||||
|
bool streamEnd{false};
|
||||||
|
bool pushNotified{false};
|
||||||
|
bool popNotified{false};
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#include "AudioRecorder.h"
|
||||||
|
|
||||||
|
#include "audio/AudioQueue.h" // for AudioQueue
|
||||||
|
#include "audio/DeviceInfo.h" // for DeviceInfo
|
||||||
|
#include "audio/PortAudioProducer.h" // for PortAudioProducer
|
||||||
|
#include "audio/VorbisConsumer.h" // for VorbisConsumer
|
||||||
|
|
||||||
|
AudioRecorder::AudioRecorder(Settings& settings):
|
||||||
|
audioQueue(std::make_unique<AudioQueue<float>>()),
|
||||||
|
portAudioProducer(std::make_unique<PortAudioProducer>(settings, *audioQueue)),
|
||||||
|
vorbisConsumer(std::make_unique<VorbisConsumer>(settings, *audioQueue)) {}
|
||||||
|
|
||||||
|
AudioRecorder::~AudioRecorder() { this->stop(); }
|
||||||
|
|
||||||
|
auto AudioRecorder::start(fs::path const& file) -> bool {
|
||||||
|
bool status = this->portAudioProducer->startRecording();
|
||||||
|
// Start the consumer for writing the data
|
||||||
|
status = status && this->vorbisConsumer->start(file);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioRecorder::stop() {
|
||||||
|
this->portAudioProducer->stopRecording();
|
||||||
|
this->vorbisConsumer->join(); // libsox must write all the data before we can continue
|
||||||
|
this->audioQueue->reset(); // next recording requires empty queue
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioRecorder::isRecording() const -> bool { return this->portAudioProducer->isRecording(); }
|
||||||
|
|
||||||
|
auto AudioRecorder::getInputDevices() const -> std::vector<DeviceInfo> {
|
||||||
|
return this->portAudioProducer->getInputDevices();
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class to record audio and store it as MP3-file
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for make_unique, unique_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue;
|
||||||
|
class DeviceInfo;
|
||||||
|
class PortAudioProducer;
|
||||||
|
class Settings;
|
||||||
|
class VorbisConsumer;
|
||||||
|
|
||||||
|
class AudioRecorder final {
|
||||||
|
public:
|
||||||
|
explicit AudioRecorder(Settings& settings);
|
||||||
|
AudioRecorder(AudioRecorder const&) = delete;
|
||||||
|
AudioRecorder(AudioRecorder&&) = delete;
|
||||||
|
auto operator=(AudioRecorder const&) -> AudioRecorder& = delete;
|
||||||
|
auto operator=(AudioRecorder&&) -> AudioRecorder& = delete;
|
||||||
|
~AudioRecorder();
|
||||||
|
|
||||||
|
bool start(fs::path const& file);
|
||||||
|
void stop();
|
||||||
|
[[nodiscard]] bool isRecording() const;
|
||||||
|
std::vector<DeviceInfo> getInputDevices() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<AudioQueue<float>> audioQueue;
|
||||||
|
std::unique_ptr<PortAudioProducer> portAudioProducer;
|
||||||
|
std::unique_ptr<VorbisConsumer> vorbisConsumer;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#include "DeviceInfo.h"
|
||||||
|
|
||||||
|
DeviceInfo::DeviceInfo(portaudio::Device* device, bool selected):
|
||||||
|
deviceName(device->name()),
|
||||||
|
index(device->index()),
|
||||||
|
selected(selected),
|
||||||
|
inputChannels((device->isFullDuplexDevice() || device->isInputOnlyDevice()) ? device->maxInputChannels() : 0),
|
||||||
|
outputChannels((device->isFullDuplexDevice() || device->isOutputOnlyDevice()) ? device->maxOutputChannels() :
|
||||||
|
0) {}
|
||||||
|
|
||||||
|
auto DeviceInfo::getDeviceName() const -> const std::string& { return deviceName; }
|
||||||
|
|
||||||
|
auto DeviceInfo::getIndex() const -> PaDeviceIndex { return index; }
|
||||||
|
|
||||||
|
auto DeviceInfo::getSelected() const -> bool { return selected; }
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class storing information about an audio device
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "config-features.h"
|
||||||
|
|
||||||
|
#ifdef ENABLE_AUDIO
|
||||||
|
#include <portaudiocpp/PortAudioCpp.hxx> // for PaDeviceIndex
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceInfo {
|
||||||
|
public:
|
||||||
|
DeviceInfo(portaudio::Device* device, bool selected);
|
||||||
|
|
||||||
|
public:
|
||||||
|
const std::string& getDeviceName() const;
|
||||||
|
PaDeviceIndex getIndex() const;
|
||||||
|
bool getSelected() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::string deviceName;
|
||||||
|
const PaDeviceIndex index{};
|
||||||
|
const bool selected{};
|
||||||
|
const int inputChannels{};
|
||||||
|
const int outputChannels{};
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#include "PortAudioConsumer.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for for_each, transform, max
|
||||||
|
#include <deque> // for _Deque_iterator
|
||||||
|
#include <iterator> // for next, prev
|
||||||
|
#include <string> // for to_string, string
|
||||||
|
|
||||||
|
#include <glib.h> // for g_warning
|
||||||
|
|
||||||
|
#include "audio/AudioQueue.h" // for AudioQueue
|
||||||
|
#include "audio/DeviceInfo.h" // for DeviceInfo
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "util/safe_casts.h" // for as_unsigned
|
||||||
|
|
||||||
|
#include "AudioPlayer.h" // for AudioPlayer
|
||||||
|
|
||||||
|
constexpr auto FRAMES_PER_BUFFER{64U};
|
||||||
|
|
||||||
|
auto PortAudioConsumer::getOutputDevices() const -> std::vector<DeviceInfo> {
|
||||||
|
std::vector<DeviceInfo> deviceList;
|
||||||
|
deviceList.reserve(as_unsigned(this->sys.deviceCount()));
|
||||||
|
|
||||||
|
for (auto i = this->sys.devicesBegin(); i != sys.devicesEnd(); ++i) {
|
||||||
|
if (i->isFullDuplexDevice() || i->isOutputOnlyDevice()) {
|
||||||
|
DeviceInfo deviceInfo(&(*i), this->audioPlayer.getSettings().getAudioOutputDevice() == i->index());
|
||||||
|
deviceList.push_back(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deviceList;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PortAudioConsumer::getSelectedOutputDevice() const -> DeviceInfo {
|
||||||
|
PaDeviceIndex idx = this->audioPlayer.getSettings().getAudioOutputDevice();
|
||||||
|
if (idx == -1) {
|
||||||
|
return DeviceInfo(&sys.defaultOutputDevice(), true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return DeviceInfo(&sys.deviceByIndex(idx), true);
|
||||||
|
} catch (const portaudio::PaException& e) {
|
||||||
|
g_warning("PortAudioConsumer: Selected output device was not found - fallback to default output device\nCaused "
|
||||||
|
"by: %s",
|
||||||
|
e.what());
|
||||||
|
return DeviceInfo(&sys.defaultOutputDevice(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PortAudioConsumer::isPlaying() const -> bool { return this->outputStream && this->outputStream->isActive(); }
|
||||||
|
|
||||||
|
auto PortAudioConsumer::startPlaying() -> bool {
|
||||||
|
// Abort a playback stream if one is currently active
|
||||||
|
if (isPlaying()) {
|
||||||
|
this->outputStream->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [sampleRate, channels] = this->audioQueue.getAudioAttributes();
|
||||||
|
|
||||||
|
if (sampleRate == -1) {
|
||||||
|
g_warning("PortAudioConsumer: Timing issue - Sample rate requested before known");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the device information of our output device
|
||||||
|
portaudio::Device* device = nullptr;
|
||||||
|
try {
|
||||||
|
device = &sys.deviceByIndex(getSelectedOutputDevice().getIndex());
|
||||||
|
} catch (const portaudio::PaException&) {
|
||||||
|
g_warning("PortAudioConsumer: Unable to find selected output device");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device->maxOutputChannels() < channels) {
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
g_warning("Output device has not enough channels to play audio file. (Requires at least 2 channels)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->outputChannels = channels;
|
||||||
|
portaudio::DirectionSpecificStreamParameters outParams(*device, channels, portaudio::FLOAT32, true,
|
||||||
|
device->defaultLowOutputLatency(), nullptr);
|
||||||
|
portaudio::StreamParameters params(portaudio::DirectionSpecificStreamParameters::null(), outParams, sampleRate,
|
||||||
|
FRAMES_PER_BUFFER, paNoFlag);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this->outputStream = std::make_unique<portaudio::MemFunCallbackStream<PortAudioConsumer>>(
|
||||||
|
params, *this, &PortAudioConsumer::playCallback);
|
||||||
|
} catch (const portaudio::PaException& e) {
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
g_warning("PortAudioConsumer: Unable to open stream to device\nCaused by: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Start the recording
|
||||||
|
try {
|
||||||
|
this->outputStream->start();
|
||||||
|
} catch (const portaudio::PaException& e) {
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
g_warning("PortAudioConsumer: Unable to start stream\nCaused by: %s", e.what());
|
||||||
|
this->outputStream.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PortAudioConsumer::playCallback(const void* /*inputBuffer*/, void* outputBuffer, unsigned long framesPerBuffer,
|
||||||
|
const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags statusFlags) -> int {
|
||||||
|
if (statusFlags) {
|
||||||
|
g_warning("PortAudioConsumer: PortAudio reported a stream warning: %s", std::to_string(statusFlags).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputBuffer != nullptr) {
|
||||||
|
auto begI = static_cast<float*>(outputBuffer);
|
||||||
|
auto midI = this->audioQueue.pop(begI, framesPerBuffer * as_unsigned(this->outputChannels));
|
||||||
|
auto endI = std::next(begI, as_signed(framesPerBuffer) * this->outputChannels);
|
||||||
|
// Fill buffer to requested length if necessary
|
||||||
|
|
||||||
|
if (midI != endI) {
|
||||||
|
// Show underflow warning if there are not enough samples and the stream is not yet finished
|
||||||
|
if (!this->audioQueue.hasStreamEnded()) {
|
||||||
|
g_warning("PortAudioConsumer: Not enough audio samples available to fill requested frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (midI > std::next(begI, this->outputChannels)) {
|
||||||
|
// If there is previous audio data use this data to ramp down the audio samples
|
||||||
|
std::transform(std::prev(midI, this->outputChannels), std::prev(endI, this->outputChannels), midI,
|
||||||
|
[](auto&& ampl) { return ampl / 2.0; });
|
||||||
|
} else {
|
||||||
|
// If there is no data that could be used to ramp down just output silence
|
||||||
|
std::for_each(midI, endI, [](auto& ampl) { ampl = 0; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue playback if there is still data available
|
||||||
|
if (this->audioQueue.hasStreamEnded() && this->audioQueue.empty()) {
|
||||||
|
this->audioPlayer.disableAudioPlaybackButtons();
|
||||||
|
return paComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return paContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The output buffer is no longer available - Abort!
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
this->audioPlayer.disableAudioPlaybackButtons();
|
||||||
|
return paAbort;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortAudioConsumer::stopPlaying() {
|
||||||
|
// Stop the playback
|
||||||
|
if (this->outputStream) {
|
||||||
|
try {
|
||||||
|
if (this->outputStream->isActive()) {
|
||||||
|
this->outputStream->stop();
|
||||||
|
}
|
||||||
|
} catch (const portaudio::PaException&) {
|
||||||
|
/*
|
||||||
|
* We try closing the stream but this->outputStream might be an invalid object at this time if the stream
|
||||||
|
* was previously closed by the backend. Just ignore this as the stream is closed either way.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->outputStream.reset();
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class to play audio using libportaudio
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <portaudiocpp/PortAudioCpp.hxx> // for MemFunCallbackStream, System
|
||||||
|
|
||||||
|
#include "DeviceInfo.h" // for DeviceInfo
|
||||||
|
|
||||||
|
class AudioPlayer;
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue;
|
||||||
|
|
||||||
|
class PortAudioConsumer final {
|
||||||
|
public:
|
||||||
|
explicit PortAudioConsumer(AudioPlayer& audioPlayer, AudioQueue<float>& audioQueue):
|
||||||
|
audioPlayer(audioPlayer), audioQueue(audioQueue) {}
|
||||||
|
|
||||||
|
std::vector<DeviceInfo> getOutputDevices() const;
|
||||||
|
DeviceInfo getSelectedOutputDevice() const;
|
||||||
|
bool isPlaying() const;
|
||||||
|
bool startPlaying();
|
||||||
|
int playCallback(const void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
|
||||||
|
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags);
|
||||||
|
void stopPlaying();
|
||||||
|
|
||||||
|
private:
|
||||||
|
portaudio::System& sys{portaudio::System::instance()};
|
||||||
|
AudioPlayer& audioPlayer;
|
||||||
|
AudioQueue<float>& audioQueue;
|
||||||
|
|
||||||
|
std::unique_ptr<portaudio::MemFunCallbackStream<PortAudioConsumer>> outputStream;
|
||||||
|
|
||||||
|
int outputChannels = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#include "PortAudioProducer.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for min, max
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <iterator> // for next
|
||||||
|
#include <string> // for to_string, string
|
||||||
|
|
||||||
|
#include <glib.h> // for g_message
|
||||||
|
|
||||||
|
#include "audio/AudioQueue.h" // for AudioQueue
|
||||||
|
#include "audio/DeviceInfo.h" // for DeviceInfo
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "util/safe_casts.h" // for as_unsigned
|
||||||
|
|
||||||
|
|
||||||
|
constexpr auto FRAMES_PER_BUFFER{64U};
|
||||||
|
|
||||||
|
auto PortAudioProducer::getInputDevices() const -> std::vector<DeviceInfo> {
|
||||||
|
auto devCount = as_unsigned(this->sys.deviceCount());
|
||||||
|
std::vector<DeviceInfo> deviceList;
|
||||||
|
deviceList.reserve(devCount);
|
||||||
|
|
||||||
|
for (auto i = this->sys.devicesBegin(); i != sys.devicesEnd(); ++i) {
|
||||||
|
|
||||||
|
if (i->isFullDuplexDevice() || i->isInputOnlyDevice()) {
|
||||||
|
DeviceInfo deviceInfo(&(*i), this->settings.getAudioInputDevice() == i->index());
|
||||||
|
deviceList.push_back(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deviceList;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PortAudioProducer::getSelectedInputDevice() const -> DeviceInfo {
|
||||||
|
PaDeviceIndex idx = this->settings.getAudioInputDevice();
|
||||||
|
if (idx == -1) {
|
||||||
|
return DeviceInfo(&sys.defaultInputDevice(), true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return DeviceInfo(&sys.deviceByIndex(idx), true);
|
||||||
|
} catch (const portaudio::PaException& e) {
|
||||||
|
g_message(
|
||||||
|
"PortAudioProducer: Selected input device not found - fallback to default input device\nCaused by: %s",
|
||||||
|
e.what());
|
||||||
|
return DeviceInfo(&sys.defaultInputDevice(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PortAudioProducer::isRecording() const -> bool { return this->inputStream && this->inputStream->isActive(); }
|
||||||
|
|
||||||
|
auto PortAudioProducer::startRecording() -> bool {
|
||||||
|
// Check if there already is a recording
|
||||||
|
if (this->inputStream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the device information of our input device
|
||||||
|
portaudio::Device* device = nullptr;
|
||||||
|
try {
|
||||||
|
device = &sys.deviceByIndex(getSelectedInputDevice().getIndex());
|
||||||
|
} catch (const portaudio::PaException&) {
|
||||||
|
g_message("PortAudioProducer: Unable to find selected input device");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict recording channels to 2 as playback devices should have 2 channels at least
|
||||||
|
this->inputChannels = std::min(2, device->maxInputChannels());
|
||||||
|
portaudio::DirectionSpecificStreamParameters inParams(*device, this->inputChannels, portaudio::FLOAT32, true,
|
||||||
|
device->defaultLowInputLatency(), nullptr);
|
||||||
|
portaudio::StreamParameters params(inParams, portaudio::DirectionSpecificStreamParameters::null(),
|
||||||
|
this->settings.getAudioSampleRate(), FRAMES_PER_BUFFER, paNoFlag);
|
||||||
|
|
||||||
|
this->audioQueue.setAudioAttributes(this->settings.getAudioSampleRate(),
|
||||||
|
static_cast<unsigned int>(this->inputChannels));
|
||||||
|
|
||||||
|
// Specify the callback used for buffering the recorded data
|
||||||
|
try {
|
||||||
|
this->inputStream = std::make_unique<portaudio::MemFunCallbackStream<PortAudioProducer>>(
|
||||||
|
params, *this, &PortAudioProducer::recordCallback);
|
||||||
|
} catch (const portaudio::PaException&) {
|
||||||
|
g_message("PortAudioProducer: Unable to open stream");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the recording
|
||||||
|
try {
|
||||||
|
this->inputStream->start();
|
||||||
|
} catch (const portaudio::PaException&) {
|
||||||
|
g_message("PortAudioProducer: Unable to start stream");
|
||||||
|
this->inputStream.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PortAudioProducer::recordCallback(const void* inputBuffer, void* /*outputBuffer*/, unsigned long framesPerBuffer,
|
||||||
|
const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags statusFlags) -> int {
|
||||||
|
if (statusFlags) {
|
||||||
|
g_message("PortAudioProducer: statusFlag: %s", std::to_string(statusFlags).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputBuffer != nullptr) {
|
||||||
|
size_t providedFrames = framesPerBuffer * as_unsigned(this->inputChannels);
|
||||||
|
auto begI = static_cast<float const*>(inputBuffer);
|
||||||
|
this->audioQueue.emplace(begI, std::next(begI, as_signed(providedFrames)));
|
||||||
|
}
|
||||||
|
return paContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortAudioProducer::stopRecording() {
|
||||||
|
// Stop the recording
|
||||||
|
if (this->inputStream) {
|
||||||
|
try {
|
||||||
|
if (this->inputStream->isActive()) {
|
||||||
|
this->inputStream->stop();
|
||||||
|
}
|
||||||
|
} catch (const portaudio::PaException&) {
|
||||||
|
g_message("PortAudioProducer: Closing stream failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the consumer at the other side that there will be no more data
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
|
||||||
|
// Allow new recording by removing the old one
|
||||||
|
this->inputStream.reset();
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class to record audio using libportaudio
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <portaudiocpp/PortAudioCpp.hxx> // for MemFunCallbackStream, System
|
||||||
|
|
||||||
|
#include "DeviceInfo.h" // for DeviceInfo
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue;
|
||||||
|
|
||||||
|
|
||||||
|
class PortAudioProducer {
|
||||||
|
public:
|
||||||
|
PortAudioProducer(Settings& settings, AudioQueue<float>& audioQueue): settings(settings), audioQueue(audioQueue){};
|
||||||
|
|
||||||
|
std::vector<DeviceInfo> getInputDevices() const;
|
||||||
|
|
||||||
|
DeviceInfo getSelectedInputDevice() const;
|
||||||
|
|
||||||
|
bool isRecording() const;
|
||||||
|
|
||||||
|
bool startRecording();
|
||||||
|
|
||||||
|
int recordCallback(const void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
|
||||||
|
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags);
|
||||||
|
|
||||||
|
void stopRecording();
|
||||||
|
|
||||||
|
private:
|
||||||
|
portaudio::System& sys{portaudio::System::instance()};
|
||||||
|
Settings& settings;
|
||||||
|
|
||||||
|
AudioQueue<float>& audioQueue;
|
||||||
|
|
||||||
|
std::unique_ptr<portaudio::MemFunCallbackStream<PortAudioProducer>> inputStream;
|
||||||
|
|
||||||
|
int inputChannels = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Helper functions for libsndfile
|
||||||
|
* platform dependent code to open std::filesystem::paths
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <sndfile.h>
|
||||||
|
|
||||||
|
#include "filesystem.h"
|
||||||
|
|
||||||
|
namespace xoj::audio {
|
||||||
|
|
||||||
|
struct SNDFILE_Deleter {
|
||||||
|
void operator()(SNDFILE* file) { sf_close(file); }
|
||||||
|
};
|
||||||
|
|
||||||
|
using SNDFileGuard = std::unique_ptr<SNDFILE, SNDFILE_Deleter>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a file with libsndfile
|
||||||
|
*
|
||||||
|
* @param path std::string const& path to ensure system single byte encoding
|
||||||
|
* @return The resource pointer to the sound file
|
||||||
|
*/
|
||||||
|
inline SNDFILE* sf_open_pp(std::string const& path, int mode, SF_INFO* info) {
|
||||||
|
return sf_open(path.c_str(), mode, info);
|
||||||
|
}
|
||||||
|
#if (defined(ENABLE_SNDFILE_WINDOWS_PROTOTYPES) && ENABLE_SNDFILE_WINDOWS_PROTOTYPES)
|
||||||
|
/**
|
||||||
|
* Open a file with libsndfile
|
||||||
|
*
|
||||||
|
* @param path std::wstring const& path to ensure system double byte encoding
|
||||||
|
* @return The resource pointer to the sound file
|
||||||
|
*/
|
||||||
|
inline SNDFILE* sf_open_pp(std::wstring const& path, int mode, SF_INFO* info) {
|
||||||
|
return sf_wchar_open(path.c_str(), mode, info);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// naming similar to std::make_unique
|
||||||
|
inline auto make_snd_file(fs::path const& file, int mode, SF_INFO* info) {
|
||||||
|
return SNDFileGuard{sf_open_pp(file, mode, info)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace xoj::audio
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#include "VorbisConsumer.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for for_each, min, max
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <deque> // for _Deque_iterator
|
||||||
|
#include <iterator> // for back_insert_iterator, back_in...
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <string> // for string
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <glib.h> // for g_warning
|
||||||
|
#include <sndfile.h> // for SF_INFO, sf_strerror, sf_writ...
|
||||||
|
|
||||||
|
#include "audio/AudioQueue.h" // for AudioQueue
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
|
#include "SNDFileCpp.h" // for make_snd_file, xoj
|
||||||
|
|
||||||
|
using namespace xoj;
|
||||||
|
|
||||||
|
auto VorbisConsumer::start(fs::path const& file) -> bool {
|
||||||
|
auto [sampleRate, channels] = this->audioQueue.getAudioAttributes();
|
||||||
|
|
||||||
|
if (sampleRate == -1) {
|
||||||
|
g_warning("VorbisConsumer: Timing issue - Sample rate requested before known");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SF_INFO sfInfo;
|
||||||
|
sfInfo.channels = channels;
|
||||||
|
sfInfo.format = SF_FORMAT_OGG | SF_FORMAT_VORBIS;
|
||||||
|
sfInfo.samplerate = static_cast<int>(this->settings.getAudioSampleRate());
|
||||||
|
|
||||||
|
auto sfFile = audio::make_snd_file(file.native(), SFM_WRITE, &sfInfo);
|
||||||
|
if (!sfFile) {
|
||||||
|
g_warning("VorbisConsumer: output file \"%s\" could not be opened\ncaused by:%s",
|
||||||
|
char_cast(file.u8string().c_str()), sf_strerror(sfFile.get()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->consumerThread = std::thread([this, sfFile = std::move(sfFile), channels = channels] {
|
||||||
|
auto lock{audioQueue.acquire_lock()};
|
||||||
|
auto buffer_size{size_t(64 * channels)};
|
||||||
|
std::vector<float> buffer;
|
||||||
|
buffer.reserve(buffer_size); // efficiency
|
||||||
|
float audioGain = static_cast<float>(this->settings.getAudioGain());
|
||||||
|
|
||||||
|
while (!(this->stopConsumer || (audioQueue.hasStreamEnded() && audioQueue.empty()))) {
|
||||||
|
audioQueue.waitForProducer(lock);
|
||||||
|
while (audioQueue.size() > buffer_size || (audioQueue.hasStreamEnded() && !audioQueue.empty())) {
|
||||||
|
buffer.resize(0);
|
||||||
|
this->audioQueue.pop(std::back_inserter(buffer), buffer_size);
|
||||||
|
// apply gain
|
||||||
|
if (audioGain != 1.0f) {
|
||||||
|
std::for_each(begin(buffer), end(buffer), [audioGain](auto& val) { val *= audioGain; });
|
||||||
|
}
|
||||||
|
sf_writef_float(sfFile.get(), buffer.data(),
|
||||||
|
std::min<sf_count_t>(sf_count_t(buffer.size()) / channels, 64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorbisConsumer::join() {
|
||||||
|
// Join the consumer thread to wait for completion
|
||||||
|
if (this->consumerThread.joinable()) {
|
||||||
|
this->consumerThread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorbisConsumer::stop() {
|
||||||
|
// Stop consumer
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
|
||||||
|
// Wait for consumer to finish
|
||||||
|
join();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class to save audio data in an opus file
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic> // for atomic
|
||||||
|
#include <thread> // for thread
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue;
|
||||||
|
|
||||||
|
class VorbisConsumer final {
|
||||||
|
public:
|
||||||
|
explicit VorbisConsumer(Settings& settings, AudioQueue<float>& audioQueue):
|
||||||
|
settings(settings), audioQueue(audioQueue) {}
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool start(fs::path const& file);
|
||||||
|
void join();
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Settings& settings;
|
||||||
|
AudioQueue<float>& audioQueue;
|
||||||
|
|
||||||
|
std::thread consumerThread{};
|
||||||
|
std::atomic<bool> stopConsumer{false};
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#include "VorbisProducer.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for fill_n, max
|
||||||
|
#include <cstdio> // for size_t, SEEK_CUR, SEEK_SET
|
||||||
|
#include <iterator> // for begin, end
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <string> // for string
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <glib.h> // for g_warning
|
||||||
|
#include <sndfile.h> // for SF_INFO, sf_seek, sf_count_t, sf_readf...
|
||||||
|
|
||||||
|
#include "audio/AudioQueue.h" // for AudioQueue
|
||||||
|
#include "audio/SNDFileCpp.h" // for make_snd_file, xoj
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
|
using namespace xoj;
|
||||||
|
|
||||||
|
constexpr auto sample_buffer_size = size_t{16384U};
|
||||||
|
|
||||||
|
auto VorbisProducer::start(fs::path const& file, unsigned int timestamp) -> bool {
|
||||||
|
SF_INFO sfInfo{};
|
||||||
|
auto sfFile = audio::make_snd_file(file, SFM_READ, &sfInfo);
|
||||||
|
if (!sfFile) {
|
||||||
|
g_warning("VorbisProducer: input file \"%s\" could not be opened\ncaused by:%s",
|
||||||
|
char_cast(file.u8string().c_str()), sf_strerror(sfFile.get()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf_count_t seekPosition = sfInfo.samplerate / 1000 * sf_count_t(timestamp);
|
||||||
|
|
||||||
|
if (seekPosition < sfInfo.frames) {
|
||||||
|
sf_seek(sfFile.get(), seekPosition, SEEK_SET);
|
||||||
|
} else {
|
||||||
|
g_warning("VorbisProducer: Seeking outside of audio file extent");
|
||||||
|
}
|
||||||
|
|
||||||
|
this->audioQueue.setAudioAttributes(sfInfo.samplerate, static_cast<unsigned int>(sfInfo.channels));
|
||||||
|
|
||||||
|
this->producerThread = std::thread([this, sfInfo, sfFile = std::move(sfFile)] {
|
||||||
|
sf_count_t numFrames{1};
|
||||||
|
size_t const bufferSize{size_t(1024U) * size_t(sfInfo.channels)};
|
||||||
|
std::vector<float> sampleBuffer(bufferSize);
|
||||||
|
auto lock = audioQueue.acquire_lock();
|
||||||
|
|
||||||
|
while (!this->stopProducer && numFrames > 0 && !this->audioQueue.hasStreamEnded()) {
|
||||||
|
sampleBuffer.resize(bufferSize);
|
||||||
|
numFrames = sf_readf_float(sfFile.get(), sampleBuffer.data(), 1024);
|
||||||
|
sampleBuffer.resize(size_t(numFrames * sfInfo.channels));
|
||||||
|
|
||||||
|
while (this->audioQueue.size() >= sample_buffer_size && !this->audioQueue.hasStreamEnded() &&
|
||||||
|
!this->stopProducer) {
|
||||||
|
audioQueue.waitForConsumer(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto tmpSeekSeconds = this->seekSeconds.load(); tmpSeekSeconds != 0) {
|
||||||
|
sf_seek(sfFile.get(), tmpSeekSeconds * sfInfo.samplerate, SEEK_CUR);
|
||||||
|
this->seekSeconds -= tmpSeekSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->audioQueue.emplace(begin(sampleBuffer), end(sampleBuffer));
|
||||||
|
}
|
||||||
|
this->audioQueue.signalEndOfStream();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorbisProducer::abort() {
|
||||||
|
this->stopProducer = true;
|
||||||
|
// Wait for producer to finish
|
||||||
|
stop();
|
||||||
|
this->stopProducer = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorbisProducer::stop() {
|
||||||
|
// Wait for producer to finish
|
||||||
|
if (this->producerThread.joinable()) {
|
||||||
|
this->producerThread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void VorbisProducer::seek(int seconds) { this->seekSeconds = seconds; }
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Class to read audio data from an mp3 file
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic> // for atomic
|
||||||
|
#include <thread> // for thread
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class AudioQueue;
|
||||||
|
|
||||||
|
class VorbisProducer final {
|
||||||
|
public:
|
||||||
|
explicit VorbisProducer(AudioQueue<float>& audioQueue): audioQueue(audioQueue) {}
|
||||||
|
|
||||||
|
bool start(fs::path const& file, unsigned int timestamp);
|
||||||
|
void abort();
|
||||||
|
void stop();
|
||||||
|
void seek(int seconds);
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioQueue<float>& audioQueue;
|
||||||
|
std::thread producerThread{};
|
||||||
|
|
||||||
|
std::atomic<bool> stopProducer{false};
|
||||||
|
std::atomic<int> seekSeconds{0};
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "AudioController.h"
|
||||||
|
|
||||||
|
#include <array> // for array
|
||||||
|
#include <cstdio> // for snprintf
|
||||||
|
#include <ctime> // for tm, localtime, time
|
||||||
|
#include <string> // for string, allocator
|
||||||
|
|
||||||
|
#include <glib.h> // for g_get_monotonic_time
|
||||||
|
|
||||||
|
#include "audio/AudioPlayer.h" // for AudioPlayer
|
||||||
|
#include "audio/AudioRecorder.h" // for AudioRecorder
|
||||||
|
#include "audio/DeviceInfo.h" // for DeviceInfo
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/actions/ActionDatabase.h" // for ActionDatabase
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/toolbarMenubar/ToolMenuHandler.h" // for ToolMenuHandler
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
#include "util/safe_casts.h" // for as_signed
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::vector;
|
||||||
|
|
||||||
|
AudioController::AudioController(Settings* settings, Control* control):
|
||||||
|
settings(*settings),
|
||||||
|
control(*control),
|
||||||
|
audioRecorder(std::make_unique<AudioRecorder>(*settings)),
|
||||||
|
audioPlayer(std::make_unique<AudioPlayer>(*control, *settings)) {}
|
||||||
|
|
||||||
|
AudioController::~AudioController() = default;
|
||||||
|
|
||||||
|
|
||||||
|
auto AudioController::startRecording() -> bool {
|
||||||
|
if (!this->isRecording()) {
|
||||||
|
if (getAudioFolder().empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->timestamp = static_cast<size_t>(g_get_monotonic_time() / 1000);
|
||||||
|
|
||||||
|
std::array<char, 50> buffer{};
|
||||||
|
time_t secs = time(nullptr);
|
||||||
|
tm* t = localtime(&secs);
|
||||||
|
// This prints the date and time in ISO format.
|
||||||
|
snprintf(buffer.data(), buffer.size(), "%04d-%02d-%02d_%02d-%02d-%02d", t->tm_year + 1900, t->tm_mon + 1,
|
||||||
|
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
|
||||||
|
string data(buffer.data());
|
||||||
|
data += ".ogg";
|
||||||
|
|
||||||
|
audioFilename = data;
|
||||||
|
|
||||||
|
g_message("Start recording");
|
||||||
|
|
||||||
|
bool isRecording = this->audioRecorder->start(getAudioFolder() / data);
|
||||||
|
|
||||||
|
if (!isRecording) {
|
||||||
|
audioFilename = "";
|
||||||
|
this->timestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRecording;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioController::stopRecording() -> bool {
|
||||||
|
if (this->audioRecorder->isRecording()) {
|
||||||
|
audioFilename = "";
|
||||||
|
this->timestamp = 0;
|
||||||
|
|
||||||
|
g_message("Stop recording");
|
||||||
|
|
||||||
|
this->audioRecorder->stop();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioController::isRecording() -> bool { return this->audioRecorder->isRecording(); }
|
||||||
|
|
||||||
|
auto AudioController::isPlaying() -> bool { return this->audioPlayer->isPlaying(); }
|
||||||
|
|
||||||
|
auto AudioController::startPlayback(fs::path const& file, unsigned int timestamp) -> bool {
|
||||||
|
this->audioPlayer->stop();
|
||||||
|
bool status = this->audioPlayer->start(file, timestamp);
|
||||||
|
if (status) {
|
||||||
|
auto* actionDB = this->control.getActionDatabase();
|
||||||
|
actionDB->enableAction(Action::AUDIO_PAUSE_PLAYBACK, true);
|
||||||
|
actionDB->enableAction(Action::AUDIO_STOP_PLAYBACK, true);
|
||||||
|
actionDB->enableAction(Action::AUDIO_SEEK_FORWARDS, true);
|
||||||
|
actionDB->enableAction(Action::AUDIO_SEEK_BACKWARDS, true);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioController::pausePlayback() {
|
||||||
|
this->control.getActionDatabase()->setActionState(Action::AUDIO_PAUSE_PLAYBACK, true);
|
||||||
|
|
||||||
|
this->audioPlayer->pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioController::seekForwards() { this->audioPlayer->seek(as_signed(this->settings.getDefaultSeekTime())); }
|
||||||
|
|
||||||
|
void AudioController::seekBackwards() { this->audioPlayer->seek(-1 * as_signed(this->settings.getDefaultSeekTime())); }
|
||||||
|
|
||||||
|
void AudioController::continuePlayback() {
|
||||||
|
this->control.getActionDatabase()->setActionState(Action::AUDIO_PAUSE_PLAYBACK, false);
|
||||||
|
|
||||||
|
this->audioPlayer->play();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioController::stopPlayback() { this->audioPlayer->stop(); }
|
||||||
|
|
||||||
|
auto AudioController::getAudioFilename() const -> fs::path const& { return this->audioFilename; }
|
||||||
|
|
||||||
|
auto AudioController::getAudioFolder() const -> fs::path {
|
||||||
|
auto const& af = this->settings.getAudioFolder();
|
||||||
|
|
||||||
|
if (!fs::is_directory(af)) {
|
||||||
|
string msg = _("Audio folder not set or invalid! Recording won't work!\nPlease set the "
|
||||||
|
"recording folder under \"Preferences > Audio recording\"");
|
||||||
|
XojMsgBox::showErrorToUser(this->control.getGtkWindow(), msg);
|
||||||
|
return fs::path{};
|
||||||
|
}
|
||||||
|
return af;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AudioController::getStartTime() const -> size_t { return this->timestamp; }
|
||||||
|
|
||||||
|
auto AudioController::getOutputDevices() const -> vector<DeviceInfo> { return this->audioPlayer->getOutputDevices(); }
|
||||||
|
|
||||||
|
auto AudioController::getInputDevices() const -> vector<DeviceInfo> { return this->audioRecorder->getInputDevices(); }
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Audio Recording / Playing controller
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "config-features.h" // for ENABLE_AUDIO
|
||||||
|
#ifdef ENABLE_AUDIO
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <memory> // for make_unique, unique_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <portaudiocpp/PortAudioCpp.hxx> // for AutoSystem
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class AudioPlayer;
|
||||||
|
class AudioRecorder;
|
||||||
|
class Control;
|
||||||
|
class DeviceInfo;
|
||||||
|
class Settings;
|
||||||
|
|
||||||
|
class AudioController final {
|
||||||
|
public:
|
||||||
|
// Todo convert Pointers to reference (changes to control.cpp are necessary)
|
||||||
|
AudioController(Settings* settings, Control* control);
|
||||||
|
~AudioController();
|
||||||
|
|
||||||
|
bool startRecording();
|
||||||
|
bool stopRecording();
|
||||||
|
bool isRecording();
|
||||||
|
|
||||||
|
bool isPlaying();
|
||||||
|
bool startPlayback(fs::path const& file, unsigned int timestamp);
|
||||||
|
void pausePlayback();
|
||||||
|
void continuePlayback();
|
||||||
|
void stopPlayback();
|
||||||
|
void seekForwards();
|
||||||
|
void seekBackwards();
|
||||||
|
|
||||||
|
fs::path const& getAudioFilename() const;
|
||||||
|
fs::path getAudioFolder() const;
|
||||||
|
size_t getStartTime() const;
|
||||||
|
std::vector<DeviceInfo> getOutputDevices() const;
|
||||||
|
std::vector<DeviceInfo> getInputDevices() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Settings& settings;
|
||||||
|
Control& control;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RAII initializer don't move below the portaudio::System::instance() calls in
|
||||||
|
* AudioRecorder and AudioPlayer
|
||||||
|
* */
|
||||||
|
portaudio::AutoSystem autoSys;
|
||||||
|
std::unique_ptr<AudioRecorder> audioRecorder;
|
||||||
|
std::unique_ptr<AudioPlayer> audioPlayer;
|
||||||
|
|
||||||
|
fs::path audioFilename;
|
||||||
|
size_t timestamp = 0;
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
class AudioController final {};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
#include "ClipboardHandler.h"
|
||||||
|
|
||||||
|
#include <set> // for multiset, operator!=
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <cairo-svg.h> // for cairo_svg_surface_c...
|
||||||
|
#include <cairo.h> // for cairo_create, cairo...
|
||||||
|
#include <gdk/gdkpixbuf.h> // for gdk_pixbuf_get_from_surface
|
||||||
|
#include <glib-object.h> // for g_object_unref, g_s...
|
||||||
|
|
||||||
|
#include "control/tools/EditSelection.h" // for EditSelection
|
||||||
|
#include "model/Element.h" // for Element, ELEMENT_TEXT
|
||||||
|
#include "model/Text.h" // for Text
|
||||||
|
#include "util/Util.h" // for DPI_NORMALIZATION_F...
|
||||||
|
#include "util/gtk4_helper.h" // for gtk_widget_get_clipboard
|
||||||
|
#include "util/safe_casts.h" // for as_unsigned
|
||||||
|
#include "util/serializing/BinObjectEncoding.h" // for BinObjectEncoding
|
||||||
|
#include "util/serializing/ObjectInputStream.h" // for ObjectInputStream
|
||||||
|
#include "util/serializing/ObjectOutputStream.h" // for ObjectOutputStream
|
||||||
|
#include "view/ElementContainerView.h" // for ElementContainerView
|
||||||
|
#include "view/View.h" // for Context
|
||||||
|
|
||||||
|
#include "config.h" // for PROJECT_STRING
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
|
||||||
|
ClipboardListener::~ClipboardListener() = default;
|
||||||
|
|
||||||
|
ClipboardHandler::ClipboardHandler(ClipboardListener* listener, GtkWidget* widget) {
|
||||||
|
this->listener = listener;
|
||||||
|
this->clipboard = gtk_widget_get_clipboard(widget);
|
||||||
|
|
||||||
|
this->handlerId = g_signal_connect(this->clipboard, "owner-change", G_CALLBACK(&ownerChangedCallback), this);
|
||||||
|
|
||||||
|
this->listener->clipboardCutCopyEnabled(false);
|
||||||
|
|
||||||
|
gtk_clipboard_request_contents(clipboard, gdk_atom_intern_static_string("TARGETS"),
|
||||||
|
reinterpret_cast<GtkClipboardReceivedFunc>(receivedClipboardContents), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardHandler::~ClipboardHandler() { g_signal_handler_disconnect(this->clipboard, this->handlerId); }
|
||||||
|
|
||||||
|
static GdkAtom atomXournal = gdk_atom_intern_static_string("application/xournal");
|
||||||
|
|
||||||
|
auto ClipboardHandler::paste() -> bool {
|
||||||
|
/* Request targets again, since the owner-change signal is not emitted on MacOS and under X11 with no XFIXES
|
||||||
|
* extension. See https://docs.gtk.org/gdk3/struct.EventOwnerChange.html and
|
||||||
|
* https://gitlab.gnome.org/GNOME/gtk/-/issues/1757 */
|
||||||
|
gtk_clipboard_request_contents(clipboard, gdk_atom_intern_static_string("TARGETS"),
|
||||||
|
reinterpret_cast<GtkClipboardReceivedFunc>(receivedClipboardContents), this);
|
||||||
|
|
||||||
|
if (this->containsXournal) {
|
||||||
|
gtk_clipboard_request_contents(this->clipboard, atomXournal,
|
||||||
|
reinterpret_cast<GtkClipboardReceivedFunc>(pasteClipboardContents), this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this->containsText) {
|
||||||
|
gtk_clipboard_request_text(this->clipboard, reinterpret_cast<GtkClipboardTextReceivedFunc>(pasteClipboardText),
|
||||||
|
this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this->containsImage) {
|
||||||
|
gtk_clipboard_request_image(this->clipboard,
|
||||||
|
reinterpret_cast<GtkClipboardImageReceivedFunc>(pasteClipboardImage), this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ClipboardHandler::cut() -> bool {
|
||||||
|
bool result = this->copy();
|
||||||
|
this->listener->deleteSelection();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ElementCompareFunc(const Element* a, const Element* b) -> bool {
|
||||||
|
if (a->getY() == b->getY()) {
|
||||||
|
return (a->getX() - b->getX()) < 0;
|
||||||
|
}
|
||||||
|
return (a->getY() - b->getY()) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GdkAtom atomSvg1 = gdk_atom_intern_static_string("image/svg");
|
||||||
|
static GdkAtom atomSvg2 = gdk_atom_intern_static_string("image/svg+xml");
|
||||||
|
|
||||||
|
// The contents of the clipboard
|
||||||
|
class ClipboardContents {
|
||||||
|
public:
|
||||||
|
ClipboardContents(string text, GdkPixbuf* image, string svg, GString* str) {
|
||||||
|
this->text = std::move(text);
|
||||||
|
this->image = image;
|
||||||
|
this->svg = std::move(svg);
|
||||||
|
this->str = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
~ClipboardContents() {
|
||||||
|
g_object_unref(this->image);
|
||||||
|
g_string_free(this->str, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void getFunction(GtkClipboard* clipboard, GtkSelectionData* selection, guint info,
|
||||||
|
ClipboardContents* contents) {
|
||||||
|
GdkAtom target = gtk_selection_data_get_target(selection);
|
||||||
|
|
||||||
|
if (target == gdk_atom_intern_static_string("UTF8_STRING")) {
|
||||||
|
gtk_selection_data_set_text(selection, contents->text.c_str(), -1);
|
||||||
|
} else if (target == gdk_atom_intern_static_string("image/png") ||
|
||||||
|
target == gdk_atom_intern_static_string("image/jpeg") ||
|
||||||
|
target == gdk_atom_intern_static_string("image/gif")) {
|
||||||
|
gtk_selection_data_set_pixbuf(selection, contents->image);
|
||||||
|
} else if (atomSvg1 == target || atomSvg2 == target) {
|
||||||
|
gtk_selection_data_set(selection, target, 8, reinterpret_cast<guchar const*>(contents->svg.c_str()),
|
||||||
|
static_cast<gint>(contents->svg.length()));
|
||||||
|
} else if (atomXournal == target) {
|
||||||
|
gtk_selection_data_set(selection, target, 8, reinterpret_cast<guchar*>(contents->str->str),
|
||||||
|
static_cast<gint>(contents->str->len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearFunction(GtkClipboard* clipboard, ClipboardContents* contents) { delete contents; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
string text;
|
||||||
|
GdkPixbuf* image;
|
||||||
|
string svg;
|
||||||
|
GString* str;
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto svgWriteFunction(GString* string, const unsigned char* data, unsigned int length) -> cairo_status_t {
|
||||||
|
g_string_append_len(string, reinterpret_cast<const gchar*>(data), length);
|
||||||
|
return CAIRO_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ClipboardHandler::copy() -> bool {
|
||||||
|
if (!this->selection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// prepare xournal contents
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
ObjectOutputStream out(new BinObjectEncoding());
|
||||||
|
|
||||||
|
out.writeString(PROJECT_STRING);
|
||||||
|
|
||||||
|
this->selection->serialize(out);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// prepare text contents
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
std::multiset<const Text*, decltype(&ElementCompareFunc)> textElements(ElementCompareFunc);
|
||||||
|
|
||||||
|
for (const Element* e: this->selection->getElementsView()) {
|
||||||
|
if (e->getType() == ELEMENT_TEXT) {
|
||||||
|
textElements.insert(dynamic_cast<const Text*>(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string text{};
|
||||||
|
for (const Text* t: textElements) {
|
||||||
|
if (!text.empty()) {
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
|
text += t->getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// prepare image contents: PNG
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
double dpiFactor = 1.0 / Util::DPI_NORMALIZATION_FACTOR * 300.0;
|
||||||
|
|
||||||
|
int width = static_cast<int>(selection->getWidth() * dpiFactor);
|
||||||
|
int height = static_cast<int>(selection->getHeight() * dpiFactor);
|
||||||
|
cairo_surface_t* surfacePng = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
|
||||||
|
cairo_t* crPng = cairo_create(surfacePng);
|
||||||
|
cairo_scale(crPng, dpiFactor, dpiFactor);
|
||||||
|
|
||||||
|
cairo_translate(crPng, -selection->getOriginalXOnView(), -selection->getOriginalYOnView());
|
||||||
|
|
||||||
|
xoj::view::ElementContainerView view(this->selection);
|
||||||
|
view.draw(xoj::view::Context::createDefault(crPng));
|
||||||
|
|
||||||
|
cairo_destroy(crPng);
|
||||||
|
|
||||||
|
GdkPixbuf* image = gdk_pixbuf_get_from_surface(surfacePng, 0, 0, width, height);
|
||||||
|
|
||||||
|
cairo_surface_destroy(surfacePng);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// prepare image contents: SVG
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
GString* svgString = g_string_sized_new(1048576); // 1MB
|
||||||
|
|
||||||
|
cairo_surface_t* surfaceSVG =
|
||||||
|
cairo_svg_surface_create_for_stream(reinterpret_cast<cairo_write_func_t>(svgWriteFunction), svgString,
|
||||||
|
selection->getWidth(), selection->getHeight());
|
||||||
|
cairo_t* crSVG = cairo_create(surfaceSVG);
|
||||||
|
|
||||||
|
cairo_translate(crSVG, -selection->getOriginalXOnView(), -selection->getOriginalYOnView());
|
||||||
|
view.draw(xoj::view::Context::createDefault(crSVG));
|
||||||
|
|
||||||
|
cairo_surface_destroy(surfaceSVG);
|
||||||
|
cairo_destroy(crSVG);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// copy to clipboard
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
GtkTargetList* list = gtk_target_list_new(nullptr, 0);
|
||||||
|
GtkTargetEntry* targets = nullptr;
|
||||||
|
int n_targets = 0;
|
||||||
|
|
||||||
|
// if we have text elements...
|
||||||
|
if (!text.empty()) {
|
||||||
|
gtk_target_list_add_text_targets(list, 0);
|
||||||
|
}
|
||||||
|
// we always copy an image to clipboard
|
||||||
|
gtk_target_list_add_image_targets(list, 0, true);
|
||||||
|
gtk_target_list_add(list, atomSvg1, 0, 0);
|
||||||
|
gtk_target_list_add(list, atomSvg2, 0, 0);
|
||||||
|
gtk_target_list_add(list, atomXournal, 0, 0);
|
||||||
|
|
||||||
|
targets = gtk_target_table_new_from_list(list, &n_targets);
|
||||||
|
|
||||||
|
auto* contents = new ClipboardContents(text, image, svgString->str, out.stealData());
|
||||||
|
|
||||||
|
gtk_clipboard_set_with_data(this->clipboard, targets, static_cast<guint>(n_targets),
|
||||||
|
reinterpret_cast<GtkClipboardGetFunc>(ClipboardContents::getFunction),
|
||||||
|
reinterpret_cast<GtkClipboardClearFunc>(ClipboardContents::clearFunction), contents);
|
||||||
|
gtk_clipboard_set_can_store(this->clipboard, nullptr, 0);
|
||||||
|
|
||||||
|
gtk_target_table_free(targets, n_targets);
|
||||||
|
gtk_target_list_unref(list);
|
||||||
|
|
||||||
|
g_string_free(svgString, true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::setSelection(EditSelection* selection) {
|
||||||
|
this->selection = selection;
|
||||||
|
|
||||||
|
this->listener->clipboardCutCopyEnabled(selection != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::setCopyCutEnabled(bool enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
listener->clipboardCutCopyEnabled(true);
|
||||||
|
} else if (!selection) {
|
||||||
|
listener->clipboardCutCopyEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::ownerChangedCallback(GtkClipboard* clip, GdkEvent* event, ClipboardHandler* handler) {
|
||||||
|
if (gdk_event_get_event_type(event) == GDK_OWNER_CHANGE) {
|
||||||
|
handler->clipboardUpdated(event->owner_change.selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::clipboardUpdated(GdkAtom atom) {
|
||||||
|
gtk_clipboard_request_contents(clipboard, gdk_atom_intern_static_string("TARGETS"),
|
||||||
|
reinterpret_cast<GtkClipboardReceivedFunc>(receivedClipboardContents), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::pasteClipboardImage(GtkClipboard* clipboard, GdkPixbuf* pixbuf, ClipboardHandler* handler) {
|
||||||
|
if (pixbuf) {
|
||||||
|
handler->listener->clipboardPasteImage(pixbuf);
|
||||||
|
} else {
|
||||||
|
g_warning("Trying to paste image, but pixbuf is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::pasteClipboardContents(GtkClipboard* clipboard, GtkSelectionData* selectionData,
|
||||||
|
ClipboardHandler* handler) {
|
||||||
|
ObjectInputStream in;
|
||||||
|
|
||||||
|
if (in.read(reinterpret_cast<const char*>(gtk_selection_data_get_data(selectionData)),
|
||||||
|
as_unsigned(gtk_selection_data_get_length(selectionData)))) {
|
||||||
|
handler->listener->clipboardPasteXournal(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::pasteClipboardText(GtkClipboard* clipboard, const gchar* text, ClipboardHandler* handler) {
|
||||||
|
if (text) {
|
||||||
|
handler->listener->clipboardPasteText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto gtk_selection_data_targets_include_xournal(GtkSelectionData* selection_data) -> gboolean {
|
||||||
|
GdkAtom* targets = nullptr;
|
||||||
|
gint n_targets = 0;
|
||||||
|
gboolean result = false;
|
||||||
|
|
||||||
|
if (gtk_selection_data_get_targets(selection_data, &targets, &n_targets)) {
|
||||||
|
for (int i = 0; i < n_targets; i++) {
|
||||||
|
if (targets[i] == atomXournal) {
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_free(targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClipboardHandler::receivedClipboardContents(GtkClipboard* clipboard, GtkSelectionData* selectionData,
|
||||||
|
ClipboardHandler* handler) {
|
||||||
|
handler->containsText = gtk_selection_data_targets_include_text(selectionData);
|
||||||
|
handler->containsXournal = gtk_selection_data_targets_include_xournal(selectionData);
|
||||||
|
handler->containsImage = gtk_selection_data_targets_include_image(selectionData, false);
|
||||||
|
|
||||||
|
handler->listener->clipboardPasteEnabled(handler->containsText || handler->containsXournal ||
|
||||||
|
handler->containsImage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* The main Control
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <limits> // for numeric_limits
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <gdk-pixbuf/gdk-pixbuf.h> // for GdkPixbuf
|
||||||
|
#include <gdk/gdk.h> // for GdkAtom, GdkEvent
|
||||||
|
#include <glib.h> // for gchar, gulong
|
||||||
|
#include <gtk/gtk.h> // for GtkClipboard, GtkSelectionData
|
||||||
|
|
||||||
|
class ObjectInputStream;
|
||||||
|
class EditSelection;
|
||||||
|
|
||||||
|
class ClipboardListener {
|
||||||
|
public:
|
||||||
|
virtual void clipboardCutCopyEnabled(bool enabled) = 0;
|
||||||
|
virtual void clipboardPasteEnabled(bool enabled) = 0;
|
||||||
|
virtual void clipboardPasteText(std::string text) = 0;
|
||||||
|
virtual void clipboardPasteImage(GdkPixbuf* img) = 0;
|
||||||
|
virtual void clipboardPasteXournal(ObjectInputStream& in) = 0;
|
||||||
|
virtual void deleteSelection() = 0;
|
||||||
|
|
||||||
|
virtual ~ClipboardListener();
|
||||||
|
};
|
||||||
|
|
||||||
|
class ClipboardHandler {
|
||||||
|
public:
|
||||||
|
ClipboardHandler(ClipboardListener* listener, GtkWidget* widget);
|
||||||
|
virtual ~ClipboardHandler();
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool paste();
|
||||||
|
bool cut();
|
||||||
|
bool copy();
|
||||||
|
|
||||||
|
void setSelection(EditSelection* selection);
|
||||||
|
|
||||||
|
void setCopyCutEnabled(bool enabled);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void ownerChangedCallback(GtkClipboard* clip, GdkEvent* event, ClipboardHandler* handler);
|
||||||
|
void clipboardUpdated(GdkAtom atom);
|
||||||
|
static void receivedClipboardContents(GtkClipboard* clipboard, GtkSelectionData* selectionData,
|
||||||
|
ClipboardHandler* handler);
|
||||||
|
|
||||||
|
static void pasteClipboardContents(GtkClipboard* clipboard, GtkSelectionData* selectionData,
|
||||||
|
ClipboardHandler* handler);
|
||||||
|
static void pasteClipboardImage(GtkClipboard* clipboard, GdkPixbuf* pixbuf, ClipboardHandler* handler);
|
||||||
|
|
||||||
|
static void pasteClipboardText(GtkClipboard* clipboard, const gchar* text, ClipboardHandler* handler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ClipboardListener* listener = nullptr;
|
||||||
|
GtkClipboard* clipboard = nullptr;
|
||||||
|
gulong handlerId = std::numeric_limits<gulong>::max();
|
||||||
|
|
||||||
|
EditSelection* selection = nullptr;
|
||||||
|
|
||||||
|
bool containsText = false;
|
||||||
|
bool containsXournal = false;
|
||||||
|
bool containsImage = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "CompassController.h"
|
||||||
|
|
||||||
|
#include "control/Control.h"
|
||||||
|
#include "control/layer/LayerController.h"
|
||||||
|
#include "gui/XournalView.h"
|
||||||
|
#include "model/Compass.h"
|
||||||
|
#include "model/GeometryTool.h"
|
||||||
|
#include "model/Stroke.h"
|
||||||
|
#include "model/XojPage.h"
|
||||||
|
|
||||||
|
CompassController::CompassController(XojPageView* view, Compass* compass): GeometryToolController(view, compass) {}
|
||||||
|
|
||||||
|
CompassController::~CompassController() = default;
|
||||||
|
|
||||||
|
auto CompassController::getType() const -> GeometryToolType { return GeometryToolType::COMPASS; }
|
||||||
|
|
||||||
|
auto CompassController::posRelToSide(double x, double y) const -> xoj::util::Point<double> {
|
||||||
|
cairo_matrix_t inv = geometryTool->getMatrix();
|
||||||
|
cairo_matrix_invert(&inv);
|
||||||
|
cairo_matrix_transform_point(&inv, &x, &y);
|
||||||
|
return xoj::util::Point<double>(x, -y);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CompassController::isInsideGeometryTool(double x, double y, double border) const -> bool {
|
||||||
|
const xoj::util::Point<double> p = posRelToSide(x, y);
|
||||||
|
return std::hypot(p.x, p.y) <= geometryTool->getHeight() + border;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CompassController::getPointForAngle(double a) const -> xoj::util::Point<double> {
|
||||||
|
cairo_matrix_t matrix = geometryTool->getMatrix();
|
||||||
|
double x = geometryTool->getHeight() * std::cos(a);
|
||||||
|
double y = geometryTool->getHeight() * std::sin(a);
|
||||||
|
cairo_matrix_transform_point(&matrix, &x, &y);
|
||||||
|
|
||||||
|
return xoj::util::Point<double>(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CompassController::getPointForRadius(double r) const -> xoj::util::Point<double> {
|
||||||
|
cairo_matrix_t matrix = geometryTool->getMatrix();
|
||||||
|
double x = r;
|
||||||
|
double y = 0.;
|
||||||
|
cairo_matrix_transform_point(&matrix, &x, &y);
|
||||||
|
|
||||||
|
return xoj::util::Point<double>(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompassController::createOutlineStroke(double a) {
|
||||||
|
if (!std::isnan(a)) {
|
||||||
|
angleMax = a;
|
||||||
|
angleMin = a;
|
||||||
|
|
||||||
|
const xoj::util::Point<double> p = this->getPointForAngle(a);
|
||||||
|
initializeStroke();
|
||||||
|
stroke->addPoint(Point(p.x, p.y));
|
||||||
|
stroke->addPoint(Point(p.x, p.y)); // doubled point
|
||||||
|
geometryTool->notify();
|
||||||
|
} else {
|
||||||
|
g_warning("No valid stroke from compass!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompassController::createRadialStroke(double x) {
|
||||||
|
if (!std::isnan(x)) {
|
||||||
|
radiusMax = x;
|
||||||
|
radiusMin = x;
|
||||||
|
|
||||||
|
const xoj::util::Point<double> p = posRelToSide(x, 0.);
|
||||||
|
initializeStroke();
|
||||||
|
stroke->addPoint(Point(p.x, p.y));
|
||||||
|
stroke->addPoint(Point(p.x, p.y)); // doubled point
|
||||||
|
geometryTool->notify();
|
||||||
|
} else {
|
||||||
|
g_warning("No valid radius from compass!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompassController::updateOutlineStroke(double x) {
|
||||||
|
angleMax = std::max(this->angleMax, x);
|
||||||
|
angleMin = std::min(this->angleMin, x);
|
||||||
|
stroke->deletePointsFrom(0);
|
||||||
|
const auto h = view->getXournal()->getControl()->getToolHandler();
|
||||||
|
const bool filled = (h->getFill() != -1);
|
||||||
|
const xoj::util::Point<double>& c = this->getGeometryTool()->getOrigin();
|
||||||
|
|
||||||
|
if (filled && angleMax < angleMin + 2 * M_PI) {
|
||||||
|
stroke->addPoint(Point(c.x, c.y));
|
||||||
|
}
|
||||||
|
for (auto i = 0; i <= 100; i++) {
|
||||||
|
const xoj::util::Point<double> p =
|
||||||
|
getPointForAngle(angleMin + static_cast<double>(i) / 100.0 * std::min(angleMax - angleMin, 2 * M_PI));
|
||||||
|
stroke->addPoint(Point(p.x, p.y));
|
||||||
|
}
|
||||||
|
if (filled && angleMax < angleMin + 2 * M_PI) {
|
||||||
|
stroke->addPoint(Point(c.x, c.y));
|
||||||
|
}
|
||||||
|
geometryTool->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompassController::updateRadialStroke(double x) {
|
||||||
|
radiusMax = std::max(this->radiusMax, x);
|
||||||
|
radiusMin = std::min(this->radiusMin, x);
|
||||||
|
stroke->deletePointsFrom(0);
|
||||||
|
const xoj::util::Point<double> p1 = getPointForRadius(radiusMin);
|
||||||
|
const xoj::util::Point<double> p2 = getPointForRadius(radiusMax);
|
||||||
|
|
||||||
|
stroke->addPoint(Point(p1.x, p1.y));
|
||||||
|
stroke->addPoint(Point(p2.x, p2.y));
|
||||||
|
geometryTool->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompassController::finalizeOutlineStroke() {
|
||||||
|
angleMax = std::numeric_limits<double>::lowest();
|
||||||
|
angleMin = std::numeric_limits<double>::max();
|
||||||
|
addStrokeToLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompassController::finalizeRadialStroke() {
|
||||||
|
radiusMax = std::numeric_limits<double>::lowest();
|
||||||
|
radiusMin = std::numeric_limits<double>::max();
|
||||||
|
addStrokeToLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CompassController::existsOutlineStroke() -> bool { return angleMax != std::numeric_limits<double>::lowest(); }
|
||||||
|
|
||||||
|
auto CompassController::existsRadialStroke() -> bool { return radiusMax != std::numeric_limits<double>::lowest(); }
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A compass controller
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include "gui/PageView.h"
|
||||||
|
#include "model/GeometryTool.h"
|
||||||
|
#include "util/Point.h"
|
||||||
|
|
||||||
|
#include "GeometryToolController.h"
|
||||||
|
|
||||||
|
class Compass;
|
||||||
|
class Stroke;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A class that controls a compass
|
||||||
|
|
||||||
|
* The compass can be moved, rotated and scaled
|
||||||
|
* There are methods for translating coordinates
|
||||||
|
* and methods to deal with the temporary stroke
|
||||||
|
* that is displayed near the the outline of the
|
||||||
|
* compass or the marked radius.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CompassController: public GeometryToolController {
|
||||||
|
public:
|
||||||
|
CompassController(XojPageView* view, Compass* compass);
|
||||||
|
~CompassController() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GeometryToolType getType() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief returns the position of a point relative to a coordinate system, in which the
|
||||||
|
* positive x-axis coincides with the distinguished compass axis and the origin lies
|
||||||
|
* in the center of the compass
|
||||||
|
* @param x the x-coordinate of the point (in document coordinates)
|
||||||
|
* @param y the y-coordinate of the point (in document coordinates)
|
||||||
|
*/
|
||||||
|
xoj::util::Point<double> posRelToSide(double x, double y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief checks whether a point with given coordinates lies in the geometry tool with an additional
|
||||||
|
* border enlarging (or shrinked) it
|
||||||
|
* @param x the x-coordinate of the given point (in document coordinates)
|
||||||
|
* @param y the y-coordinate of the given point (in document coordinates)
|
||||||
|
* @param border the size of the border (if negative, the geometry tool is shrinked via the border)
|
||||||
|
*/
|
||||||
|
bool isInsideGeometryTool(double x, double y, double border = 0.0) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the point (in document coordinates) for a given angle on the outline of the compass
|
||||||
|
* @param a the angle with respect to the distinguished compass axis of the point
|
||||||
|
*/
|
||||||
|
xoj::util::Point<double> getPointForAngle(double a) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the point (in document coordinates) for a given radius on the marked radius of the compass
|
||||||
|
* @param r the x-coordinate with respect to a coordinate system, in which the positive x-axis
|
||||||
|
* coincides with the marked radius and the origin lies in the center of the compass
|
||||||
|
*/
|
||||||
|
xoj::util::Point<double> getPointForRadius(double r) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief creates a stroke starting at the given angle of the outline of the compass
|
||||||
|
* @param a the angle of the point on the outline of the compass (when unrotated and untranslated)
|
||||||
|
*/
|
||||||
|
void createOutlineStroke(double a);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief updates the stroke near the outline of the compass
|
||||||
|
* @param a the angle of the point on the outline of the compass (when unrotated and untranslated)
|
||||||
|
* updating the stroke
|
||||||
|
*/
|
||||||
|
void updateOutlineStroke(double a);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief finishes the stroke near the outline of the compass
|
||||||
|
*/
|
||||||
|
void finalizeOutlineStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief finishes the stroke aligned to the marked radius of the compass
|
||||||
|
*/
|
||||||
|
void finalizeRadialStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief creates a stroke starting at the given point of the marked radius of the compass
|
||||||
|
* @param x the x-coordinate with respect to the marked radius
|
||||||
|
*/
|
||||||
|
void createRadialStroke(double x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief updates the stroke near the marked radius of the compass
|
||||||
|
* @param x the x-coordinate with respect to the marked radius
|
||||||
|
*/
|
||||||
|
void updateRadialStroke(double x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks whether a stroke near the outline already exists
|
||||||
|
*/
|
||||||
|
bool existsOutlineStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks whether a radius already exists
|
||||||
|
*/
|
||||||
|
bool existsRadialStroke();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief when a stroke near the radius with the measuring marks
|
||||||
|
* is drawn, the minimal and maximal radii are saved in
|
||||||
|
* the variables radiusMax and radiusMin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
double radiusMax = std::numeric_limits<double>::lowest();
|
||||||
|
double radiusMin = std::numeric_limits<double>::max();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief when a stroke near the outline of the compass is drawn, the minimal and maximal
|
||||||
|
* angles of the point to be drawn (with respect to an unrotated, and untranslated coordinate system)
|
||||||
|
* are saved in the variables angleMin and angleMax
|
||||||
|
*/
|
||||||
|
double angleMax = std::numeric_limits<double>::lowest();
|
||||||
|
double angleMin = std::numeric_limits<double>::max();
|
||||||
|
};
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,569 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* The main Control
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <optional> // for optional
|
||||||
|
#include <string> // for string, allocator
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <gdk-pixbuf/gdk-pixbuf.h> // for GdkPixbuf
|
||||||
|
#include <gio/gio.h> // for GApplication
|
||||||
|
#include <glib.h> // for guint
|
||||||
|
#include <gtk/gtk.h> // for GtkLabel
|
||||||
|
|
||||||
|
#include "control/ToolEnums.h" // for ToolSize, ToolType
|
||||||
|
#include "control/jobs/ProgressListener.h" // for ProgressListener
|
||||||
|
#include "control/settings/ViewModes.h" // for ViewModeId
|
||||||
|
#include "control/tools/EditSelection.h" // for OrderChange
|
||||||
|
#include "enums/Action.enum.h" // for Action
|
||||||
|
#include "gui/toolbarMenubar/model/ColorPalette.h" // for ColorPalette
|
||||||
|
#include "model/DocumentHandler.h" // for DocumentHandler
|
||||||
|
#include "model/DocumentListener.h" // for DocumentListener
|
||||||
|
#include "model/GeometryTool.h" // for GeometryTool
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler (ptr only)
|
||||||
|
|
||||||
|
#include "ClipboardHandler.h" // for ClipboardListener
|
||||||
|
#include "ToolHandler.h" // for ToolListener
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class LoadHandler;
|
||||||
|
class GeometryToolController;
|
||||||
|
class AudioController;
|
||||||
|
class FullscreenHandler;
|
||||||
|
class Sidebar;
|
||||||
|
class GladeSearchpath;
|
||||||
|
class MetadataManager;
|
||||||
|
class XournalppCursor;
|
||||||
|
class ToolbarDragDropHandler;
|
||||||
|
class MetadataEntry;
|
||||||
|
class MetadataCallbackData;
|
||||||
|
class PageBackgroundChangeController;
|
||||||
|
class PageTypeHandler;
|
||||||
|
class BaseExportJob;
|
||||||
|
class LayerController;
|
||||||
|
class PluginController;
|
||||||
|
class Document;
|
||||||
|
class EditSelection;
|
||||||
|
class Element;
|
||||||
|
class MainWindow;
|
||||||
|
class ObjectInputStream;
|
||||||
|
class ScrollHandler;
|
||||||
|
class SearchBar;
|
||||||
|
class Settings;
|
||||||
|
class TextEditor;
|
||||||
|
class XournalScheduler;
|
||||||
|
class ZoomControl;
|
||||||
|
class ToolMenuHandler;
|
||||||
|
class XojFont;
|
||||||
|
class XojPdfRectangle;
|
||||||
|
class Callback;
|
||||||
|
class ActionDatabase;
|
||||||
|
|
||||||
|
class Control:
|
||||||
|
public ToolListener,
|
||||||
|
public DocumentHandler,
|
||||||
|
public UndoRedoListener,
|
||||||
|
public ClipboardListener,
|
||||||
|
public ProgressListener {
|
||||||
|
public:
|
||||||
|
Control(GApplication* gtkApp, GladeSearchpath* gladeSearchPath, bool disableAudio);
|
||||||
|
Control(Control const&) = delete;
|
||||||
|
Control(Control&&) = delete;
|
||||||
|
auto operator=(Control const&) -> Control& = delete;
|
||||||
|
auto operator=(Control&&) -> Control& = delete;
|
||||||
|
~Control() override;
|
||||||
|
|
||||||
|
void initWindow(MainWindow* win);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Asymchronously closes the current document and replaces it by a new file
|
||||||
|
void newFile(fs::path filepath = {});
|
||||||
|
|
||||||
|
/// @brief Shows an open file dialog and opens the selected file after closing the previously opened file
|
||||||
|
void askToOpenFile();
|
||||||
|
/**
|
||||||
|
* @brief Asynchronously opens the provided path, safely closes the current opened document and replaces it with the
|
||||||
|
* newly parsed file. Calls callback afterwards, with boolean parameter true on success. Does nothing to this->doc
|
||||||
|
* in case of failure at any point.
|
||||||
|
*/
|
||||||
|
void openFile(
|
||||||
|
fs::path filepath, std::function<void(bool)> callback = [](bool) {}, int scrollToPage = -1,
|
||||||
|
bool forceOpen = false);
|
||||||
|
/// Shows an open file dialog and opens the selected file
|
||||||
|
void askToAnnotatePdf();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Potentially asynchronously) Opens the given file without saving any previously opened Document. Calls callback
|
||||||
|
* afterwards, with boolean parameter true if success. WARNING: may lead to data loss if the current Document has
|
||||||
|
* not been saved yet.
|
||||||
|
*/
|
||||||
|
void openFileWithoutSavingTheCurrentDocument(fs::path filepath, bool attachToDocument, int scrollToPage,
|
||||||
|
std::function<void(bool)> callback);
|
||||||
|
|
||||||
|
void print();
|
||||||
|
void exportAsPdf();
|
||||||
|
void exportAs();
|
||||||
|
void quit(bool allowCancel = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
void saveAs(std::function<void(bool)> callback = [](bool) {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current document as saved if it is currently marked as unsaved.
|
||||||
|
*/
|
||||||
|
void resetSavedStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the current document, prompting to save unsaved changes.
|
||||||
|
*
|
||||||
|
* @param callback Called after trying to close the document, with param true in case of success, false otherwise.
|
||||||
|
* @param allowDestroy Whether clicking "Discard" should destroy the current document.
|
||||||
|
* @param allowCancel Whether the user should be able to cancel closing the document.
|
||||||
|
* @param forceClose Whether to skip the save dialog and unconditionally discard unsaved changes.
|
||||||
|
* @return true if the user closed the document, otherwise false.
|
||||||
|
*/
|
||||||
|
void close(std::function<void(bool)> callback, bool allowDestroy = false, bool allowCancel = true,
|
||||||
|
bool forceClose = false);
|
||||||
|
|
||||||
|
// Menu edit
|
||||||
|
void showSettings();
|
||||||
|
|
||||||
|
// The core handler for inserting latex
|
||||||
|
void runLatex();
|
||||||
|
|
||||||
|
// Menu Help
|
||||||
|
void showAbout();
|
||||||
|
void showGtkDemo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the Cursor and the Toolbar based on the active color
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void toolColorChanged() override;
|
||||||
|
/**
|
||||||
|
* @brief Change the color of the current selection based on the active Tool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void changeColorOfSelection() override;
|
||||||
|
void toolChanged() override;
|
||||||
|
void toolSizeChanged() override;
|
||||||
|
void toolFillChanged() override;
|
||||||
|
void toolLineStyleChanged() override;
|
||||||
|
|
||||||
|
void selectTool(ToolType type);
|
||||||
|
void selectDefaultTool();
|
||||||
|
|
||||||
|
void setFontSelected(const XojFont& font); ///< Modifies the Action state without triggering callbacks
|
||||||
|
void fontChanged(const XojFont& font); ///< Set the font after the user selected a font
|
||||||
|
|
||||||
|
void updatePageNumbers(size_t page, size_t pdfPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current state (selected tool etc.)
|
||||||
|
*/
|
||||||
|
void saveSettings();
|
||||||
|
|
||||||
|
void updateWindowTitle();
|
||||||
|
void setViewPairedPages(bool enabled);
|
||||||
|
void setViewFullscreenMode(bool enabled);
|
||||||
|
void setViewPresentationMode(bool enabled);
|
||||||
|
void setPairsOffset(int numOffset);
|
||||||
|
void setViewColumns(int numColumns);
|
||||||
|
void setViewRows(int numRows);
|
||||||
|
void setViewLayoutVert(bool vert);
|
||||||
|
void setViewLayoutR2L(bool r2l);
|
||||||
|
void setViewLayoutB2T(bool b2t);
|
||||||
|
|
||||||
|
void manageToolbars();
|
||||||
|
void customizeToolbars();
|
||||||
|
void setFullscreen(bool enabled);
|
||||||
|
void setShowSidebar(bool enabled);
|
||||||
|
void setShowToolbar(bool enabled);
|
||||||
|
void setShowMenubar(bool enabled);
|
||||||
|
|
||||||
|
void gotoPage();
|
||||||
|
|
||||||
|
void setToolDrawingType(DrawingType type);
|
||||||
|
|
||||||
|
void paperTemplate();
|
||||||
|
void paperFormat();
|
||||||
|
void changePageBackgroundColor();
|
||||||
|
void updateBackgroundSizeButton();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the view mode (hide/show menu-,tool-&sidebar)
|
||||||
|
*/
|
||||||
|
bool loadViewMode(ViewModeId mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Search text on the given page. The matches (if any) are stored in the XojPageView::SearchControl instance.
|
||||||
|
* @param occurrences If not nullptr, the pointed variable will contain the number of matches on the page
|
||||||
|
* @param matchRect If not nullptr, will contain the topleft point of the first match on the page
|
||||||
|
* (Used for scrolling to the first match)
|
||||||
|
* @return true if at least one match was found
|
||||||
|
*/
|
||||||
|
bool searchTextOnPage(const std::string& text, size_t pageNumber, size_t index, size_t* occurrences,
|
||||||
|
XojPdfRectangle* matchRect);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
size_t firePageSelected(const PageRef& page);
|
||||||
|
void firePageSelected(size_t page);
|
||||||
|
|
||||||
|
void addDefaultPage(const std::optional<std::string>& pageTemplate, Document* doc = nullptr);
|
||||||
|
void duplicatePage();
|
||||||
|
void insertNewPage(size_t position, bool automatedInsertion = false);
|
||||||
|
void appendNewPdfPages();
|
||||||
|
void insertPage(const PageRef& page, size_t position, bool shouldScrollToPage = true);
|
||||||
|
void deletePage();
|
||||||
|
void movePageTowardsBeginning();
|
||||||
|
void movePageTowardsEnd();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the user whether a page with the given id
|
||||||
|
* should be added to the document.
|
||||||
|
*/
|
||||||
|
void askInsertPdfPage(size_t pdfPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable / enable page action buttons
|
||||||
|
*/
|
||||||
|
void updatePageActions();
|
||||||
|
|
||||||
|
// selection handling
|
||||||
|
void clearSelection();
|
||||||
|
|
||||||
|
void moveSelectionToLayer(size_t layerNo);
|
||||||
|
|
||||||
|
void setCopyCutEnabled(bool enabled);
|
||||||
|
|
||||||
|
void enableAutosave(bool enable);
|
||||||
|
|
||||||
|
void clearSelectionEndText();
|
||||||
|
|
||||||
|
void selectAllOnPage();
|
||||||
|
|
||||||
|
void reorderSelection(EditSelection::OrderChange change);
|
||||||
|
|
||||||
|
void setToolSize(ToolSize size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the line style of the PEN if select, or of selected elements if any
|
||||||
|
* Otherwise, select the PEN tool and set its linestyle
|
||||||
|
*/
|
||||||
|
void setLineStyle(const std::string& style);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the eraser type. If the eraser is not selected, select it as well.
|
||||||
|
*/
|
||||||
|
void setEraserType(EraserType type);
|
||||||
|
|
||||||
|
void setFill(bool fill);
|
||||||
|
|
||||||
|
TextEditor* getTextEditor();
|
||||||
|
|
||||||
|
GladeSearchpath* getGladeSearchPath() const;
|
||||||
|
|
||||||
|
void disableSidebarTmp(bool disabled);
|
||||||
|
|
||||||
|
XournalScheduler* getScheduler() const;
|
||||||
|
|
||||||
|
void block(const std::string& name);
|
||||||
|
void unblock();
|
||||||
|
|
||||||
|
void setLastAutosaveFile(fs::path newAutosaveFile);
|
||||||
|
void deleteLastAutosaveFile();
|
||||||
|
void setClipboardHandlerSelection(EditSelection* selection);
|
||||||
|
|
||||||
|
void addChangedDocumentListener(DocumentListener* dl);
|
||||||
|
void removeChangedDocumentListener(DocumentListener* dl);
|
||||||
|
|
||||||
|
MetadataManager* getMetadataManager() const;
|
||||||
|
Settings* getSettings() const;
|
||||||
|
ToolHandler* getToolHandler() const;
|
||||||
|
ZoomControl* getZoomControl() const;
|
||||||
|
Document* getDocument() const;
|
||||||
|
UndoRedoHandler* getUndoRedoHandler() const;
|
||||||
|
MainWindow* getWindow() const;
|
||||||
|
GtkWindow* getGtkWindow() const;
|
||||||
|
ScrollHandler* getScrollHandler() const;
|
||||||
|
PageRef getCurrentPage();
|
||||||
|
size_t getCurrentPageNo() const;
|
||||||
|
XournalppCursor* getCursor() const;
|
||||||
|
Sidebar* getSidebar() const;
|
||||||
|
SearchBar* getSearchBar() const;
|
||||||
|
AudioController* getAudioController() const;
|
||||||
|
PageTypeHandler* getPageTypes() const;
|
||||||
|
PageBackgroundChangeController* getPageBackgroundChangeController() const;
|
||||||
|
LayerController* getLayerController() const;
|
||||||
|
PluginController* getPluginController() const;
|
||||||
|
const Palette& getPalette() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show floating toolbox at specified coordinates
|
||||||
|
* @param x x coordinate relative to main window
|
||||||
|
* @param y y coordinate relative to main window
|
||||||
|
*/
|
||||||
|
void showFloatingToolbox(int x, int y);
|
||||||
|
|
||||||
|
bool copy();
|
||||||
|
bool cut();
|
||||||
|
bool paste();
|
||||||
|
|
||||||
|
void help();
|
||||||
|
|
||||||
|
void selectAlpha(OpacityFeature feature);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the all button tools based on the respective ButtonConfigs
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void initButtonTool();
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
// UndoRedoListener interface
|
||||||
|
void undoRedoChanged() override;
|
||||||
|
void undoRedoPageChanged(PageRef page) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ProgressListener interface
|
||||||
|
void setMaximumState(size_t max) override;
|
||||||
|
void setCurrentState(size_t state) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ClipboardListener interface
|
||||||
|
void clipboardCutCopyEnabled(bool enabled) override;
|
||||||
|
void clipboardPasteEnabled(bool enabled) override;
|
||||||
|
void clipboardPasteText(std::string text) override;
|
||||||
|
void clipboardPasteImage(GdkPixbuf* img) override;
|
||||||
|
void clipboardPasteXournal(ObjectInputStream& in) override;
|
||||||
|
void deleteSelection() override;
|
||||||
|
|
||||||
|
void clipboardPaste(ElementPtr e);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void registerPluginToolButtons(ToolMenuHandler* toolMenuHandler);
|
||||||
|
inline ActionDatabase* getActionDatabase() const { return actionDB.get(); }
|
||||||
|
void loadPaletteFromSettings();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setRotationSnapping(bool enable);
|
||||||
|
void setGridSnapping(bool enable);
|
||||||
|
|
||||||
|
void showFontDialog();
|
||||||
|
void showColorChooserDialog();
|
||||||
|
|
||||||
|
void fileLoaded(int scrollToPage = -1);
|
||||||
|
|
||||||
|
void eraserSizeChanged();
|
||||||
|
void penSizeChanged();
|
||||||
|
void highlighterSizeChanged();
|
||||||
|
|
||||||
|
static bool checkChangedDocument(Control* control);
|
||||||
|
static bool autosaveCallback(Control* control);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load metadata later, md will be deleted
|
||||||
|
*/
|
||||||
|
void loadMetadata(MetadataEntry md);
|
||||||
|
|
||||||
|
static bool loadMetadataCallback(MetadataCallbackData* data);
|
||||||
|
|
||||||
|
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.
|
||||||
|
* @return true if a geometric tool was created
|
||||||
|
*/
|
||||||
|
template <class ToolClass, class ViewClass, class ControllerClass, class InputHandlerClass,
|
||||||
|
GeometryToolType toolType>
|
||||||
|
bool toggleGeometryTool();
|
||||||
|
void resetGeometryTool();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a compass if it's not on the current page yet. Deletes it if it already exists.
|
||||||
|
* @return true if a compass was created
|
||||||
|
*/
|
||||||
|
bool toggleCompass();
|
||||||
|
/**
|
||||||
|
* @brief Creates a setsquare if it's not on the current page yet. Deletes it if it already exists.
|
||||||
|
* @return true if a setsquare was created
|
||||||
|
*/
|
||||||
|
bool toggleSetsquare();
|
||||||
|
|
||||||
|
struct MissingPdfData;
|
||||||
|
/**
|
||||||
|
* Prompt the user that the PDF background is missing and offer solution options
|
||||||
|
*/
|
||||||
|
void promptMissingPdf(MissingPdfData& missingPdf, const fs::path& filepath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the response from the missing PDF dialog
|
||||||
|
*/
|
||||||
|
void missingPdfDialogResponseHandler(fs::path proposedPdfFilepath, int responseId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Closes" the document, preparing the editor for a new document.
|
||||||
|
*/
|
||||||
|
void closeDocument();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly replaces the opened document.
|
||||||
|
* WARNING: Be sure the active document has been saved (or discarded) before calling replaceDocument()
|
||||||
|
*/
|
||||||
|
void replaceDocument(std::unique_ptr<Document> doc, int scrollToPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously opens the provided path and parse it as a .xopp or .xoj file. Then forcibly replaces the currently
|
||||||
|
* opened document with the new one. Asks the user in case of doubts (e.g. wrong file version) and aborts if the
|
||||||
|
* document was not correctly and entirely parsed. Calls callback afterwards, with boolean parameter true on
|
||||||
|
* success. WARNING: Be sure the active document has been saved (or discarded) before calling openXoppFile()
|
||||||
|
*/
|
||||||
|
void openXoppFile(fs::path filepath, int scrollToPage, std::function<void(bool)> callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the provided path and parse it as a PDF file. Then forcibly replaces the currently opened document with a
|
||||||
|
* new one based on the PDF. WARNING: Be sure the active document has been saved (or discarded) before calling
|
||||||
|
* openPdfFile()
|
||||||
|
*
|
||||||
|
* @return true on success
|
||||||
|
*/
|
||||||
|
bool openPdfFile(fs::path filepath, bool attachToDocument, int scrollToPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the provided path and parse it as a PNG file. Then forcibly replaces the currently opened document with a
|
||||||
|
* new one based on the PNG. WARNING: Be sure the active document has been saved (or discarded) before calling
|
||||||
|
* openPngFile()
|
||||||
|
*
|
||||||
|
* @return true on success
|
||||||
|
*/
|
||||||
|
bool openPngFile(fs::path filepath, bool attachToDocument, int scrollToPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the provided path and parse it as a .xopt template file. Then forcibly replaces the currently opened
|
||||||
|
* document with a new one based on the template. WARNING: Be sure the active document has been saved (or discarded)
|
||||||
|
* before calling openXoptFile()
|
||||||
|
*
|
||||||
|
* @return true on success
|
||||||
|
*/
|
||||||
|
bool openXoptFile(fs::path filepath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the pen line style to select in the toolbar
|
||||||
|
*
|
||||||
|
* @return style to select, empty if no style should be selected (active
|
||||||
|
* selection with differing line styles)
|
||||||
|
*/
|
||||||
|
auto getLineStyleToSelect() -> std::optional<std::string> const;
|
||||||
|
|
||||||
|
UndoRedoHandler* undoRedo = nullptr;
|
||||||
|
ZoomControl* zoom = nullptr;
|
||||||
|
|
||||||
|
Settings* settings = nullptr;
|
||||||
|
std::unique_ptr<Palette> palette;
|
||||||
|
MainWindow* win = nullptr;
|
||||||
|
|
||||||
|
Document* doc = nullptr;
|
||||||
|
|
||||||
|
Sidebar* sidebar = nullptr;
|
||||||
|
SearchBar* searchBar = nullptr;
|
||||||
|
|
||||||
|
ToolHandler* toolHandler;
|
||||||
|
|
||||||
|
ScrollHandler* scrollHandler;
|
||||||
|
|
||||||
|
ToolbarDragDropHandler* dragDropHandler = nullptr;
|
||||||
|
|
||||||
|
GApplication* gtkApp = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cursor handler
|
||||||
|
*/
|
||||||
|
XournalppCursor* cursor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout id: the timeout watches the changes and actualizes the previews from time to time
|
||||||
|
*/
|
||||||
|
guint changeTimout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pages wihch has changed since the last update (for preview update)
|
||||||
|
*/
|
||||||
|
std::vector<PageRef> changedPages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocumentListener instances that are to be updated by checkChangedDocument.
|
||||||
|
*/
|
||||||
|
std::list<DocumentListener*> changedDocumentListeners;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our clipboard abstraction
|
||||||
|
*/
|
||||||
|
ClipboardHandler* clipboardHandler = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The autosave handler ID
|
||||||
|
*/
|
||||||
|
guint autosaveTimeout = 0;
|
||||||
|
fs::path lastAutosaveFilename;
|
||||||
|
|
||||||
|
XournalScheduler* scheduler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State / Blocking attributes
|
||||||
|
*/
|
||||||
|
GtkWidget* statusbar = nullptr;
|
||||||
|
GtkLabel* lbState = nullptr;
|
||||||
|
GtkProgressBar* pgState = nullptr;
|
||||||
|
size_t maxState = 0;
|
||||||
|
bool isBlocking;
|
||||||
|
|
||||||
|
GladeSearchpath* gladeSearchPath;
|
||||||
|
|
||||||
|
MetadataManager* metadata;
|
||||||
|
|
||||||
|
PageTypeHandler* pageTypes;
|
||||||
|
|
||||||
|
std::unique_ptr<PageBackgroundChangeController> pageBackgroundChangeController;
|
||||||
|
|
||||||
|
LayerController* layerController;
|
||||||
|
|
||||||
|
std::unique_ptr<GeometryTool> geometryTool;
|
||||||
|
std::unique_ptr<GeometryToolController> geometryToolController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage all Xournal++ plugins
|
||||||
|
*/
|
||||||
|
PluginController* pluginController;
|
||||||
|
|
||||||
|
std::unique_ptr<ActionDatabase> actionDB;
|
||||||
|
template <Action a>
|
||||||
|
friend struct ActionProperties;
|
||||||
|
|
||||||
|
// Keep after the ActionDatabase so it is destroyed first: ~AudioController refers to the ActionDatabase
|
||||||
|
std::unique_ptr<AudioController> audioController;
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#include "CrashHandler.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <csignal>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "control/xojfile/SaveHandler.h" // for SaveHandler
|
||||||
|
#include "util/PathUtil.h"
|
||||||
|
#include "util/Stacktrace.h"
|
||||||
|
#include "util/VersionInfo.h"
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
static std::atomic<const Document*> document = nullptr;
|
||||||
|
static std::atomic<int> alreadyCrashed = 0;
|
||||||
|
|
||||||
|
extern "C" void forceClose(int sig) {
|
||||||
|
g_warning("Force close requested with signal %i", sig);
|
||||||
|
emergencySave();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEmergencyDocument(const Document* doc) { document = doc; }
|
||||||
|
|
||||||
|
void emergencySave() {
|
||||||
|
if (document == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "Trying to emergency save the current open document..." << std::endl;
|
||||||
|
|
||||||
|
auto const& filepath = Util::getConfigFile("emergencysave.xopp");
|
||||||
|
|
||||||
|
SaveHandler handler;
|
||||||
|
handler.prepareSave(document, filepath);
|
||||||
|
handler.saveTo(filepath);
|
||||||
|
|
||||||
|
if (!handler.getErrorMessage().empty()) {
|
||||||
|
std::cerr << "Error: " << handler.getErrorMessage() << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Successfully saved document to \"" << char_cast(filepath.u8string()) << "\"" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Print a backtrace and try to make an emergency save
|
||||||
|
extern "C" void crashHandler(int sig) {
|
||||||
|
int crash = ++alreadyCrashed;
|
||||||
|
|
||||||
|
std::cerr << "\n\n\n*************************************************************************\n\n";
|
||||||
|
std::cerr << "[Crash Handler] Crashed " << crash << " time(s) with signal " << sig << std::endl;
|
||||||
|
if (crash == 1) { // Avoid a loop in case we crash again on emergencySave()
|
||||||
|
std::cerr << xoj::util::getVersionInfo() << std::endl;
|
||||||
|
|
||||||
|
std::cerr << "\nTry to get a stacktrace...\n";
|
||||||
|
|
||||||
|
Stacktrace::printStacktrace(std::cerr);
|
||||||
|
std::cerr << std::endl;
|
||||||
|
|
||||||
|
emergencySave();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __unix__
|
||||||
|
// Forward the signal for the system's default handling - we may get a coredump
|
||||||
|
std::signal(sig, SIG_DFL);
|
||||||
|
kill(getpid(), sig);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
exit(crash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void installCrashHandlers() {
|
||||||
|
std::signal(SIGTERM, forceClose);
|
||||||
|
std::signal(SIGINT, forceClose);
|
||||||
|
std::signal(SIGSEGV, crashHandler);
|
||||||
|
std::signal(SIGFPE, crashHandler);
|
||||||
|
std::signal(SIGILL, crashHandler);
|
||||||
|
std::signal(SIGABRT, crashHandler);
|
||||||
|
#ifdef SIGTRAP
|
||||||
|
std::signal(SIGTRAP, crashHandler);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Error handler, prints a stacktrace if Xournal++ crashes
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
class Document;
|
||||||
|
void setEmergencyDocument(const Document* doc);
|
||||||
|
void installCrashHandlers();
|
||||||
|
void emergencySave();
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#include "control/DeviceListHelper.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for find, remove_if
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <glib.h> // for g_list_free, g_warning
|
||||||
|
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "util/GListView.h" // for GListView, GListView<>::GList...
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
|
||||||
|
|
||||||
|
void storeNewUnlistedDevice(std::vector<InputDevice>& deviceList, GdkDevice* device) {
|
||||||
|
// This could potentially be problematic with systems having a multitude of input devices as it searches linearily
|
||||||
|
auto it = std::find(deviceList.begin(), deviceList.end(), InputDevice(device));
|
||||||
|
if (it != deviceList.end()) {
|
||||||
|
// Device is already known but source may be unknown
|
||||||
|
it->updateType(gdk_device_get_source(device));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceList.emplace_back(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addDevicesToList(std::vector<InputDevice>& deviceList, GListView<GdkDevice> devView, bool ignoreTouchDevices) {
|
||||||
|
for (GdkDevice& dev: devView) {
|
||||||
|
if (GDK_SOURCE_KEYBOARD == gdk_device_get_source(&dev)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (gdk_device_get_vendor_id(&dev) == nullptr && gdk_device_get_product_id(&dev) == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ignoreTouchDevices && GDK_SOURCE_TOUCHSCREEN == gdk_device_get_source(&dev)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
storeNewUnlistedDevice(deviceList, &dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DeviceListHelper::getDeviceList(Settings* settings, bool ignoreTouchDevices) -> std::vector<InputDevice> {
|
||||||
|
std::vector<InputDevice> deviceList = settings->getKnownInputDevices();
|
||||||
|
|
||||||
|
if (ignoreTouchDevices) {
|
||||||
|
deviceList.erase(
|
||||||
|
std::remove_if(deviceList.begin(), deviceList.end(),
|
||||||
|
[](const InputDevice& device) { return device.getSource() == GDK_SOURCE_TOUCHSCREEN; }),
|
||||||
|
deviceList.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
GdkDisplay* display = gdk_display_get_default();
|
||||||
|
GdkSeat* defaultSeat = gdk_display_get_default_seat(display);
|
||||||
|
GdkDevice* pointer = gdk_seat_get_pointer(defaultSeat);
|
||||||
|
GdkSeat* pointerSeat = gdk_device_get_seat(pointer);
|
||||||
|
auto* pointerSlaves = gdk_seat_get_slaves(pointerSeat, GDK_SEAT_CAPABILITY_ALL_POINTING);
|
||||||
|
addDevicesToList(deviceList, pointerSlaves, ignoreTouchDevices);
|
||||||
|
g_list_free(pointerSlaves);
|
||||||
|
|
||||||
|
if (deviceList.empty()) {
|
||||||
|
g_warning("No device found. Is Xournal++ running in debugger / Eclipse...?\n"
|
||||||
|
"Probably this is the reason for not finding devices!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceList;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDeviceClass DeviceListHelper::getSourceMapping(GdkInputSource source, Settings* settings) {
|
||||||
|
auto deviceList = DeviceListHelper::getDeviceList(settings);
|
||||||
|
|
||||||
|
for (InputDevice const& inputDevice: deviceList) {
|
||||||
|
InputDeviceClass deviceClass =
|
||||||
|
InputEvents::translateDeviceType(inputDevice.getName(), inputDevice.getSource(), settings);
|
||||||
|
|
||||||
|
if (inputDevice.getSource() == source) {
|
||||||
|
return deviceClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputDeviceClass::INPUT_DEVICE_IGNORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDevice::InputDevice() = default;
|
||||||
|
|
||||||
|
InputDevice::InputDevice(GdkDevice* device): name(gdk_device_get_name(device)), source(gdk_device_get_source(device)) {}
|
||||||
|
|
||||||
|
InputDevice::InputDevice(std::string name, GdkInputSource source): name(std::move(name)), source(source) {}
|
||||||
|
|
||||||
|
auto InputDevice::getName() const -> std::string { return this->name; }
|
||||||
|
|
||||||
|
auto InputDevice::getSource() const -> GdkInputSource { return this->source; }
|
||||||
|
|
||||||
|
void InputDevice::updateType(GdkInputSource newSource) { this->source = newSource; }
|
||||||
|
|
||||||
|
auto InputDevice::getType() const -> std::string {
|
||||||
|
switch (source) {
|
||||||
|
case GDK_SOURCE_MOUSE:
|
||||||
|
return _("mouse");
|
||||||
|
case GDK_SOURCE_PEN:
|
||||||
|
return _("pen");
|
||||||
|
case GDK_SOURCE_ERASER:
|
||||||
|
return _("eraser");
|
||||||
|
case GDK_SOURCE_CURSOR:
|
||||||
|
return _("cursor");
|
||||||
|
case GDK_SOURCE_KEYBOARD:
|
||||||
|
// not used: filtered above
|
||||||
|
return _("keyboard");
|
||||||
|
case GDK_SOURCE_TOUCHSCREEN:
|
||||||
|
return _("touchscreen");
|
||||||
|
case GDK_SOURCE_TOUCHPAD:
|
||||||
|
return _("touchpad");
|
||||||
|
#if (GDK_MAJOR_VERSION >= 3 && GDK_MINOR_VERSION >= 22)
|
||||||
|
case GDK_SOURCE_TRACKPOINT:
|
||||||
|
return _("trackpoint");
|
||||||
|
case GDK_SOURCE_TABLET_PAD:
|
||||||
|
return _("tablet pad");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto InputDevice::operator==(const InputDevice& inputDevice) const -> bool {
|
||||||
|
return this->getName() == inputDevice.getName();
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Helper functions to iterate over devices
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <gdk/gdk.h> // for GdkInputSource, GdkDevice
|
||||||
|
|
||||||
|
#include "gui/inputdevices/InputEvents.h" // for InputDeviceClass
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
|
||||||
|
class InputDevice {
|
||||||
|
public:
|
||||||
|
InputDevice();
|
||||||
|
explicit InputDevice(GdkDevice* device);
|
||||||
|
explicit InputDevice(std::string name, GdkInputSource source);
|
||||||
|
~InputDevice() = default;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string getType() const;
|
||||||
|
std::string getName() const;
|
||||||
|
GdkInputSource getSource() const;
|
||||||
|
void updateType(GdkInputSource newSource);
|
||||||
|
|
||||||
|
bool operator==(const InputDevice& inputDevice) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string name;
|
||||||
|
GdkInputSource source{GDK_SOURCE_MOUSE};
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace DeviceListHelper {
|
||||||
|
std::vector<InputDevice> getDeviceList(Settings* settings, bool ignoreTouchDevices = false);
|
||||||
|
InputDeviceClass getSourceMapping(GdkInputSource source, Settings* settings);
|
||||||
|
} // namespace DeviceListHelper
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#include "ExportHelper.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for max
|
||||||
|
#include <memory> // for unique_ptr, allocator
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <gio/gio.h> // for g_file_new_for_commandlin...
|
||||||
|
#include <glib.h> // for g_message, g_error
|
||||||
|
|
||||||
|
#include "control/jobs/ImageExport.h" // for ImageExport, EXPORT_GRAPH...
|
||||||
|
#include "control/jobs/ProgressListener.h" // for DummyProgressListener
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "pdf/base/XojPdfExport.h" // for XojPdfExport
|
||||||
|
#include "pdf/base/XojPdfExportFactory.h" // for XojPdfExportFactory
|
||||||
|
#include "util/ElementRange.h" // for parse, PageRangeVector
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
#include "util/raii/GObjectSPtr.h" // for GObjectSPtr
|
||||||
|
|
||||||
|
#include "filesystem.h" // for operator==, path
|
||||||
|
|
||||||
|
namespace ExportHelper {
|
||||||
|
|
||||||
|
auto exportImg(Document* doc, fs::path output, const char* range, const char* layerRange, int pngDpi, int pngWidth,
|
||||||
|
int pngHeight, ExportBackgroundType exportBackground) -> int {
|
||||||
|
|
||||||
|
ExportGraphicsFormat format = EXPORT_GRAPHICS_PNG;
|
||||||
|
|
||||||
|
if (output.extension() == ".svg") {
|
||||||
|
format = EXPORT_GRAPHICS_SVG;
|
||||||
|
}
|
||||||
|
|
||||||
|
PageRangeVector exportRange;
|
||||||
|
if (range) {
|
||||||
|
exportRange = ElementRange::parse(range, doc->getPageCount());
|
||||||
|
} else {
|
||||||
|
exportRange.emplace_back(0, doc->getPageCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
DummyProgressListener progress;
|
||||||
|
|
||||||
|
ImageExport imgExport(doc, std::move(output), format, exportBackground, exportRange);
|
||||||
|
|
||||||
|
if (format == EXPORT_GRAPHICS_PNG) {
|
||||||
|
if (pngDpi > 0) {
|
||||||
|
imgExport.setQualityParameter(EXPORT_QUALITY_DPI, pngDpi);
|
||||||
|
} else if (pngWidth > 0) {
|
||||||
|
imgExport.setQualityParameter(EXPORT_QUALITY_WIDTH, pngWidth);
|
||||||
|
} else if (pngHeight > 0) {
|
||||||
|
imgExport.setQualityParameter(EXPORT_QUALITY_HEIGHT, pngHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgExport.setLayerRange(layerRange);
|
||||||
|
|
||||||
|
imgExport.exportGraphics(&progress);
|
||||||
|
|
||||||
|
std::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 exportPdf(Document* doc, const fs::path& output, const char* range, const char* layerRange,
|
||||||
|
ExportBackgroundType exportBackground, bool progressiveMode, ExportBackend backend) -> int {
|
||||||
|
std::unique_ptr<XojPdfExport> pdfe = XojPdfExportFactory::createExport(doc, nullptr, backend);
|
||||||
|
pdfe->setExportBackground(exportBackground);
|
||||||
|
|
||||||
|
// Check if we're trying to overwrite the background PDF file
|
||||||
|
auto backgroundPDF = doc->getPdfFilepath();
|
||||||
|
try {
|
||||||
|
if (!backgroundPDF.empty() && fs::exists(backgroundPDF)) {
|
||||||
|
if (fs::weakly_canonical(output) == fs::weakly_canonical(backgroundPDF)) {
|
||||||
|
g_message("Do not overwrite the background PDF! This will cause errors!");
|
||||||
|
return -3; // Return error code for export failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error& fe) {
|
||||||
|
g_warning("The check for overwriting the background failed with: %s", fe.what());
|
||||||
|
return -3; // Return error code for export failure
|
||||||
|
}
|
||||||
|
|
||||||
|
bool exportSuccess = 0; // Return of the export job
|
||||||
|
|
||||||
|
pdfe->setLayerRange(layerRange);
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
// Parse the range
|
||||||
|
PageRangeVector exportRange = ElementRange::parse(range, doc->getPageCount());
|
||||||
|
// Do the export
|
||||||
|
exportSuccess = pdfe->createPdf(output, exportRange, progressiveMode);
|
||||||
|
} else {
|
||||||
|
exportSuccess = pdfe->createPdf(output, progressiveMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exportSuccess) {
|
||||||
|
g_message("%s", pdfe->getLastError().c_str());
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_message("%s", _("PDF file successfully created"));
|
||||||
|
|
||||||
|
return 0; // no error
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ExportHelper
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Helper functions to iterate over devices
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
#include "control/jobs/BaseExportJob.h" // for ExportBackgroundType
|
||||||
|
#include "pdf/base/PdfExportBackend.h"
|
||||||
|
|
||||||
|
#include "filesystem.h"
|
||||||
|
|
||||||
|
class Document;
|
||||||
|
|
||||||
|
namespace ExportHelper {
|
||||||
|
/**
|
||||||
|
* @brief Export the input file as a bunch of image files (one per page)
|
||||||
|
* @param doc Document to export
|
||||||
|
* @param output Path to the output file(s)
|
||||||
|
* @param range Page range to be parsed. If range=nullptr, exports the whole file
|
||||||
|
* @param layerRange Layer range to be parsed. Will only export those layers, for every exported page.
|
||||||
|
* If a number is too high for the number of layers on a given page, it is just ignored.
|
||||||
|
* If range=nullptr, exports all layers.
|
||||||
|
* @param pngDpi Set dpi for Png files. Non positive values are ignored
|
||||||
|
* @param pngWidth Set the width for Png files. Non positive values are ignored
|
||||||
|
* @param pngHeight Set the height for Png files. Non positive values are ignored
|
||||||
|
* @param exportBackground If EXPORT_BACKGROUND_NONE, the exported image file has transparent background
|
||||||
|
*
|
||||||
|
* The priority is: pngDpi overwrites pngWidth overwrites pngHeight
|
||||||
|
*
|
||||||
|
* @return 0 on success, -2 on failure opening the input file, -3 on export failure
|
||||||
|
*/
|
||||||
|
int exportImg(Document* doc, fs::path output, const char* range, const char* layerRange, int pngDpi, int pngWidth,
|
||||||
|
int pngHeight, ExportBackgroundType exportBackground);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Export the input file as pdf
|
||||||
|
* @param doc Document to export
|
||||||
|
* @param output Path to the output file
|
||||||
|
* @param range Page range to be parsed. If range=nullptr, exports the whole file
|
||||||
|
* @param layerRange Layer range to be parsed. Will only export those layers, for every exported page.
|
||||||
|
* If a number is too high for the number of layers on a given page, it is just ignored.
|
||||||
|
* If range=nullptr, exports all layers.
|
||||||
|
* @param exportBackground If EXPORT_BACKGROUND_NONE, the exported pdf file has white background
|
||||||
|
* @param progressiveMode If true, then for each xournalpp page, instead of rendering one PDF page, the page layers are
|
||||||
|
* rendered one by one to produce as many pages as there are layers.
|
||||||
|
*
|
||||||
|
* @return 0 on success, -2 on failure opening the input file, -3 on export failure
|
||||||
|
*/
|
||||||
|
int exportPdf(Document* doc, const fs::path& output, const char* range, const char* layerRange,
|
||||||
|
ExportBackgroundType exportBackground, bool progressiveMode,
|
||||||
|
ExportBackend backend = ExportBackend::DEFAULT);
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace ExportHelper
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#include "GeometryToolController.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "control/Control.h"
|
||||||
|
#include "control/layer/LayerController.h"
|
||||||
|
#include "gui/XournalView.h"
|
||||||
|
#include "gui/XournalppCursor.h"
|
||||||
|
#include "model/Document.h"
|
||||||
|
#include "model/GeometryTool.h"
|
||||||
|
#include "model/Stroke.h"
|
||||||
|
#include "model/XojPage.h"
|
||||||
|
#include "undo/InsertUndoAction.h"
|
||||||
|
|
||||||
|
using xoj::util::Rectangle;
|
||||||
|
|
||||||
|
constexpr double MARK_SIZE = 2.;
|
||||||
|
|
||||||
|
GeometryToolController::GeometryToolController(XojPageView* view, GeometryTool* geometryTool):
|
||||||
|
view(view), geometryTool(geometryTool) {}
|
||||||
|
|
||||||
|
GeometryToolController::~GeometryToolController() = default;
|
||||||
|
|
||||||
|
void GeometryToolController::translate(const xoj::util::Point<double>& offset) {
|
||||||
|
geometryTool->setOrigin(geometryTool->getOrigin() + offset);
|
||||||
|
geometryTool->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeometryToolController::rotate(double da, const xoj::util::Point<double>& center) {
|
||||||
|
const auto offset = geometryTool->getOrigin() - center;
|
||||||
|
geometryTool->setOrigin(center + xoj::util::Point<double>(offset.x * std::cos(da) - offset.y * std::sin(da),
|
||||||
|
offset.x * std::sin(da) + offset.y * std::cos(da)));
|
||||||
|
this->rotate(da);
|
||||||
|
}
|
||||||
|
void GeometryToolController::rotate(double da) {
|
||||||
|
geometryTool->setRotation(geometryTool->getRotation() + da);
|
||||||
|
geometryTool->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void GeometryToolController::scale(double f, const xoj::util::Point<double>& center) {
|
||||||
|
geometryTool->setOrigin(center + (geometryTool->getOrigin() - center) * f);
|
||||||
|
this->scale(f);
|
||||||
|
}
|
||||||
|
void GeometryToolController::scale(double f) {
|
||||||
|
geometryTool->setHeight(geometryTool->getHeight() * f);
|
||||||
|
geometryTool->notify(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void GeometryToolController::markOrigin() {
|
||||||
|
const auto control = view->getXournal()->getControl();
|
||||||
|
const auto h = control->getToolHandler();
|
||||||
|
auto cross = std::make_unique<Stroke>();
|
||||||
|
cross->setWidth(h->getToolThickness(TOOL_PEN)[TOOL_SIZE_FINE]);
|
||||||
|
cross->setColor(h->getTool(TOOL_PEN).getColor());
|
||||||
|
const double x = geometryTool->getOrigin().x;
|
||||||
|
const double y = geometryTool->getOrigin().y;
|
||||||
|
cross->addPoint(Point(x + MARK_SIZE, y + MARK_SIZE));
|
||||||
|
cross->addPoint(Point(x - MARK_SIZE, y - MARK_SIZE));
|
||||||
|
cross->addPoint(Point(x, y));
|
||||||
|
cross->addPoint(Point(x + MARK_SIZE, y - MARK_SIZE));
|
||||||
|
cross->addPoint(Point(x - MARK_SIZE, y + MARK_SIZE));
|
||||||
|
|
||||||
|
auto* ptr = cross.get();
|
||||||
|
const auto doc = control->getDocument();
|
||||||
|
const auto page = view->getPage();
|
||||||
|
doc->lock();
|
||||||
|
const auto layer = page->getSelectedLayer();
|
||||||
|
layer->addElement(std::move(cross));
|
||||||
|
doc->unlock();
|
||||||
|
|
||||||
|
const auto undo = control->getUndoRedoHandler();
|
||||||
|
undo->addUndoAction(std::make_unique<InsertUndoAction>(page, layer, ptr));
|
||||||
|
|
||||||
|
|
||||||
|
const Rectangle<double> rect{ptr->getX(), ptr->getY(), ptr->getElementWidth(), ptr->getElementHeight()};
|
||||||
|
view->rerenderRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeometryToolController::addStrokeToLayer() {
|
||||||
|
const auto xournal = view->getXournal();
|
||||||
|
const auto control = xournal->getControl();
|
||||||
|
const auto doc = control->getDocument();
|
||||||
|
const auto page = view->getPage();
|
||||||
|
|
||||||
|
auto ptr = stroke.get();
|
||||||
|
doc->lock();
|
||||||
|
const auto layer = page->getSelectedLayer();
|
||||||
|
layer->addElement(std::move(this->stroke));
|
||||||
|
doc->unlock();
|
||||||
|
|
||||||
|
const auto undo = control->getUndoRedoHandler();
|
||||||
|
undo->addUndoAction(std::make_unique<InsertUndoAction>(page, layer, ptr));
|
||||||
|
const Rectangle<double> rect{ptr->getX(), ptr->getY(), ptr->getElementWidth(), ptr->getElementHeight()};
|
||||||
|
view->rerenderRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
geometryTool->setStroke(nullptr);
|
||||||
|
xournal->getCursor()->updateCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeometryToolController::initializeStroke() {
|
||||||
|
const auto h = view->getXournal()->getControl()->getToolHandler();
|
||||||
|
stroke = std::make_unique<Stroke>();
|
||||||
|
geometryTool->setStroke(stroke.get());
|
||||||
|
stroke->setWidth(h->getThickness());
|
||||||
|
stroke->setColor(h->getColor());
|
||||||
|
stroke->setFill(h->getFill());
|
||||||
|
stroke->setLineStyle(h->getLineStyle());
|
||||||
|
switch (h->getToolType()) {
|
||||||
|
case TOOL_PEN:
|
||||||
|
stroke->setToolType(StrokeTool::PEN);
|
||||||
|
break;
|
||||||
|
case TOOL_HIGHLIGHTER:
|
||||||
|
stroke->setToolType(StrokeTool::HIGHLIGHTER);
|
||||||
|
break;
|
||||||
|
case TOOL_ERASER:
|
||||||
|
stroke->setToolType(StrokeTool::ERASER);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
g_warning("Unhandled tool when initializing stroke in geometry tool controller");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GeometryToolController::getPage() const -> const PageRef { return view->getPage(); }
|
||||||
|
|
||||||
|
auto GeometryToolController::getView() const -> XojPageView* { return view; }
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A geometry tool controller
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include "gui/PageView.h"
|
||||||
|
#include "model/GeometryTool.h"
|
||||||
|
#include "util/Point.h"
|
||||||
|
|
||||||
|
class Stroke;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A class that controls a geometry tool
|
||||||
|
|
||||||
|
* The geometry tool can be moved, rotated and scaled
|
||||||
|
* There are methods for translating coordinates
|
||||||
|
* and methods to deal with the temporary stroke
|
||||||
|
*/
|
||||||
|
|
||||||
|
class GeometryToolController {
|
||||||
|
public:
|
||||||
|
GeometryToolController(XojPageView* view, GeometryTool* geometryTool);
|
||||||
|
virtual ~GeometryToolController();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief translates the geometry tool in x- and y-direction
|
||||||
|
* @param offset the translation vector (in document coordinates)
|
||||||
|
*/
|
||||||
|
void translate(const xoj::util::Point<double>& offset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief rotates the geometry tool around then given rotation center
|
||||||
|
* @param da the rotation angle
|
||||||
|
* @param center the rotation center (in document coordinates
|
||||||
|
*/
|
||||||
|
void rotate(double da, const xoj::util::Point<double>& center);
|
||||||
|
/// Rotates around the tool's origin
|
||||||
|
void rotate(double da);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief resizes the geometry tool by the factor f with respect to a given scaling center
|
||||||
|
* @param f the scaling factor
|
||||||
|
* @param center the scaling center (in document coordinates)
|
||||||
|
*/
|
||||||
|
void scale(double f, const xoj::util::Point<double>& center);
|
||||||
|
/// Rescales around the tools origin
|
||||||
|
void scale(double f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief marks the origin with a "x" and puts the mark onto the current layer
|
||||||
|
*/
|
||||||
|
void markOrigin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief adds the stroke to the layer and rerenders the stroke area
|
||||||
|
*/
|
||||||
|
void addStrokeToLayer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief initializes the stroke by using the properties from the tool handler
|
||||||
|
*/
|
||||||
|
void initializeStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the page view of the page with respect to which the geometry tool is initialized
|
||||||
|
*/
|
||||||
|
XojPageView* getView() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the page with respect to which the setsquare is initialized
|
||||||
|
*/
|
||||||
|
const PageRef getPage() const;
|
||||||
|
|
||||||
|
inline GeometryTool* getGeometryTool() const { return geometryTool; }
|
||||||
|
|
||||||
|
virtual bool isInsideGeometryTool(double x, double y, double border = 0.0) const = 0;
|
||||||
|
|
||||||
|
virtual GeometryToolType getType() const = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
XojPageView* view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the underlying geometry tool
|
||||||
|
*/
|
||||||
|
GeometryTool* geometryTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The stroke drawn
|
||||||
|
*/
|
||||||
|
std::unique_ptr<Stroke> stroke;
|
||||||
|
};
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
#include "LatexController.h"
|
||||||
|
|
||||||
|
#include <cstdlib> // for free
|
||||||
|
#include <fstream> // for ifstream, basic_istream
|
||||||
|
#include <iterator> // for istreambuf_iterator, ope...
|
||||||
|
#include <limits> // for numeric_limits
|
||||||
|
#include <memory> // for unique_ptr, allocator
|
||||||
|
#include <optional> // for optional
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <variant> // for get_if
|
||||||
|
|
||||||
|
#include <glib.h> // for g_error_free, g_error_ma...
|
||||||
|
|
||||||
|
#include "control/Tool.h" // for Tool
|
||||||
|
#include "control/ToolEnums.h" // for TOOL_TEXT
|
||||||
|
#include "control/ToolHandler.h" // for ToolHandler
|
||||||
|
#include "control/latex/LatexGenerator.h" // for LatexGenerator::GenError
|
||||||
|
#include "control/settings/LatexSettings.h" // for LatexSettings
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "control/tools/EditSelection.h" // for EditSelection
|
||||||
|
#include "gui/Layout.h" // for Layout
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/PageView.h" // for XojPageView
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "gui/dialog/ExtEdLatexDialog.h" // for ExtEdLatexDialog
|
||||||
|
#include "gui/dialog/IntEdLatexDialog.h" // for IntEdLatexDialog
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/Element.h" // for Element
|
||||||
|
#include "model/Layer.h" // for Layer
|
||||||
|
#include "model/TexImage.h" // for TexImage
|
||||||
|
#include "model/Text.h" // for Text
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "undo/InsertUndoAction.h" // for InsertUndoAction
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler
|
||||||
|
#include "util/Assert.h" // for xoj_assert
|
||||||
|
#include "util/Color.h" // for Color, get_color_contrast
|
||||||
|
#include "util/PathUtil.h" // for ensureFolderExists, getT...
|
||||||
|
#include "util/PlaceholderString.h" // for PlaceholderString
|
||||||
|
#include "util/PopupWindowWrapper.h" // for PopupWindowWrapper
|
||||||
|
#include "util/Rectangle.h" // for Rectangle
|
||||||
|
#include "util/Util.h" // for npos
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for FS, _, _F, N_
|
||||||
|
#include "util/safe_casts.h" // for round_cast
|
||||||
|
|
||||||
|
#include "Control.h" // for Control
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
|
||||||
|
constexpr Color LIGHT_PREVIEW_BACKGROUND = Colors::white;
|
||||||
|
constexpr Color DARK_PREVIEW_BACKGROUND = Colors::black;
|
||||||
|
|
||||||
|
LatexController::LatexController(Control* control):
|
||||||
|
control(control),
|
||||||
|
settings(control->getSettings()->latexSettings),
|
||||||
|
doc(control->getDocument()),
|
||||||
|
texTmpDir(Util::getTmpDirSubfolder("tex")),
|
||||||
|
generator(settings) {
|
||||||
|
Util::ensureFolderExists(this->texTmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
LatexController::~LatexController() {
|
||||||
|
if (updating_cancellable) {
|
||||||
|
g_cancellable_cancel(updating_cancellable);
|
||||||
|
g_object_unref(updating_cancellable);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->control = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the tex executable, return false if not found
|
||||||
|
*/
|
||||||
|
auto LatexController::findTexDependencies() -> LatexController::FindDependencyStatus {
|
||||||
|
auto templatePath = this->settings.globalTemplatePath;
|
||||||
|
if (fs::is_regular_file(templatePath)) {
|
||||||
|
std::ifstream is(templatePath, std::ios_base::binary);
|
||||||
|
if (!is.is_open()) {
|
||||||
|
g_message("%s", templatePath.string().c_str());
|
||||||
|
string msg = _("Global template file does not exist. Please check your settings.");
|
||||||
|
return LatexController::FindDependencyStatus(false, msg);
|
||||||
|
}
|
||||||
|
this->latexTemplate = std::string(std::istreambuf_iterator<char>(is), {});
|
||||||
|
if (!is.good()) {
|
||||||
|
string msg = _("Failed to read global template file. Please check your settings.");
|
||||||
|
return LatexController::FindDependencyStatus(false, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LatexController::FindDependencyStatus(true, "");
|
||||||
|
} else {
|
||||||
|
string msg = _("Global template file is not a regular file. Please check your settings. ");
|
||||||
|
return LatexController::FindDependencyStatus(false, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a selected tex element, and load it
|
||||||
|
*/
|
||||||
|
void LatexController::findSelectedTexElement() {
|
||||||
|
std::shared_lock<Document> lock(*doc);
|
||||||
|
auto pageNr = this->control->getCurrentPageNo();
|
||||||
|
if (pageNr == npos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->view = this->control->getWindow()->getXournal()->getViewFor(pageNr);
|
||||||
|
if (view == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we get the selection
|
||||||
|
this->page = this->doc->getPage(pageNr);
|
||||||
|
this->layer = page->getSelectedLayer();
|
||||||
|
|
||||||
|
auto* tex = view->getSelectedTex();
|
||||||
|
this->selectedElem =
|
||||||
|
tex != nullptr ? static_cast<const Element*>(tex) : static_cast<const Element*>(view->getSelectedText());
|
||||||
|
|
||||||
|
if (this->selectedElem) {
|
||||||
|
// this will get the position of the Latex properly
|
||||||
|
EditSelection* theSelection = control->getWindow()->getXournal()->getSelection();
|
||||||
|
xoj::util::Rectangle<double> rect = theSelection->getSnappedBounds();
|
||||||
|
this->posx = rect.x;
|
||||||
|
this->posy = rect.y;
|
||||||
|
|
||||||
|
if (auto* img = dynamic_cast<const TexImage*>(this->selectedElem)) {
|
||||||
|
this->initialTex = img->getText();
|
||||||
|
this->temporaryRender = img->cloneTexImage();
|
||||||
|
this->isValidTex = true;
|
||||||
|
} else if (auto* txt = dynamic_cast<const Text*>(this->selectedElem)) {
|
||||||
|
this->initialTex = "\\text{" + txt->getText() + "}";
|
||||||
|
}
|
||||||
|
this->imgwidth = this->selectedElem->getElementWidth();
|
||||||
|
this->imgheight = this->selectedElem->getElementHeight();
|
||||||
|
} else {
|
||||||
|
// This is a new latex object, so here we pick a convenient initial location
|
||||||
|
const double zoom = this->control->getWindow()->getXournal()->getZoom();
|
||||||
|
Layout* layout = this->control->getWindow()->getXournal()->getLayout();
|
||||||
|
|
||||||
|
// Calculate coordinates (screen) of the center of the visible area
|
||||||
|
const auto visibleBounds = layout->getVisibleRect();
|
||||||
|
const double centerX = visibleBounds.x + 0.5 * visibleBounds.width;
|
||||||
|
const double centerY = visibleBounds.y + 0.5 * visibleBounds.height;
|
||||||
|
|
||||||
|
if (layout->getPageViewAt(round_cast<int>(centerX), round_cast<int>(centerY)) == this->view) {
|
||||||
|
// Pick the center of the visible area (converting from screen to page coordinates)
|
||||||
|
this->posx = (centerX - this->view->getX()) / zoom;
|
||||||
|
this->posy = (centerY - this->view->getY()) / zoom;
|
||||||
|
} else {
|
||||||
|
// No better location, so just center it on the page (possibly out of viewport)
|
||||||
|
this->posx = this->page->getWidth() / 2;
|
||||||
|
this->posy = this->page->getHeight() / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatexController::showTexEditDialog(std::unique_ptr<LatexController> ctrl) {
|
||||||
|
LatexController* texCtrl = ctrl.get();
|
||||||
|
if (ctrl->settings.useExternalEditor) {
|
||||||
|
xoj::popup::PopupWindowWrapper<ExtEdLatexDialog> popup(texCtrl->control->getGladeSearchPath(), std::move(ctrl));
|
||||||
|
|
||||||
|
popup.show(GTK_WINDOW(texCtrl->control->getWindow()->getWindow()));
|
||||||
|
} else {
|
||||||
|
xoj::popup::PopupWindowWrapper<IntEdLatexDialog> popup(texCtrl->control->getGladeSearchPath(), std::move(ctrl));
|
||||||
|
|
||||||
|
popup.show(GTK_WINDOW(texCtrl->control->getWindow()->getWindow()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatexController::triggerImageUpdate(const string& texString) {
|
||||||
|
if (isUpdating()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color textColor = control->getToolHandler()->getTool(TOOL_TEXT).getColor();
|
||||||
|
|
||||||
|
// Determine a background color that has enough contrast with the text color:
|
||||||
|
if (Util::get_color_contrast(textColor, LIGHT_PREVIEW_BACKGROUND) > 0.5) {
|
||||||
|
dlg->setPreviewBackgroundColor(LIGHT_PREVIEW_BACKGROUND);
|
||||||
|
} else {
|
||||||
|
dlg->setPreviewBackgroundColor(DARK_PREVIEW_BACKGROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPreviewedTex = texString;
|
||||||
|
const std::string texContents = LatexGenerator::templateSub(texString, latexTemplate, textColor);
|
||||||
|
auto result = generator.asyncRun(texTmpDir, texContents);
|
||||||
|
if (auto* err = std::get_if<LatexGenerator::GenError>(&result)) {
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), err->message);
|
||||||
|
} else if (auto** proc = std::get_if<GSubprocess*>(&result)) {
|
||||||
|
// Render the TeX and capture the process' output.
|
||||||
|
updating_cancellable = g_cancellable_new();
|
||||||
|
char* stdinBuff = nullptr; // No stdin
|
||||||
|
|
||||||
|
g_subprocess_communicate_utf8_async(*proc, stdinBuff, updating_cancellable,
|
||||||
|
reinterpret_cast<GAsyncReadyCallback>(onPdfRenderComplete), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text-changed handler: when the Buffer in the dialog changes, this handler
|
||||||
|
* removes the previous existing render and creates a new one. We need to do it
|
||||||
|
* through 'self' because signal handlers cannot directly access non-static
|
||||||
|
* methods and non-static fields such as 'dlg' so we need to wrap all the dlg
|
||||||
|
* method inside small methods in 'self'. To improve performance, we render the
|
||||||
|
* text asynchronously.
|
||||||
|
*/
|
||||||
|
void LatexController::handleTexChanged(LatexController* self) {
|
||||||
|
self->triggerImageUpdate(self->dlg->getBufferContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatexController::onPdfRenderComplete(GObject* procObj, GAsyncResult* res, LatexController* self) {
|
||||||
|
GError* err = nullptr;
|
||||||
|
bool procExited = false;
|
||||||
|
GSubprocess* proc = G_SUBPROCESS(procObj);
|
||||||
|
|
||||||
|
// Extract the process' output and store it.
|
||||||
|
{
|
||||||
|
char* procStdout_ptr = nullptr;
|
||||||
|
|
||||||
|
// Stdout and stderr should be merged.
|
||||||
|
procExited = g_subprocess_communicate_utf8_finish(proc, res, &procStdout_ptr, nullptr, &err);
|
||||||
|
|
||||||
|
// If we have stdout, store it.
|
||||||
|
if (procStdout_ptr != nullptr) {
|
||||||
|
self->texProcessOutput = procStdout_ptr;
|
||||||
|
free(procStdout_ptr);
|
||||||
|
} else {
|
||||||
|
g_warning("latex command: no stdout stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err != nullptr) {
|
||||||
|
if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
||||||
|
// the render was canceled
|
||||||
|
g_error_free(err);
|
||||||
|
return;
|
||||||
|
} else if (!g_error_matches(err, G_SPAWN_EXIT_ERROR, 1)) {
|
||||||
|
// The error was not caused by invalid LaTeX.
|
||||||
|
string message =
|
||||||
|
FS(_F("Latex generation encountered an error: {1} (exit code: {2})") % err->message % err->code);
|
||||||
|
XojMsgBox::showErrorToUser(self->control->getGtkWindow(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
self->isValidTex = false;
|
||||||
|
g_error_free(err);
|
||||||
|
} else if (procExited && g_subprocess_get_exit_status(proc) != 0) {
|
||||||
|
// Command exited with non-zero exit status.
|
||||||
|
|
||||||
|
self->isValidTex = false;
|
||||||
|
} else {
|
||||||
|
self->isValidTex = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the PDF if the TeX is invalid.
|
||||||
|
if (!self->isValidTex) {
|
||||||
|
fs::path pdfPath = self->texTmpDir / "tex.pdf";
|
||||||
|
fs::remove(pdfPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string currentTex = self->dlg->getBufferContents();
|
||||||
|
bool shouldUpdate = self->lastPreviewedTex != currentTex;
|
||||||
|
if (self->isValidTex) {
|
||||||
|
self->temporaryRender = self->loadRendered(currentTex);
|
||||||
|
if (self->temporaryRender != nullptr) {
|
||||||
|
self->dlg->setTempRender(self->temporaryRender->getPdf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_clear_object(&self->updating_cancellable);
|
||||||
|
g_clear_object(&proc);
|
||||||
|
|
||||||
|
self->updateStatus();
|
||||||
|
|
||||||
|
// If dlg is an ExtEdLatexDialog and the user has the auto-confirm option set, the dialog might
|
||||||
|
// have closed itself in the above updateStatus call, causing self->dlg to be nullptr.
|
||||||
|
if (shouldUpdate && self->dlg) {
|
||||||
|
self->triggerImageUpdate(currentTex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LatexController::isUpdating() { return updating_cancellable; }
|
||||||
|
|
||||||
|
void LatexController::updateStatus() { this->dlg->setCompilationStatus(isValidTex, !isUpdating(), texProcessOutput); }
|
||||||
|
|
||||||
|
void LatexController::deleteOldImage() {
|
||||||
|
if (this->selectedElem) {
|
||||||
|
auto sel = SelectionFactory::createFromElementOnActiveLayer(control, page, view, selectedElem);
|
||||||
|
this->view->getXournal()->deleteSelection(sel.release());
|
||||||
|
this->selectedElem = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LatexController::loadRendered(string renderedTex) -> std::unique_ptr<TexImage> {
|
||||||
|
if (!this->isValidTex) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path pdfPath = texTmpDir / "tex.pdf";
|
||||||
|
auto contents = Util::readString(pdfPath, true, std::ios::binary);
|
||||||
|
if (!contents) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto img = std::make_unique<TexImage>();
|
||||||
|
GError* err{};
|
||||||
|
bool loaded = img->loadData(std::move(*contents), &err);
|
||||||
|
|
||||||
|
if (err != nullptr) {
|
||||||
|
string message = FS(_F("Could not load LaTeX PDF file: {1}") % err->message);
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), message);
|
||||||
|
g_error_free(err);
|
||||||
|
return nullptr;
|
||||||
|
} else if (!loaded || !img->getPdf()) {
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), FS(_F("Could not load LaTeX PDF file")));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
img->setX(posx);
|
||||||
|
img->setY(posy);
|
||||||
|
img->setText(std::move(renderedTex));
|
||||||
|
if (std::abs(imgheight) > 1024 * std::numeric_limits<double>::epsilon()) {
|
||||||
|
double ratio = img->getElementWidth() / img->getElementHeight();
|
||||||
|
if (ratio == 0) {
|
||||||
|
img->setWidth(imgwidth == 0 ? 10 : imgwidth);
|
||||||
|
} else {
|
||||||
|
img->setWidth(imgheight * ratio);
|
||||||
|
}
|
||||||
|
img->setHeight(imgheight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatexController::insertTexImage() {
|
||||||
|
xoj_assert(this->isValidTex);
|
||||||
|
xoj_assert(this->temporaryRender != nullptr);
|
||||||
|
|
||||||
|
this->control->clearSelectionEndText();
|
||||||
|
this->deleteOldImage();
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(
|
||||||
|
std::make_unique<InsertUndoAction>(page, layer, this->temporaryRender.get()));
|
||||||
|
|
||||||
|
// Select element
|
||||||
|
auto selection =
|
||||||
|
SelectionFactory::createFromFloatingElement(control, page, layer, view, std::move(this->temporaryRender));
|
||||||
|
view->getXournal()->setSelection(selection.release());
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatexController::cancelEditing() {
|
||||||
|
// The original element is currently selected. This drops it back onto the page
|
||||||
|
this->control->clearSelectionEndText();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LatexController::run(Control* ctrl) {
|
||||||
|
auto self = std::make_unique<LatexController>(ctrl);
|
||||||
|
auto depStatus = self->findTexDependencies();
|
||||||
|
if (!depStatus.success) {
|
||||||
|
XojMsgBox::showErrorToUser(ctrl->getGtkWindow(), depStatus.errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->findSelectedTexElement();
|
||||||
|
showTexEditDialog(std::move(self));
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Controller for Latex stuff
|
||||||
|
*
|
||||||
|
* @author W Brenna
|
||||||
|
* http://wbrenna.ca
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <gio/gio.h> // for GAsyncResult, GCancellable
|
||||||
|
#include <gtk/gtk.h> // for GtkTextBuffer
|
||||||
|
#include <poppler.h> // for GObject
|
||||||
|
|
||||||
|
#include "control/latex/LatexGenerator.h" // for LatexGenerator
|
||||||
|
#include "gui/dialog/AbstractLatexDialog.h"
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
class TexImage;
|
||||||
|
class Document;
|
||||||
|
class XojPageView;
|
||||||
|
class Layer;
|
||||||
|
class Element;
|
||||||
|
class LatexSettings;
|
||||||
|
class IntEdLatexDialog;
|
||||||
|
|
||||||
|
class LatexController final {
|
||||||
|
public:
|
||||||
|
LatexController() = delete;
|
||||||
|
LatexController(const LatexController& other) = delete;
|
||||||
|
LatexController& operator=(const LatexController& other) = delete;
|
||||||
|
LatexController(Control* control);
|
||||||
|
~LatexController();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Open a LatexDialog, wait for the user to provide the LaTeX formula, and
|
||||||
|
* insert the rendered formula into the document if the supplied LaTeX is
|
||||||
|
* valid.
|
||||||
|
*/
|
||||||
|
static void run(Control* ctrl);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Provides information about whether a particular dependency was found or not.
|
||||||
|
*/
|
||||||
|
class FindDependencyStatus {
|
||||||
|
public:
|
||||||
|
FindDependencyStatus(bool success, std::string errorMsg): success(success), errorMsg(std::move(errorMsg)) {};
|
||||||
|
bool success;
|
||||||
|
std::string errorMsg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the required LaTeX files, returning false if at least one of them is not found.
|
||||||
|
*/
|
||||||
|
LatexController::FindDependencyStatus findTexDependencies();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a selected tex element, and load it
|
||||||
|
*/
|
||||||
|
void findSelectedTexElement();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a previous image/text is selected, delete it
|
||||||
|
*/
|
||||||
|
void deleteOldImage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously runs the LaTeX command and then updates the TeX image with
|
||||||
|
* the given LaTeX string. If the preview is already being updated, then
|
||||||
|
* this method will be a no-op.
|
||||||
|
*/
|
||||||
|
void triggerImageUpdate(const std::string& texString);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the LaTex Editor dialog and process its output
|
||||||
|
*/
|
||||||
|
static void showTexEditDialog(std::unique_ptr<LatexController> texCtrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal handler, updates the rendered image when the text in the editor
|
||||||
|
* changes.
|
||||||
|
*/
|
||||||
|
static void handleTexChanged(LatexController* self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the display once the PDF file is generated.
|
||||||
|
*
|
||||||
|
* If the Latex text has changed since the last update, triggerPreviewUpdate
|
||||||
|
* will be called again.
|
||||||
|
*/
|
||||||
|
static void onPdfRenderComplete(GObject* procObj, GAsyncResult* res, LatexController* self);
|
||||||
|
|
||||||
|
void updateStatus();
|
||||||
|
bool isUpdating();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the preview PDF from disk and create a TexImage object.
|
||||||
|
*/
|
||||||
|
std::unique_ptr<TexImage> loadRendered(std::string renderedTex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert the generated preview TexImage into the current page.
|
||||||
|
*/
|
||||||
|
void insertTexImage();
|
||||||
|
|
||||||
|
/// Cancels the current edition: puts the original Tex element back in place (does nothing if there was none)
|
||||||
|
void cancelEditing();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Control* control = nullptr;
|
||||||
|
LatexSettings const& settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contents of the latex template, loaded from disk.
|
||||||
|
*/
|
||||||
|
std::string latexTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LaTex editor dialog
|
||||||
|
*/
|
||||||
|
AbstractLatexDialog* dlg = nullptr;
|
||||||
|
friend class AbstractLatexDialog;
|
||||||
|
friend class ExtEdLatexDialog;
|
||||||
|
friend class IntEdLatexDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tex binary full path
|
||||||
|
*/
|
||||||
|
fs::path pdflatexPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original TeX string when the dialog was opened, or the empty string
|
||||||
|
* if creating a new LaTeX element.
|
||||||
|
*/
|
||||||
|
std::string initialTex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last TeX string shown in the preview.
|
||||||
|
*/
|
||||||
|
std::string lastPreviewedTex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a preview is currently being generated.
|
||||||
|
*/
|
||||||
|
GCancellable* updating_cancellable = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The output of the last run of the
|
||||||
|
* TeX command.
|
||||||
|
*/
|
||||||
|
std::string texProcessOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the current TeX string is valid.
|
||||||
|
*/
|
||||||
|
bool isValidTex = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X-Position
|
||||||
|
*/
|
||||||
|
double posx = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Y-Position
|
||||||
|
*/
|
||||||
|
double posy = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image width
|
||||||
|
*/
|
||||||
|
double imgwidth = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image height
|
||||||
|
*/
|
||||||
|
double imgheight = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document
|
||||||
|
*/
|
||||||
|
Document* doc = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page View
|
||||||
|
*/
|
||||||
|
XojPageView* view = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected Page
|
||||||
|
*/
|
||||||
|
PageRef page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected layer
|
||||||
|
*/
|
||||||
|
Layer* layer = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The directory in which the LaTeX files will be generated. Note that this
|
||||||
|
* should be within a system temporary directory.
|
||||||
|
*/
|
||||||
|
fs::path texTmpDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element that is currently being edited.
|
||||||
|
*/
|
||||||
|
const Element* selectedElem = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The controller owns the rendered preview in order to be able to delete it
|
||||||
|
* when a new render is created
|
||||||
|
*/
|
||||||
|
std::unique_ptr<TexImage> temporaryRender;
|
||||||
|
|
||||||
|
LatexGenerator generator;
|
||||||
|
};
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
#include "PageBackgroundChangeController.h"
|
||||||
|
|
||||||
|
#include <memory> // for __shared_ptr...
|
||||||
|
#include <string> // for allocator
|
||||||
|
#include <utility> // for move
|
||||||
|
|
||||||
|
#include <gdk-pixbuf/gdk-pixbuf.h> // for gdk_pixbuf_g...
|
||||||
|
#include <gio/gio.h> // for GFile
|
||||||
|
#include <glib.h> // for g_error_free
|
||||||
|
|
||||||
|
#include "control/settings/PageTemplateSettings.h" // for PageTemplate...
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "gui/dialog/backgroundSelect/ImagesDialog.h" // for ImagesDialog
|
||||||
|
#include "gui/dialog/backgroundSelect/PdfPagesDialog.h" // for PdfPagesDialog
|
||||||
|
#include "gui/menus/menubar/Menubar.h" // for Menubar
|
||||||
|
#include "gui/menus/menubar/PageTypeSubmenu.h" // for PageTypeSubmenu
|
||||||
|
#include "model/BackgroundImage.h" // for BackgroundImage
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/PageType.h" // for PageType
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfPageSPtr
|
||||||
|
#include "undo/GroupUndoAction.h" // for GroupUndoAction
|
||||||
|
#include "undo/MissingPdfUndoAction.h" // for MissingPdfUn...
|
||||||
|
#include "undo/PageBackgroundChangedUndoAction.h" // for PageBackgrou...
|
||||||
|
#include "undo/UndoAction.h" // for UndoAction
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler
|
||||||
|
#include "util/Assert.h" // for xoj_assert
|
||||||
|
#include "util/PathUtil.h" // for fromGFile
|
||||||
|
#include "util/PopupWindowWrapper.h"
|
||||||
|
#include "util/Util.h" // for npos
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for FS, _, _F
|
||||||
|
|
||||||
|
#include "Control.h" // for Control
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
|
||||||
|
PageBackgroundChangeController::PageBackgroundChangeController(Control* control):
|
||||||
|
control(control), pageTypeForNewPages(control->getSettings()->getPageTemplateSettings().getPageInsertType()) {
|
||||||
|
registerListener(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::applyBackgroundToAllPages(const PageType& pt) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto apply = [pt, this](CommitParameter param = std::nullopt) {
|
||||||
|
auto groupUndoAction = std::make_unique<GroupUndoAction>();
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
doc->lock();
|
||||||
|
auto nbPages = doc->getPageCount();
|
||||||
|
for (size_t p = 0; p < nbPages; p++) {
|
||||||
|
auto undoAction = commitPageTypeChange(p, pt, param);
|
||||||
|
if (undoAction) {
|
||||||
|
groupUndoAction->addAction(std::move(undoAction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc->unlock();
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::move(groupUndoAction));
|
||||||
|
|
||||||
|
// Special background types may alter the page sizes as well
|
||||||
|
auto fire = pt.isSpecial() ? &Control::firePageSizeChanged : &Control::firePageChanged;
|
||||||
|
for (size_t n = 0; n < nbPages; n++) {
|
||||||
|
(control->*fire)(n);
|
||||||
|
}
|
||||||
|
control->updateBackgroundSizeButton();
|
||||||
|
control->getWindow()->getMenubar()->getPageTypeSubmenu().setSelectedPT(std::move(pt));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pt.isImagePage()) {
|
||||||
|
askForImageBackground([apply = std::move(apply)](BackgroundImage img) { apply(std::move(img)); });
|
||||||
|
} else if (pt.isPdfPage()) {
|
||||||
|
askForPdfBackground([apply = std::move(apply)](size_t pdfPage) { apply(pdfPage); });
|
||||||
|
} else {
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::applyPageSizeToAllPages(const PaperSize& paperSize) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
auto groupUndoAction = std::make_unique<GroupUndoAction>();
|
||||||
|
|
||||||
|
for (size_t p = 0; p < doc->getPageCount(); p++) {
|
||||||
|
auto undoAction = commitPageSizeChange(p, paperSize);
|
||||||
|
if (undoAction) {
|
||||||
|
groupUndoAction->addAction(std::move(undoAction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::move(groupUndoAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::applyCurrentPageBackgroundToAll() {
|
||||||
|
PageType pt = control->getCurrentPage()->getBackgroundType();
|
||||||
|
applyBackgroundToAllPages(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::changePdfPagesBackground(const fs::path& filepath, bool attachPdf) {
|
||||||
|
Document* doc = this->control->getDocument();
|
||||||
|
|
||||||
|
const fs::path oldFilepath = doc->getPdfFilepath();
|
||||||
|
const bool oldAttachPdf = doc->isAttachPdf();
|
||||||
|
|
||||||
|
if (!doc->readPdf(filepath, false, attachPdf)) {
|
||||||
|
std::string msg = FS(_F("Error reading PDF: {1}") % doc->getLastErrorMsg());
|
||||||
|
XojMsgBox::showErrorToUser(this->control->getGtkWindow(), msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->control->getWindow()->getXournal()->recreatePdfCache();
|
||||||
|
|
||||||
|
this->control->fireDocumentChanged(DOCUMENT_CHANGE_COMPLETE);
|
||||||
|
|
||||||
|
auto undoAction = std::make_unique<MissingPdfUndoAction>(oldFilepath, oldAttachPdf);
|
||||||
|
this->control->getUndoRedoHandler()->addUndoAction(std::move(undoAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::changeCurrentPageBackground(const PageType& pt) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
const size_t pageNr = control->getCurrentPageNo();
|
||||||
|
xoj_assert(pageNr != npos);
|
||||||
|
|
||||||
|
auto apply = [this, pt, pageNr](CommitParameter param = std::nullopt) {
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
doc->lock();
|
||||||
|
auto undoAction = commitPageTypeChange(pageNr, pt, std::move(param));
|
||||||
|
doc->unlock();
|
||||||
|
if (undoAction) {
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::move(undoAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special background types may alter the page sizes as well
|
||||||
|
if (pt.isSpecial()) {
|
||||||
|
control->firePageSizeChanged(pageNr);
|
||||||
|
} else {
|
||||||
|
control->firePageChanged(pageNr);
|
||||||
|
}
|
||||||
|
control->updateBackgroundSizeButton();
|
||||||
|
control->getWindow()->getMenubar()->getPageTypeSubmenu().setSelectedPT(std::move(pt));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pt.isImagePage()) {
|
||||||
|
askForImageBackground([apply = std::move(apply)](BackgroundImage img) { apply(std::move(img)); });
|
||||||
|
} else if (pt.isPdfPage()) {
|
||||||
|
askForPdfBackground([apply = std::move(apply)](size_t pdfPage) { apply(pdfPage); });
|
||||||
|
} else {
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::changeCurrentPageSize(const PaperSize& pageSize) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto undoAction = commitPageSizeChange(control->getCurrentPageNo(), pageSize);
|
||||||
|
if (undoAction) {
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::move(undoAction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::setPageTypeForNewPages(const std::optional<PageType>& pt) {
|
||||||
|
this->pageTypeForNewPages = pt;
|
||||||
|
}
|
||||||
|
void PageBackgroundChangeController::setPaperSizeForNewPages(const std::optional<PaperSize>& ps) {
|
||||||
|
this->paperSizeForNewPages = ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setPageImageBackground(const PageRef& page, BackgroundImage img) {
|
||||||
|
page->setBackgroundImage(std::move(img));
|
||||||
|
page->setBackgroundType(PageType(PageTypeFormat::Image));
|
||||||
|
|
||||||
|
// Apply correct page size
|
||||||
|
GdkPixbuf* pixbuf = page->getBackgroundImage().getPixbuf();
|
||||||
|
if (pixbuf) {
|
||||||
|
page->setSize(gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf));
|
||||||
|
} else {
|
||||||
|
g_warning("PageBackgroundChangeController::setPageImageBackground(): Page with image background but nullptr "
|
||||||
|
"pixbuf");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setPagePdfBackground(const PageRef& page, size_t pdfPageNr, Document* doc) {
|
||||||
|
if (pdfPageNr < doc->getPdfPageCount()) {
|
||||||
|
// no need to set a type, if we set the page number the type is also set
|
||||||
|
page->setBackgroundPdfPageNr(pdfPageNr);
|
||||||
|
|
||||||
|
XojPdfPageSPtr p = doc->getPdfPage(pdfPageNr);
|
||||||
|
page->setSize(p->getWidth(), p->getHeight());
|
||||||
|
} else {
|
||||||
|
g_warning("PageBackgroundChangeController::setPagePdfBackground() with past-the-end PDF page number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PageBackgroundChangeController::commitPageTypeChange(size_t pageNum, const PageType& pageType,
|
||||||
|
CommitParameter param) -> std::unique_ptr<UndoAction> {
|
||||||
|
xoj_assert(pageType.isImagePage() == std::holds_alternative<BackgroundImage>(param));
|
||||||
|
xoj_assert(pageType.isPdfPage() == std::holds_alternative<size_t>(param));
|
||||||
|
|
||||||
|
PageRef page = control->getDocument()->getPage(pageNum);
|
||||||
|
if (!page) {
|
||||||
|
g_warning("PageBackgroundChangeController::commitPageTypeChange() could not fetch page %zu", pageNum);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
// Get values for Undo / Redo
|
||||||
|
const double origW = page->getWidth();
|
||||||
|
const double origH = page->getHeight();
|
||||||
|
BackgroundImage origBackgroundImage = page->getBackgroundImage();
|
||||||
|
const size_t origPdfPage = page->getPdfPageNr();
|
||||||
|
PageType origType = page->getBackgroundType();
|
||||||
|
|
||||||
|
// Apply the new background
|
||||||
|
if (std::holds_alternative<BackgroundImage>(param)) { // Image background
|
||||||
|
xoj_assert(!std::get<BackgroundImage>(param).isEmpty());
|
||||||
|
setPageImageBackground(page, std::move(std::get<BackgroundImage>(param)));
|
||||||
|
} else if (std::holds_alternative<size_t>(param)) { // PDF background
|
||||||
|
setPagePdfBackground(page, std::get<size_t>(param), doc);
|
||||||
|
} else {
|
||||||
|
page->setBackgroundType(pageType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_unique<PageBackgroundChangedUndoAction>(page, origType, origPdfPage, origBackgroundImage, origW,
|
||||||
|
origH);
|
||||||
|
}
|
||||||
|
auto PageBackgroundChangeController::commitPageSizeChange(const size_t pageNum, const PaperSize& pageSize)
|
||||||
|
-> std::unique_ptr<UndoAction> {
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
doc->lock();
|
||||||
|
PageRef page = doc->getPage(pageNum);
|
||||||
|
if (!page) {
|
||||||
|
doc->unlock();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t pageNr = doc->indexOf(page);
|
||||||
|
xoj_assert(pageNr != npos);
|
||||||
|
|
||||||
|
// Get values for Undo / Redo
|
||||||
|
const double origW = page->getWidth();
|
||||||
|
const double origH = page->getHeight();
|
||||||
|
BackgroundImage origBackgroundImage = page->getBackgroundImage();
|
||||||
|
const size_t origPdfPage = page->getPdfPageNr();
|
||||||
|
PageType origType = page->getBackgroundType();
|
||||||
|
|
||||||
|
page->setSize(pageSize.width, pageSize.height);
|
||||||
|
doc->unlock();
|
||||||
|
|
||||||
|
control->firePageSizeChanged(pageNr);
|
||||||
|
return std::make_unique<PageBackgroundChangedUndoAction>(page, origType, origPdfPage, origBackgroundImage, origW,
|
||||||
|
origH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::askForImageBackground(std::function<void(BackgroundImage)> callback) {
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
auto dlg = xoj::popup::PopupWindowWrapper<ImagesDialog>(control->getGladeSearchPath(), doc, control->getSettings(),
|
||||||
|
std::move(callback));
|
||||||
|
dlg.show(control->getGtkWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::askForPdfBackground(std::function<void(size_t)> callback) {
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
if (doc->getPdfPageCount() == 0) {
|
||||||
|
std::string msg = _("You don't have any PDF pages to select from. Cancel operation.\n"
|
||||||
|
"Please select another background type: Menu \"Journal\" → \"Configure Page Template\".");
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dlg = xoj::popup::PopupWindowWrapper<PdfPagesDialog>(control->getGladeSearchPath(), doc,
|
||||||
|
control->getSettings(), std::move(callback));
|
||||||
|
dlg.show(control->getGtkWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the background from source to target
|
||||||
|
*/
|
||||||
|
void PageBackgroundChangeController::copyBackgroundFromOtherPage(PageRef target, PageRef source) {
|
||||||
|
// Copy page background type
|
||||||
|
PageType bg = source->getBackgroundType();
|
||||||
|
target->setBackgroundType(bg);
|
||||||
|
|
||||||
|
if (bg.isPdfPage()) {
|
||||||
|
// If PDF: Copy PDF Page and its size
|
||||||
|
target->setSize(source->getWidth(), source->getHeight());
|
||||||
|
target->setBackgroundPdfPageNr(source->getPdfPageNr());
|
||||||
|
} else if (bg.isImagePage()) {
|
||||||
|
// If Image: Copy the Image and its size
|
||||||
|
target->setSize(source->getWidth(), source->getHeight());
|
||||||
|
target->setBackgroundImage(source->getBackgroundImage());
|
||||||
|
} else {
|
||||||
|
// Copy the background color
|
||||||
|
target->setBackgroundColor(source->getBackgroundColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::insertNewPage(size_t position, bool automatedInsertion) {
|
||||||
|
if (automatedInsertion) {
|
||||||
|
if (pageTypeForNewPages && (pageTypeForNewPages->isImagePage() || pageTypeForNewPages->isPdfPage())) {
|
||||||
|
// Do not insert image or pdf pages automatically
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the user acted to get the page create, the selection should be dropped
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
}
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
if (position > doc->getPageCount()) {
|
||||||
|
position = doc->getPageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
PageRef current = control->getCurrentPage();
|
||||||
|
xoj_assert(current);
|
||||||
|
double width, height;
|
||||||
|
if (paperSizeForNewPages) {
|
||||||
|
width = paperSizeForNewPages->width;
|
||||||
|
height = paperSizeForNewPages->height;
|
||||||
|
} else {
|
||||||
|
width = current->getWidth();
|
||||||
|
height = current->getHeight();
|
||||||
|
}
|
||||||
|
auto page = std::make_shared<XojPage>(width, height);
|
||||||
|
|
||||||
|
auto afterConfigured = [position, shouldScrollToPage = !automatedInsertion, ctrl = this->control](PageRef page) {
|
||||||
|
ctrl->insertPage(page, position, shouldScrollToPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pageTypeForNewPages) {
|
||||||
|
copyBackgroundFromOtherPage(page, current);
|
||||||
|
afterConfigured(std::move(page));
|
||||||
|
} else if (pageTypeForNewPages->isImagePage()) {
|
||||||
|
askForImageBackground([after = std::move(afterConfigured), page = std::move(page)](BackgroundImage img) {
|
||||||
|
setPageImageBackground(page, std::move(img));
|
||||||
|
after(std::move(page));
|
||||||
|
});
|
||||||
|
} else if (pageTypeForNewPages->isPdfPage()) {
|
||||||
|
askForPdfBackground([after = std::move(afterConfigured), page = std::move(page), doc](size_t pdfPageNr) {
|
||||||
|
setPagePdfBackground(page, pdfPageNr, doc);
|
||||||
|
after(std::move(page));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create a new page from template
|
||||||
|
page->setBackgroundType(pageTypeForNewPages.value());
|
||||||
|
|
||||||
|
// Set background Color
|
||||||
|
PageTemplateSettings model;
|
||||||
|
model.parse(control->getSettings()->getPageTemplate());
|
||||||
|
page->setBackgroundColor(model.getBackgroundColor());
|
||||||
|
|
||||||
|
afterConfigured(std::move(page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::documentChanged(DocumentChangeType type) {}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::pageSizeChanged(size_t page) {}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::pageChanged(size_t page) {}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::pageInserted(size_t page) {}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::pageDeleted(size_t page) {}
|
||||||
|
|
||||||
|
void PageBackgroundChangeController::pageSelected(size_t page) {
|
||||||
|
auto const& current = control->getCurrentPage();
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
control->getWindow()->getMenubar()->getPageTypeSubmenu().setSelectedPT(current->getBackgroundType());
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Handle page background change and all other PageBackground stuff
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <functional>
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <optional>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include "model/BackgroundImage.h"
|
||||||
|
#include "model/DocumentChangeType.h" // for DocumentChangeType
|
||||||
|
#include "model/DocumentListener.h" // for DocumentListener
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "model/PageType.h" // for PageType
|
||||||
|
#include "model/PaperSize.h"
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
class UndoAction;
|
||||||
|
|
||||||
|
class PageBackgroundChangeController: public DocumentListener {
|
||||||
|
public:
|
||||||
|
PageBackgroundChangeController(Control* control);
|
||||||
|
~PageBackgroundChangeController() override = default;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void changeCurrentPageBackground(const PageType& pageType);
|
||||||
|
void changeCurrentPageSize(const PaperSize& pageSize);
|
||||||
|
/**
|
||||||
|
* @brief (Un)set the page type for newly created pages
|
||||||
|
* @param pageType The new page type.
|
||||||
|
* Passing std::nullopt will unset the page type.
|
||||||
|
* If the page type is not set, newly created pages will have the same type as the current page.
|
||||||
|
*/
|
||||||
|
void setPageTypeForNewPages(const std::optional<PageType>& pageType);
|
||||||
|
/**
|
||||||
|
* @brief (Un)set the paper size for newly created pages
|
||||||
|
* @param paperSize The new paper size.
|
||||||
|
* Passing std::nullopt will unset the paper size.
|
||||||
|
* If the page size is not set, newly created pages will have the same format as the current page.
|
||||||
|
*/
|
||||||
|
void setPaperSizeForNewPages(const std::optional<PaperSize>& paperSize);
|
||||||
|
void applyCurrentPageBackgroundToAll();
|
||||||
|
void applyBackgroundToAllPages(const PageType& pt);
|
||||||
|
void applyPageSizeToAllPages(const PaperSize& paperSize);
|
||||||
|
void changePdfPagesBackground(const fs::path& filepath, bool attachPdf);
|
||||||
|
void insertNewPage(size_t position, bool automatedInsertion = false);
|
||||||
|
|
||||||
|
// DocumentListener
|
||||||
|
public:
|
||||||
|
void documentChanged(DocumentChangeType type) override;
|
||||||
|
void pageSizeChanged(size_t page) override;
|
||||||
|
void pageChanged(size_t page) override;
|
||||||
|
void pageInserted(size_t page) override;
|
||||||
|
void pageDeleted(size_t page) override;
|
||||||
|
void pageSelected(size_t page) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Copy the background from source to target
|
||||||
|
*/
|
||||||
|
static void copyBackgroundFromOtherPage(PageRef target, PageRef source);
|
||||||
|
|
||||||
|
/// Asks the user to choose a pdf page, and calls callback
|
||||||
|
void askForPdfBackground(std::function<void(size_t)> callback);
|
||||||
|
|
||||||
|
/// Asks the user to choose an image background, and calls callback
|
||||||
|
void askForImageBackground(std::function<void(BackgroundImage)> callback);
|
||||||
|
|
||||||
|
/// Nothing, a BackgroundImage or the number of a pdf page
|
||||||
|
using CommitParameter = std::variant<std::nullopt_t, BackgroundImage, size_t>;
|
||||||
|
/**
|
||||||
|
* Applies pageType to page number pageNum.
|
||||||
|
* If pageType.isImage(), a BackgroundImage must be provided
|
||||||
|
* If pageType.isPdf(), a pdf page number must be provided in the std::variant
|
||||||
|
*/
|
||||||
|
auto commitPageTypeChange(size_t pageNum, const PageType& pageType, CommitParameter param = std::nullopt)
|
||||||
|
-> std::unique_ptr<UndoAction>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform page size change.
|
||||||
|
* @param pageNum Index of the page whose size will be changed
|
||||||
|
* @param pageSize New page size
|
||||||
|
* @return Undo action of the page size change
|
||||||
|
*/
|
||||||
|
auto commitPageSizeChange(size_t pageNum, const PaperSize& pageSize) -> std::unique_ptr<UndoAction>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Control* control = nullptr;
|
||||||
|
std::optional<PageType> pageTypeForNewPages;
|
||||||
|
std::optional<PaperSize> paperSizeForNewPages;
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#include "PdfCache.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for max
|
||||||
|
#include <cmath> // for ceil, abs
|
||||||
|
#include <cstdio> // for size_t
|
||||||
|
#include <memory> // for shared_ptr, __shared_ptr_access
|
||||||
|
#include <string> // for string
|
||||||
|
#include <utility> // for move
|
||||||
|
|
||||||
|
#include <glib.h> // for g_warning
|
||||||
|
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "pdf/base/XojPdfDocument.h" // for XojPdfDocument
|
||||||
|
#include "util/Assert.h" // for xoj_assert
|
||||||
|
#include "util/Range.h" // for Range
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
#include "util/safe_casts.h" // for as_unsigned
|
||||||
|
#include "view/Mask.h" // for Mask
|
||||||
|
|
||||||
|
class PdfCacheEntry {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Cache [img], the result of rendering [popplerPage] with
|
||||||
|
* the given [zoom].
|
||||||
|
* A change in the document's zoom causes a change in the
|
||||||
|
* quality of the PDF backgrounds (zoomed in => need a higher
|
||||||
|
* quality rendering).
|
||||||
|
*
|
||||||
|
* @param popplerPage
|
||||||
|
* @param buffer is the result of rendering popplerPage
|
||||||
|
*/
|
||||||
|
PdfCacheEntry(XojPdfPageSPtr popplerPage, xoj::view::Mask&& buffer):
|
||||||
|
popplerPage(std::move(popplerPage)), buffer(std::forward<xoj::view::Mask>(buffer)) {}
|
||||||
|
|
||||||
|
~PdfCacheEntry() = default;
|
||||||
|
|
||||||
|
XojPdfPageSPtr popplerPage;
|
||||||
|
xoj::view::Mask buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
static double getPercentZoomChange(double oldZoom, double newZoom) {
|
||||||
|
double averagedZoom = (oldZoom + newZoom) / 2.0;
|
||||||
|
return std::abs(oldZoom - newZoom) * 100.0 / averagedZoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfCache::PdfCache(const XojPdfDocument& doc, Settings* settings): pdfDocument(doc) { updateSettings(settings); }
|
||||||
|
|
||||||
|
PdfCache::~PdfCache() = default;
|
||||||
|
|
||||||
|
void PdfCache::setRefreshThreshold(double threshold) { this->zoomRefreshThreshold = threshold; }
|
||||||
|
|
||||||
|
void PdfCache::setMaxSize(size_t newSize) {
|
||||||
|
this->maxSize = newSize;
|
||||||
|
if (this->data.size() > this->maxSize) {
|
||||||
|
this->data.resize(this->maxSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PdfCache::updateSettings(Settings* settings) {
|
||||||
|
if (settings) {
|
||||||
|
setMaxSize(as_unsigned(settings->getPdfPageCacheSize()));
|
||||||
|
setRefreshThreshold(settings->getPDFPageRerenderThreshold());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PdfCache::evictAllExcept(const std::unordered_set<size_t>& retainedPdfPages) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->renderMutex);
|
||||||
|
|
||||||
|
for (auto& entry: this->data) {
|
||||||
|
xoj_assert(entry);
|
||||||
|
xoj_assert(entry->popplerPage);
|
||||||
|
|
||||||
|
const size_t pdfPageNo = static_cast<size_t>(entry->popplerPage->getPageId());
|
||||||
|
if (retainedPdfPages.find(pdfPageNo) == retainedPdfPages.end()) {
|
||||||
|
entry.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->data.erase(std::remove(this->data.begin(), this->data.end(), nullptr), this->data.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PdfCache::lookup(size_t pdfPageNo) const -> const PdfCacheEntry* {
|
||||||
|
for (auto& e: this->data) {
|
||||||
|
if (static_cast<size_t>(e->popplerPage->getPageId()) == pdfPageNo) {
|
||||||
|
return e.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PdfCache::cache(XojPdfPageSPtr popplerPage, xoj::view::Mask&& buffer) -> const PdfCacheEntry* {
|
||||||
|
xoj_assert(this->maxSize > 0);
|
||||||
|
xoj_assert(popplerPage);
|
||||||
|
const auto pageId = popplerPage->getPageId();
|
||||||
|
|
||||||
|
auto existingIt = std::find_if(this->data.begin(), this->data.end(),
|
||||||
|
[pageId](const auto& entry) { return entry->popplerPage->getPageId() == pageId; });
|
||||||
|
if (existingIt != this->data.end()) {
|
||||||
|
this->data.erase(existingIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->data.size() >= this->maxSize) {
|
||||||
|
this->data.resize(this->maxSize - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->data.emplace_front(
|
||||||
|
std::make_unique<PdfCacheEntry>(std::move(popplerPage), std::forward<xoj::view::Mask>(buffer)));
|
||||||
|
|
||||||
|
return this->data.front().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PdfCache::render(cairo_t* cr, size_t pdfPageNo, double zoom, double pageWidth, double pageHeight) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->renderMutex);
|
||||||
|
|
||||||
|
const PdfCacheEntry* cacheResult = lookup(pdfPageNo);
|
||||||
|
|
||||||
|
bool needsRefresh = cacheResult == nullptr;
|
||||||
|
|
||||||
|
if (!needsRefresh) {
|
||||||
|
// If we do have a cached result, is its rendering quality
|
||||||
|
// acceptable for our current zoom?
|
||||||
|
needsRefresh =
|
||||||
|
(zoom > 1.0 && getPercentZoomChange(cacheResult->buffer.getZoom(), zoom) > this->zoomRefreshThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRefresh) {
|
||||||
|
double renderZoom = std::max(zoom, 1.0);
|
||||||
|
|
||||||
|
auto popplerPage = cacheResult ? cacheResult->popplerPage : pdfDocument.getPage(pdfPageNo);
|
||||||
|
|
||||||
|
if (!popplerPage) {
|
||||||
|
g_warning("PdfCache::render Could not get the pdf page %zu from the document", pdfPageNo);
|
||||||
|
renderMissingPdfPage(cr, pageWidth, pageHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xoj::view::Mask buffer(cairo_get_target(cr), Range(0, 0, popplerPage->getWidth(), popplerPage->getHeight()),
|
||||||
|
renderZoom, CAIRO_CONTENT_COLOR_ALPHA);
|
||||||
|
popplerPage->render(buffer.get());
|
||||||
|
if (this->maxSize == 0) {
|
||||||
|
buffer.paintTo(cr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheResult = cache(popplerPage, std::move(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheResult->buffer.paintTo(cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PdfCache::renderMissingPdfPage(cairo_t* cr, double pageWidth, double pageHeight) {
|
||||||
|
cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
|
||||||
|
cairo_set_font_size(cr, 26);
|
||||||
|
|
||||||
|
cairo_set_source_rgb(cr, 0.8, 0.8, 0.8);
|
||||||
|
|
||||||
|
cairo_text_extents_t extents = {0};
|
||||||
|
std::string strMissing = _("PDF background missing");
|
||||||
|
|
||||||
|
cairo_text_extents(cr, strMissing.c_str(), &extents);
|
||||||
|
cairo_move_to(cr, pageWidth / 2 - extents.width / 2, pageHeight / 2 - extents.height / 2);
|
||||||
|
cairo_text_path(cr, strMissing.c_str());
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Caches PDF backgrounds for faster repaint
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <deque> // for deque
|
||||||
|
#include <mutex> // for mutex
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_t, cairo_surface_t
|
||||||
|
|
||||||
|
#include "pdf/base/XojPdfDocument.h" // for XojPdfDocument
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfPageSPtr
|
||||||
|
|
||||||
|
namespace xoj::view {
|
||||||
|
class Mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PdfCacheEntry;
|
||||||
|
class Settings;
|
||||||
|
|
||||||
|
class PdfCache {
|
||||||
|
public:
|
||||||
|
PdfCache(const XojPdfDocument& doc, Settings* settings);
|
||||||
|
virtual ~PdfCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
PdfCache(const PdfCache& cache);
|
||||||
|
void operator=(const PdfCache& cache);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Render the page with number pdfPageNo of the pdf document to the cairo context
|
||||||
|
* @param cr the cairo context
|
||||||
|
* @param pdfPageNo The page number (in the pdf document)
|
||||||
|
* @param zoom The current zoom level
|
||||||
|
* @param pageWidth/pageHeight Xournal++ page dimensions
|
||||||
|
*/
|
||||||
|
void render(cairo_t* cr, size_t pdfPageNo, double zoom, double pageWidth, double pageHeight);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Set the maximum tolerable zoom difference, as a percentage.
|
||||||
|
*
|
||||||
|
* @param threshold is the minimum percent-difference between the zoom value at
|
||||||
|
* which the cached version of the page was rendered and the current zoom,
|
||||||
|
* for which the page will be re-cached while zooming.
|
||||||
|
*/
|
||||||
|
void setRefreshThreshold(double percentDifference);
|
||||||
|
|
||||||
|
void setMaxSize(size_t newSize);
|
||||||
|
|
||||||
|
void updateSettings(Settings* settings);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove cached renderings for PDF pages that are not retained.
|
||||||
|
*/
|
||||||
|
void evictAllExcept(const std::unordered_set<size_t>& retainedPdfPages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Renders an error background, for when the pdf page cannot be rendered
|
||||||
|
*/
|
||||||
|
static void renderMissingPdfPage(cairo_t* cr, double pageWidth, double pageHeight);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Look up for a cache entry for the page with number pdfPgeNo in the PDF.
|
||||||
|
*/
|
||||||
|
const PdfCacheEntry* lookup(size_t pdfPageNo) const;
|
||||||
|
/**
|
||||||
|
* @brief Push a cache entry
|
||||||
|
*/
|
||||||
|
const PdfCacheEntry* cache(XojPdfPageSPtr popplerPage, xoj::view::Mask&& buffer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
XojPdfDocument pdfDocument;
|
||||||
|
|
||||||
|
std::mutex renderMutex;
|
||||||
|
|
||||||
|
std::deque<std::unique_ptr<PdfCacheEntry>> data;
|
||||||
|
decltype(data)::size_type maxSize = 0;
|
||||||
|
|
||||||
|
double zoomRefreshThreshold;
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#include "PrintHandler.h"
|
||||||
|
|
||||||
|
#include <cmath> // for M_PI_2
|
||||||
|
#include <memory> // for __shared_ptr_access
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_rotate, cairo_translate, cairo_t
|
||||||
|
#include <config-dev.h> // for PRINT_CONFIG_FILE
|
||||||
|
#include <glib-object.h> // for g_object_unref, G_CALLBACK, g_signa...
|
||||||
|
#include <glib.h> // for GError, g_error_free, g_warning
|
||||||
|
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "model/PageType.h" // for PageType
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfPageSPtr, XojPdfPage
|
||||||
|
#include "util/Assert.h" // for xoj_assert
|
||||||
|
#include "util/PathUtil.h" // for getConfigFile
|
||||||
|
#include "util/StringUtils.h" // for char_cast
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
#include "util/safe_casts.h" // for strict_cast
|
||||||
|
#include "view/DocumentView.h" // for DocumentView
|
||||||
|
|
||||||
|
#include "filesystem.h" // for exists, remove, path
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void drawPage(GtkPrintOperation* /*operation*/, GtkPrintContext* context, int pageNr, Document* doc) {
|
||||||
|
cairo_t* cr = gtk_print_context_get_cairo_context(context);
|
||||||
|
|
||||||
|
PageRef page = doc->getPage(static_cast<size_t>(pageNr));
|
||||||
|
if (!page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double width = page->getWidth();
|
||||||
|
double height = page->getHeight();
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
cairo_rotate(cr, M_PI_2);
|
||||||
|
cairo_translate(cr, 0, -height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For better quality printing, we use a dedicated pdf-renderer in this case
|
||||||
|
if (page->getBackgroundType().isPdfPage()) {
|
||||||
|
XojPdfPageSPtr popplerPage = doc->getPdfPage(page->getPdfPageNr());
|
||||||
|
if (popplerPage) {
|
||||||
|
popplerPage->renderForPrinting(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xoj::view::BackgroundFlags flags = xoj::view::BACKGROUND_SHOW_ALL;
|
||||||
|
flags.showPDF = xoj::view::HIDE_PDF_BACKGROUND; // Already printed (if any)
|
||||||
|
|
||||||
|
DocumentView view;
|
||||||
|
view.drawPage(page, cr, true /* dont render eraseable */, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestPageSetup(GtkPrintOperation* /*op*/, GtkPrintContext* /*ctx*/, int pageNr, GtkPageSetup* setup,
|
||||||
|
Document* doc) {
|
||||||
|
PageRef page = doc->getPage(static_cast<size_t>(pageNr)); // Can't be negative
|
||||||
|
if (!page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double width = page->getWidth();
|
||||||
|
double height = page->getHeight();
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
gtk_page_setup_set_orientation(setup, GTK_PAGE_ORIENTATION_LANDSCAPE);
|
||||||
|
} else {
|
||||||
|
gtk_page_setup_set_orientation(setup, GTK_PAGE_ORIENTATION_PORTRAIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkPaperSize* size = gtk_paper_size_new_custom("xoj-internal", "xoj-internal", width, height, GTK_UNIT_POINTS);
|
||||||
|
gtk_page_setup_set_paper_size(setup, size);
|
||||||
|
gtk_paper_size_free(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void handlePrintError(GError*& error, const char* message) {
|
||||||
|
if (error) {
|
||||||
|
g_warning(message, error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
error = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Todo: maybe loop over this twice,
|
||||||
|
void PrintHandler::print(Document* doc, size_t currentPage, GtkWindow* parent) {
|
||||||
|
GtkPrintSettings* settings{};
|
||||||
|
auto filepath = Util::getConfigFile(PRINT_CONFIG_FILE);
|
||||||
|
if (fs::exists(filepath)) {
|
||||||
|
GError* error{};
|
||||||
|
settings = gtk_print_settings_new_from_file(char_cast(filepath.u8string().c_str()), &error);
|
||||||
|
handlePrintError(error, "Loading print settings failed with: %s");
|
||||||
|
fs::remove(filepath);
|
||||||
|
}
|
||||||
|
if (settings == nullptr) {
|
||||||
|
settings = gtk_print_settings_new();
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkPrintOperation* op = gtk_print_operation_new();
|
||||||
|
gtk_print_operation_set_print_settings(op, settings);
|
||||||
|
gtk_print_operation_set_n_pages(op, strict_cast<int>(doc->getPageCount()));
|
||||||
|
gtk_print_operation_set_current_page(op, strict_cast<int>(currentPage));
|
||||||
|
gtk_print_operation_set_job_name(op, "Xournal++");
|
||||||
|
gtk_print_operation_set_unit(op, GTK_UNIT_POINTS);
|
||||||
|
gtk_print_operation_set_use_full_page(op, true);
|
||||||
|
g_signal_connect(op, "draw_page", G_CALLBACK(drawPage), doc);
|
||||||
|
g_signal_connect(op, "request-page-setup", G_CALLBACK(requestPageSetup), doc);
|
||||||
|
|
||||||
|
GError* error{};
|
||||||
|
GtkPrintOperationResult res = gtk_print_operation_run(op, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, parent, &error);
|
||||||
|
g_object_unref(settings);
|
||||||
|
if (GTK_PRINT_OPERATION_RESULT_APPLY == res) {
|
||||||
|
xoj_assert(!error);
|
||||||
|
settings = gtk_print_operation_get_print_settings(op);
|
||||||
|
gtk_print_settings_to_file(settings, char_cast(filepath.u8string().c_str()), nullptr);
|
||||||
|
} else if (GTK_PRINT_OPERATION_RESULT_ERROR == res) {
|
||||||
|
xoj_assert(error);
|
||||||
|
std::string msg = FS(_F("Running print operation failed with {1}") % error->message);
|
||||||
|
XojMsgBox::showErrorToUser(nullptr, msg);
|
||||||
|
g_error_free(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_object_unref(op);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Prints a document
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
|
||||||
|
#include <gtk/gtk.h> // for GtkWindow
|
||||||
|
|
||||||
|
class Document;
|
||||||
|
|
||||||
|
namespace PrintHandler {
|
||||||
|
void print(Document* doc, size_t currentPage, GtkWindow* parent);
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#include "RecentManager.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for sort, max
|
||||||
|
#include <ctime> // for time_t
|
||||||
|
#include <functional> // for greater, less
|
||||||
|
#include <string> // for string, allocator, operator+
|
||||||
|
|
||||||
|
#include "util/GListView.h" // for GListView, GListView<>::GListVie...
|
||||||
|
#include "util/PathUtil.h" // for fromUri, toUri, hasPdfFileExt
|
||||||
|
#include "util/TinyVector.h" // for TinyVector
|
||||||
|
#include "util/safe_casts.h" // for as_signed
|
||||||
|
|
||||||
|
#include "Control.h"
|
||||||
|
|
||||||
|
template <template <class> class Comp>
|
||||||
|
inline auto compareGtkRecentInfo(GtkRecentInfo const& a, GtkRecentInfo const& b) -> bool {
|
||||||
|
auto deconstify = [](auto&& ref) { return const_cast<GtkRecentInfo*>(&ref); }; // NOLINT
|
||||||
|
auto a_time = gtk_recent_info_get_modified(deconstify(a)); // actually const
|
||||||
|
auto b_time = gtk_recent_info_get_modified(deconstify(b));
|
||||||
|
return Comp<time_t>{}(a_time, b_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto operator<(GtkRecentInfo const& a, GtkRecentInfo const& b) -> bool {
|
||||||
|
return compareGtkRecentInfo<std::less>(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto operator>(GtkRecentInfo const& a, GtkRecentInfo const& b) -> bool {
|
||||||
|
return compareGtkRecentInfo<std::greater>(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum AcceptedFileType { UNSUPPORTED_FILE_TYPE, XOURNAL_FILE_TYPE, PDF_FILE_TYPE };
|
||||||
|
auto getFileType(GtkRecentInfo& info) -> AcceptedFileType {
|
||||||
|
const gchar* uri = gtk_recent_info_get_uri(&info);
|
||||||
|
if (!uri) { // issue #1071
|
||||||
|
return UNSUPPORTED_FILE_TYPE;
|
||||||
|
}
|
||||||
|
auto p = Util::fromUri(uri);
|
||||||
|
if (!p) { // Skip remote files
|
||||||
|
return UNSUPPORTED_FILE_TYPE;
|
||||||
|
}
|
||||||
|
if (Util::hasXournalFileExt(*p)) {
|
||||||
|
return XOURNAL_FILE_TYPE;
|
||||||
|
}
|
||||||
|
if (Util::hasPdfFileExt(*p)) {
|
||||||
|
return PDF_FILE_TYPE;
|
||||||
|
}
|
||||||
|
return UNSUPPORTED_FILE_TYPE;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RecentManager::clearRecentFiles() {
|
||||||
|
GtkRecentManager* recentManager = gtk_recent_manager_get_default();
|
||||||
|
GList* items = gtk_recent_manager_get_items(recentManager);
|
||||||
|
auto item_view = GListView<GtkRecentInfo>(items);
|
||||||
|
for (auto& recent: item_view) {
|
||||||
|
if (getFileType(recent) != UNSUPPORTED_FILE_TYPE) {
|
||||||
|
gtk_recent_manager_remove_item(recentManager,
|
||||||
|
gtk_recent_info_get_uri(&recent),
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_list_free_full(items, GDestroyNotify(gtk_recent_info_unref));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecentManager::addRecentFileFilename(const fs::path& filepath) {
|
||||||
|
GtkRecentManager* recentManager = gtk_recent_manager_get_default();
|
||||||
|
|
||||||
|
std::string group_name = GROUP;
|
||||||
|
std::array<gchar*, 2> groups = {group_name.data(), nullptr};
|
||||||
|
std::string app_name = g_get_application_name();
|
||||||
|
std::string app_exec = std::string(g_get_prgname()) + " %u";
|
||||||
|
std::string mime_type = Util::hasPdfFileExt(filepath) ? std::string(MIME_PDF) : std::string(MIME);
|
||||||
|
|
||||||
|
GtkRecentData recentData{};
|
||||||
|
recentData.display_name = nullptr;
|
||||||
|
recentData.description = nullptr;
|
||||||
|
recentData.app_name = app_name.data();
|
||||||
|
recentData.app_exec = app_exec.data();
|
||||||
|
recentData.groups = groups.data();
|
||||||
|
recentData.mime_type = mime_type.data();
|
||||||
|
recentData.is_private = false;
|
||||||
|
|
||||||
|
auto uri = Util::toUri(filepath);
|
||||||
|
if (!uri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gtk_recent_manager_add_full(recentManager, (*uri).c_str(), &recentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecentManager::removeRecentFileFilename(const fs::path& filename) {
|
||||||
|
auto uri = Util::toUri(filename);
|
||||||
|
if (!uri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GtkRecentManager* recentManager = gtk_recent_manager_get_default();
|
||||||
|
gtk_recent_manager_remove_item(recentManager, uri->c_str(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto RecentManager::getMostRecent() -> GtkRecentInfoSPtr {
|
||||||
|
auto recent_items = gtk_recent_manager_get_items(gtk_recent_manager_get_default());
|
||||||
|
// Todo (cpp20): replace with std::ranges::max_element
|
||||||
|
if (!recent_items) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
GtkRecentInfo* mostRecent = nullptr;
|
||||||
|
for (auto& recent: GListView<GtkRecentInfo>(recent_items)) {
|
||||||
|
auto time = gtk_recent_info_get_modified(&recent);
|
||||||
|
if (as_signed(time) < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (getFileType(recent) != XOURNAL_FILE_TYPE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mostRecent == nullptr || *mostRecent < recent) {
|
||||||
|
mostRecent = &recent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prolong lifetime past the list deletion
|
||||||
|
GtkRecentInfoSPtr res = mostRecent == nullptr ? nullptr : GtkRecentInfoSPtr(mostRecent, xoj::util::ref);
|
||||||
|
|
||||||
|
g_list_free_full(recent_items, GDestroyNotify(gtk_recent_info_unref));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto RecentManager::getRecentFiles() -> RecentFiles {
|
||||||
|
GtkRecentManager* recentManager = gtk_recent_manager_get_default();
|
||||||
|
GList* items = gtk_recent_manager_get_items(recentManager);
|
||||||
|
|
||||||
|
RecentFiles res;
|
||||||
|
for (auto& recent: GListView<GtkRecentInfo>(items)) {
|
||||||
|
auto fileType = getFileType(recent);
|
||||||
|
if (fileType == PDF_FILE_TYPE && res.recentPdfFiles.size() < MAX_RECENT) {
|
||||||
|
res.recentPdfFiles.emplace_back(&recent, xoj::util::ref);
|
||||||
|
}
|
||||||
|
if (fileType == XOURNAL_FILE_TYPE && res.recentXoppFiles.size() < MAX_RECENT) {
|
||||||
|
res.recentXoppFiles.emplace_back(&recent, xoj::util::ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_list_free_full(items, GDestroyNotify(gtk_recent_info_unref));
|
||||||
|
|
||||||
|
auto comp = [](const GtkRecentInfoSPtr& p1, const GtkRecentInfoSPtr& p2) { return *p1.get() > *p2.get(); };
|
||||||
|
|
||||||
|
std::sort(res.recentPdfFiles.begin(), res.recentPdfFiles.end(), comp);
|
||||||
|
std::sort(res.recentXoppFiles.begin(), res.recentXoppFiles.end(), comp);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* The recent opened files
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtk/gtk.h> // for GtkRecentInfo
|
||||||
|
|
||||||
|
#include "util/TinyVector.h"
|
||||||
|
#include "util/raii/CLibrariesSPtr.h"
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles the GtkMenu displaying the recent files
|
||||||
|
*/
|
||||||
|
namespace RecentManager {
|
||||||
|
constexpr auto const* MIME = "application/x-xoj";
|
||||||
|
constexpr auto const* MIME_PDF = "application/x-pdf";
|
||||||
|
constexpr auto const* GROUP = "xournal++";
|
||||||
|
constexpr int MAX_RECENT = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a file to the underlying GtkRecentManager
|
||||||
|
*/
|
||||||
|
void addRecentFileFilename(const fs::path& filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a file from the underlying GtkRecentManager
|
||||||
|
*/
|
||||||
|
void removeRecentFileFilename(const fs::path& filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all supported files from the recent file list
|
||||||
|
*/
|
||||||
|
void clearRecentFiles();
|
||||||
|
|
||||||
|
class GtkRecentInfoHandler {
|
||||||
|
public:
|
||||||
|
constexpr static auto ref = gtk_recent_info_ref;
|
||||||
|
constexpr static auto unref = gtk_recent_info_unref;
|
||||||
|
// Todo(cpp20): replace with std:identity()
|
||||||
|
constexpr static auto adopt = [](GtkRecentInfo* p) { return p; };
|
||||||
|
};
|
||||||
|
using GtkRecentInfoSPtr = xoj::util::CLibrariesSPtr<GtkRecentInfo, GtkRecentInfoHandler>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most recent xoj item from the underlying GtkRecentManager
|
||||||
|
* or nullptr, if no recent files exist
|
||||||
|
*/
|
||||||
|
GtkRecentInfoSPtr getMostRecent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
struct RecentFiles {
|
||||||
|
TinyVector<GtkRecentInfoSPtr, MAX_RECENT> recentXoppFiles;
|
||||||
|
TinyVector<GtkRecentInfoSPtr, MAX_RECENT> recentPdfFiles;
|
||||||
|
};
|
||||||
|
RecentFiles getRecentFiles();
|
||||||
|
}; // namespace RecentManager
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "ScrollHandler.h"
|
||||||
|
|
||||||
|
#include <memory> // for __shared_ptr_access
|
||||||
|
|
||||||
|
#include <glib.h> // for g_error
|
||||||
|
|
||||||
|
#include "control/zoom/ZoomControl.h" // for ZoomControl
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "gui/sidebar/Sidebar.h" // for Sidebar
|
||||||
|
#include "gui/widgets/SpinPageAdapter.h" // for SpinPageAdapter
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/LinkDestination.h" // for LinkDestination
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "util/Util.h" // for npos
|
||||||
|
|
||||||
|
#include "Control.h" // for Control
|
||||||
|
|
||||||
|
ScrollHandler::ScrollHandler(Control* control): control(control) {}
|
||||||
|
|
||||||
|
ScrollHandler::~ScrollHandler() = default;
|
||||||
|
|
||||||
|
void ScrollHandler::goToPreviousPage() {
|
||||||
|
if (this->control->getWindow()) {
|
||||||
|
scrollToPage(this->control->getWindow()->getXournal()->getCurrentPage() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::goToNextPage() {
|
||||||
|
if (this->control->getWindow()) {
|
||||||
|
scrollToPage(this->control->getWindow()->getXournal()->getCurrentPage() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::goToLastPage() {
|
||||||
|
if (this->control->getWindow()) {
|
||||||
|
scrollToPage(this->control->getDocument()->getPageCount() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::goToFirstPage() {
|
||||||
|
if (this->control->getWindow()) {
|
||||||
|
scrollToPage(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::scrollToPage(const PageRef& page, XojPdfRectangle rect) {
|
||||||
|
Document* doc = this->control->getDocument();
|
||||||
|
|
||||||
|
doc->lock_shared();
|
||||||
|
auto p = doc->indexOf(page);
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
if (p != npos) {
|
||||||
|
scrollToPage(p, rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::scrollToPage(size_t page, XojPdfRectangle rect) {
|
||||||
|
MainWindow* win = this->control->getWindow();
|
||||||
|
if (win == nullptr) {
|
||||||
|
g_error("Window is nullptr!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
win->getXournal()->scrollTo(page, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::scrollToLinkDest(const LinkDestination& dest) {
|
||||||
|
size_t pdfPage = dest.getPdfPage();
|
||||||
|
|
||||||
|
if (pdfPage != npos) {
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
doc->lock_shared();
|
||||||
|
size_t page = doc->findPdfPage(pdfPage);
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
if (page == npos) {
|
||||||
|
control->askInsertPdfPage(pdfPage);
|
||||||
|
} else {
|
||||||
|
if (dest.shouldChangeTop()) {
|
||||||
|
control->getScrollHandler()->scrollToPage(page, {dest.getLeft(), dest.getTop(), -1, -1});
|
||||||
|
} else {
|
||||||
|
if (control->getCurrentPageNo() != page) {
|
||||||
|
control->getScrollHandler()->scrollToPage(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::scrollToAnnotatedPage(bool next) {
|
||||||
|
if (!this->control->getWindow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t step = next ? size_t(1) : size_t(-1); // Don't assume blindly that size_t(-1) == npos
|
||||||
|
|
||||||
|
Document* doc = this->control->getDocument();
|
||||||
|
|
||||||
|
for (size_t i = this->control->getCurrentPageNo() + step; i < doc->getPageCount() && i != size_t(-1); i += step) {
|
||||||
|
if (doc->getPage(i)->isAnnotated()) {
|
||||||
|
scrollToPage(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ScrollHandler::isPageVisible(size_t page, int* visibleHeight) -> bool {
|
||||||
|
if (!this->control->getWindow()) {
|
||||||
|
if (visibleHeight) {
|
||||||
|
*visibleHeight = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->control->getWindow()->getXournal()->isPageVisible(page, visibleHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollHandler::pageChanged(size_t page) {
|
||||||
|
if (page == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollToPage(page - 1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Scroll handler
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
|
||||||
|
#include "gui/widgets/SpinPageAdapter.h" // for SpinPageListener
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfRectangle
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
class LinkDestination;
|
||||||
|
|
||||||
|
class ScrollHandler: public SpinPageListener {
|
||||||
|
public:
|
||||||
|
ScrollHandler(Control* control);
|
||||||
|
~ScrollHandler() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void goToPreviousPage();
|
||||||
|
void goToNextPage();
|
||||||
|
|
||||||
|
void goToLastPage();
|
||||||
|
void goToFirstPage();
|
||||||
|
|
||||||
|
void scrollToPage(const PageRef& page, XojPdfRectangle rect = {0, 0, -1, -1});
|
||||||
|
void scrollToPage(size_t page, XojPdfRectangle rect = {0, 0, -1, -1});
|
||||||
|
|
||||||
|
void scrollToAnnotatedPage(bool next);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to a given link's destination, provided the
|
||||||
|
* destination is a local destination and not a URI.
|
||||||
|
*
|
||||||
|
* If the destination is a non-existent PDF page,
|
||||||
|
* we ask the user whether to add the missing page or not.
|
||||||
|
*
|
||||||
|
* @param dest is to shown
|
||||||
|
*/
|
||||||
|
void scrollToLinkDest(const LinkDestination& dest);
|
||||||
|
|
||||||
|
bool isPageVisible(size_t page, int* visibleHeight = nullptr);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void pageChanged(size_t page) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Control* control = nullptr;
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include "SearchControl.h"
|
||||||
|
|
||||||
|
#include <memory> // for __shared_ptr_access
|
||||||
|
#include <utility> // for move
|
||||||
|
|
||||||
|
#include "model/Element.h" // for Element, ELEMENT_TEXT
|
||||||
|
#include "model/Layer.h" // for Layer
|
||||||
|
#include "model/Text.h" // for Text
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "view/overlays/SearchResultView.h" // for SEARCH_CHANGED_NOTIFICATION
|
||||||
|
|
||||||
|
SearchControl::SearchControl(const PageRef& page, XojPdfPageSPtr pdf):
|
||||||
|
page(page),
|
||||||
|
pdf(std::move(pdf)),
|
||||||
|
viewPool(std::make_shared<xoj::util::DispatchPool<xoj::view::SearchResultView>>()) {}
|
||||||
|
|
||||||
|
SearchControl::~SearchControl() = default;
|
||||||
|
|
||||||
|
auto SearchControl::search(const std::string& text, size_t index, size_t* occurrences, XojPdfRectangle* matchRect)
|
||||||
|
-> bool {
|
||||||
|
this->highlightRect = nullptr;
|
||||||
|
if (text.empty()) {
|
||||||
|
if (!this->results.empty()) {
|
||||||
|
this->results.clear();
|
||||||
|
this->currentText.clear();
|
||||||
|
this->viewPool->dispatch(xoj::view::SearchResultView::SEARCH_CHANGED_NOTIFICATION);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text != this->currentText) {
|
||||||
|
this->results.clear();
|
||||||
|
this->currentText = text;
|
||||||
|
|
||||||
|
if (this->pdf) {
|
||||||
|
this->results = this->pdf->findText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Layer* l: this->page->getLayers()) {
|
||||||
|
if (!l->isVisible()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto&& e: l->getElementsView()) {
|
||||||
|
if (e->getType() == ELEMENT_TEXT) {
|
||||||
|
const Text* t = dynamic_cast<const Text*>(e);
|
||||||
|
|
||||||
|
std::vector<XojPdfRectangle> textResult = t->findText(text);
|
||||||
|
this->results.insert(this->results.end(), textResult.begin(), textResult.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->viewPool->dispatch(xoj::view::SearchResultView::SEARCH_CHANGED_NOTIFICATION);
|
||||||
|
if (occurrences) {
|
||||||
|
*occurrences = this->results.size();
|
||||||
|
}
|
||||||
|
bool found;
|
||||||
|
if (index - 1 >= this->results.size()) {
|
||||||
|
if (matchRect) {
|
||||||
|
*matchRect = XojPdfRectangle(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
found = false;
|
||||||
|
} else {
|
||||||
|
if (matchRect) {
|
||||||
|
this->highlightRect = &this->results[index - 1];
|
||||||
|
*matchRect = *this->highlightRect;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Handles text search on a PDF page and in Xournal Texts
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "model/OverlayBase.h"
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "model/Point.h" // for Point
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfPageSPtr, XojPdfRectangle
|
||||||
|
#include "util/DispatchPool.h"
|
||||||
|
|
||||||
|
namespace xoj::view {
|
||||||
|
class OverlayView;
|
||||||
|
class Repaintable;
|
||||||
|
class SearchResultView;
|
||||||
|
}; // namespace xoj::view
|
||||||
|
|
||||||
|
class SearchControl: public OverlayBase {
|
||||||
|
public:
|
||||||
|
SearchControl(const PageRef& page, XojPdfPageSPtr pdf);
|
||||||
|
virtual ~SearchControl();
|
||||||
|
|
||||||
|
bool search(const std::string& text, size_t index, size_t* occurrences, XojPdfRectangle* UpperMostMatch);
|
||||||
|
|
||||||
|
const std::vector<XojPdfRectangle>& getResults() const { return results; }
|
||||||
|
|
||||||
|
const XojPdfRectangle* getHighlightRect() const { return highlightRect; }
|
||||||
|
|
||||||
|
const std::shared_ptr<xoj::util::DispatchPool<xoj::view::SearchResultView>>& getViewPool() const {
|
||||||
|
return viewPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PageRef page;
|
||||||
|
XojPdfPageSPtr pdf;
|
||||||
|
std::string currentText;
|
||||||
|
XojPdfRectangle* highlightRect = nullptr;
|
||||||
|
|
||||||
|
std::vector<XojPdfRectangle> results;
|
||||||
|
std::shared_ptr<xoj::util::DispatchPool<xoj::view::SearchResultView>> viewPool;
|
||||||
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#include "SetsquareController.h"
|
||||||
|
|
||||||
|
#include "control/Control.h"
|
||||||
|
#include "control/layer/LayerController.h"
|
||||||
|
#include "gui/XournalView.h"
|
||||||
|
#include "model/GeometryTool.h"
|
||||||
|
#include "model/Setsquare.h"
|
||||||
|
#include "model/Stroke.h"
|
||||||
|
#include "model/XojPage.h"
|
||||||
|
|
||||||
|
using xoj::util::Rectangle;
|
||||||
|
SetsquareController::SetsquareController(XojPageView* view, Setsquare* setsquare):
|
||||||
|
GeometryToolController(view, setsquare) {}
|
||||||
|
|
||||||
|
SetsquareController::~SetsquareController() = default;
|
||||||
|
|
||||||
|
auto SetsquareController::getType() const -> GeometryToolType { return GeometryToolType::SETSQUARE; }
|
||||||
|
|
||||||
|
auto SetsquareController::posRelToSide(Leg leg, double x, double y) const -> xoj::util::Point<double> {
|
||||||
|
cairo_matrix_t matrix = geometryTool->getMatrix();
|
||||||
|
cairo_matrix_invert(&matrix);
|
||||||
|
cairo_matrix_transform_point(&matrix, &x, &y);
|
||||||
|
switch (leg) {
|
||||||
|
case HYPOTENUSE:
|
||||||
|
return xoj::util::Point<double>(x, -y);
|
||||||
|
case LEFT_LEG:
|
||||||
|
return xoj::util::Point<double>((y + x) / std::sqrt(2.),
|
||||||
|
(y - x - geometryTool->getHeight()) / std::sqrt(2.));
|
||||||
|
case RIGHT_LEG:
|
||||||
|
return xoj::util::Point<double>((y - x) / std::sqrt(2.),
|
||||||
|
(y + x - geometryTool->getHeight()) / std::sqrt(2.));
|
||||||
|
default:
|
||||||
|
g_error("Invalid enum value: %d", leg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetsquareController::isInsideGeometryTool(double x, double y, double border) const -> bool {
|
||||||
|
return posRelToSide(HYPOTENUSE, x, y).y < border && posRelToSide(LEFT_LEG, x, y).y < border &&
|
||||||
|
posRelToSide(RIGHT_LEG, x, y).y < border;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetsquareController::getPointForPos(double xCoord) const -> xoj::util::Point<double> {
|
||||||
|
double x = xCoord;
|
||||||
|
double y = 0.0;
|
||||||
|
cairo_matrix_t matrix = geometryTool->getMatrix();
|
||||||
|
cairo_matrix_transform_point(&matrix, &x, &y);
|
||||||
|
|
||||||
|
return xoj::util::Point<double>(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetsquareController::createEdgeStroke(double x) {
|
||||||
|
if (!std::isnan(x)) {
|
||||||
|
hypotenuseMax = x;
|
||||||
|
hypotenuseMin = x;
|
||||||
|
|
||||||
|
const auto p = this->getPointForPos(x);
|
||||||
|
initializeStroke();
|
||||||
|
stroke->addPoint(Point(p.x, p.y));
|
||||||
|
stroke->addPoint(Point(p.x, p.y)); // doubled point
|
||||||
|
geometryTool->notify();
|
||||||
|
} else {
|
||||||
|
g_warning("No valid stroke from setsquare!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetsquareController::createRadialStroke(double x, double y) {
|
||||||
|
const auto p = posRelToSide(HYPOTENUSE, x, y);
|
||||||
|
this->strokeAngle = std::atan2(p.y, p.x);
|
||||||
|
initializeStroke();
|
||||||
|
updateRadialStroke(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetsquareController::updateEdgeStroke(double x) {
|
||||||
|
hypotenuseMax = std::max(this->hypotenuseMax, x);
|
||||||
|
hypotenuseMin = std::min(this->hypotenuseMin, x);
|
||||||
|
stroke->deletePointsFrom(0);
|
||||||
|
const auto p1 = getPointForPos(hypotenuseMin);
|
||||||
|
const auto p2 = getPointForPos(hypotenuseMax);
|
||||||
|
|
||||||
|
stroke->addPoint(Point(p1.x, p1.y));
|
||||||
|
stroke->addPoint(Point(p2.x, p2.y));
|
||||||
|
geometryTool->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetsquareController::updateRadialStroke(double x, double y) {
|
||||||
|
stroke->deletePointsFrom(0);
|
||||||
|
const auto c = getPointForPos(0);
|
||||||
|
stroke->addPoint(Point(c.x, c.y));
|
||||||
|
|
||||||
|
const auto p = posRelToSide(HYPOTENUSE, x, y);
|
||||||
|
const auto rad = std::hypot(p.x, p.y);
|
||||||
|
|
||||||
|
if (rad >= Setsquare::radiusFromHeight(geometryTool->getHeight()) || p.y > 0) {
|
||||||
|
this->strokeAngle = std::atan2(p.y, p.x);
|
||||||
|
stroke->addPoint(Point(x, y));
|
||||||
|
} else {
|
||||||
|
cairo_matrix_t matrix = geometryTool->getMatrix();
|
||||||
|
auto qx = rad * std::cos(this->strokeAngle);
|
||||||
|
auto qy = -rad * std::sin(this->strokeAngle);
|
||||||
|
cairo_matrix_transform_point(&matrix, &qx, &qy);
|
||||||
|
|
||||||
|
stroke->addPoint(Point(qx, qy));
|
||||||
|
}
|
||||||
|
geometryTool->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetsquareController::finalizeEdgeStroke() {
|
||||||
|
hypotenuseMax = std::numeric_limits<double>::lowest();
|
||||||
|
hypotenuseMin = std::numeric_limits<double>::max();
|
||||||
|
addStrokeToLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetsquareController::finalizeRadialStroke() {
|
||||||
|
strokeAngle = NAN;
|
||||||
|
addStrokeToLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SetsquareController::existsEdgeStroke() -> bool { return hypotenuseMax != std::numeric_limits<double>::lowest(); }
|
||||||
|
|
||||||
|
auto SetsquareController::existsRadialStroke() -> bool { return !std::isnan(strokeAngle); }
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A setsquare controller
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include "gui/PageView.h"
|
||||||
|
#include "model/GeometryTool.h"
|
||||||
|
#include "util/Point.h"
|
||||||
|
|
||||||
|
#include "GeometryToolController.h"
|
||||||
|
|
||||||
|
class Setsquare;
|
||||||
|
class Stroke;
|
||||||
|
|
||||||
|
enum Leg { HYPOTENUSE, LEFT_LEG, RIGHT_LEG };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A class that controls a setsquare
|
||||||
|
|
||||||
|
* The setsquare can be moved, rotated and scaled
|
||||||
|
* There are methods for translating coordinates
|
||||||
|
* and methods to deal with the temporary stroke
|
||||||
|
* that is displayed near the longest side or
|
||||||
|
* the midpoint to the longest side of the setsquare.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SetsquareController: public GeometryToolController {
|
||||||
|
public:
|
||||||
|
SetsquareController(XojPageView* view, Setsquare* setsquare);
|
||||||
|
~SetsquareController() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GeometryToolType getType() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief returns the position of a point relative to a coordinate system, in which the given setsquare leg lies on
|
||||||
|
* the x-axis with the origin in its center (where the unit is 1 cm)
|
||||||
|
* @param leg the leg of the setsquare
|
||||||
|
* @param x the x-coordinate of the point (in document coordinates)
|
||||||
|
* @param y the y-coordinate of the point (in document coordinates)
|
||||||
|
*/
|
||||||
|
xoj::util::Point<double> posRelToSide(Leg leg, double x, double y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief checks whether a point with given coordinates lies in the setsquare with an additional border enlarging
|
||||||
|
* (or shrinked) it
|
||||||
|
* @param x the x-coordinate of the given point (in document coordinates)
|
||||||
|
* @param y the y-coordinate of the given point (in document coordinates)
|
||||||
|
* @param border the size of the border (if negative, the setsquare is shrinked via the border)
|
||||||
|
*/
|
||||||
|
bool isInsideGeometryTool(double x, double y, double border = 0.0) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the point (in document coordinates) for a given position on the longest side of the setsquare
|
||||||
|
* @param x the x-coordinate of the point on the longest side of the setsquare (when unrotated and untranslated)
|
||||||
|
*/
|
||||||
|
xoj::util::Point<double> getPointForPos(double x) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief creates a stroke starting at the given position of the longest side of the setsquare
|
||||||
|
* @param x the x-coordinate of the point on the longest side of the setsquare (when unrotated and untranslated)
|
||||||
|
*/
|
||||||
|
void createEdgeStroke(double x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief updates the stroke aligned to the longest side of the setsquare
|
||||||
|
* @param x the x-coordinate of the point on the longest side of the setsquare (when unrotated and untranslated)
|
||||||
|
* updating the stroke
|
||||||
|
*/
|
||||||
|
void updateEdgeStroke(double x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief finishes the stroke aligned to the longest side of the setsquare
|
||||||
|
*/
|
||||||
|
void finalizeEdgeStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief creates a radius starting at the given position to the origin of the setsquare
|
||||||
|
* @param x the x-coordinate of the current point
|
||||||
|
* @param y the y-coordinate of the current point
|
||||||
|
*/
|
||||||
|
void createRadialStroke(double x, double y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief updates the radius to the origin of the setsquare
|
||||||
|
* @param x the x-coordinate of the current point
|
||||||
|
* @param y the y-coordinate of the current point
|
||||||
|
* updating the stroke
|
||||||
|
*/
|
||||||
|
void updateRadialStroke(double x, double y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief finishes the radius to the origin of the setsquare
|
||||||
|
*/
|
||||||
|
void finalizeRadialStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks whether a stroke already exists
|
||||||
|
*/
|
||||||
|
bool existsEdgeStroke();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks whether a radius already exists
|
||||||
|
*/
|
||||||
|
bool existsRadialStroke();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief when a stroke aligned to the longest side (hypotenuse) of the setsquare is drawn, the minimal and maximal
|
||||||
|
* x-coordinates of the point to be drawn (with respect to an unrotated, and untranslated coordinate system) are
|
||||||
|
* saved in the variables hypotenuseMin and hypotenuseMax
|
||||||
|
*/
|
||||||
|
double hypotenuseMax = std::numeric_limits<double>::lowest();
|
||||||
|
double hypotenuseMin = std::numeric_limits<double>::max();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief the current angle at which the temporary radius is drawn
|
||||||
|
*/
|
||||||
|
double strokeAngle = NAN;
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include "Tool.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
Tool::Tool(std::string name, ToolType type, Color color, std::optional<std::array<double, toolSizes>> thickness):
|
||||||
|
name{std::move(name)},
|
||||||
|
type{type},
|
||||||
|
thickness{std::move(thickness)},
|
||||||
|
capabilities{xoj::tool::typeToCapabilities(type)} {
|
||||||
|
setColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tool::Tool(const Tool& t): name{t.name}, type{t.type}, thickness{t.thickness}, capabilities{t.capabilities} {
|
||||||
|
setColor(t.getColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
Tool::~Tool() {}
|
||||||
|
|
||||||
|
auto Tool::getName() const -> std::string { return this->name; }
|
||||||
|
|
||||||
|
auto Tool::getToolType() const -> ToolType { return this->type; }
|
||||||
|
|
||||||
|
void Tool::setCapability(ToolCapabilities capability, bool enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
this->capabilities = static_cast<ToolCapabilities>(this->capabilities | capability);
|
||||||
|
} else {
|
||||||
|
this->capabilities = static_cast<ToolCapabilities>(this->capabilities & ~capability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Tool::hasCapability(ToolCapabilities cap) const -> bool { return (this->capabilities & cap) != 0; }
|
||||||
|
|
||||||
|
auto Tool::getThickness(ToolSize size) const -> double { return this->thickness.value()[size - TOOL_SIZE_VERY_FINE]; }
|
||||||
|
|
||||||
|
auto Tool::isDrawingTool() const -> bool {
|
||||||
|
return this->type == TOOL_PEN || this->type == TOOL_HIGHLIGHTER || this->type == TOOL_ERASER;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A tool
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array> // for array
|
||||||
|
#include <optional> // for optional
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "control/ToolEnums.h" // for ToolType, ToolCapabilities, ToolSize
|
||||||
|
#include "util/Color.h" // for Color
|
||||||
|
|
||||||
|
#include "ToolBase.h" // for ToolBase
|
||||||
|
|
||||||
|
|
||||||
|
class Tool: public ToolBase {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief number of different sizes defined for tools with Size capability
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static constexpr int toolSizes = 5;
|
||||||
|
|
||||||
|
Tool(std::string name, ToolType type, Color color, std::optional<std::array<double, Tool::toolSizes>> thickness);
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Tool object based on another tool.
|
||||||
|
* @param t tool to use as basis for new copy.
|
||||||
|
*/
|
||||||
|
Tool(const Tool& t);
|
||||||
|
~Tool() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string getName() const;
|
||||||
|
|
||||||
|
ToolType getToolType() const;
|
||||||
|
|
||||||
|
bool hasCapability(ToolCapabilities cap) const;
|
||||||
|
|
||||||
|
double getThickness(ToolSize size) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the tool is a Drawing tool.
|
||||||
|
* Drawing tools are considered all tools that directly change the canvas.
|
||||||
|
* Right now these are:
|
||||||
|
* - Highlighter
|
||||||
|
* - Pen
|
||||||
|
* - Eraser
|
||||||
|
*
|
||||||
|
* @return true if tool is a drawing tool
|
||||||
|
* @return false if tool is no drawing tool
|
||||||
|
*/
|
||||||
|
bool isDrawingTool() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setCapability(ToolCapabilities capability, bool enabled);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void operator=(const Tool& t);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string name;
|
||||||
|
ToolType type;
|
||||||
|
|
||||||
|
std::optional<std::array<double, toolSizes>> thickness;
|
||||||
|
|
||||||
|
ToolCapabilities capabilities;
|
||||||
|
|
||||||
|
friend class ToolHandler;
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#include "ToolBase.h"
|
||||||
|
|
||||||
|
#include "control/ToolEnums.h" // for DrawingType, EraserType, ToolSize
|
||||||
|
|
||||||
|
ToolBase::ToolBase() = default;
|
||||||
|
|
||||||
|
ToolBase::~ToolBase() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Color of the tool for all drawing tools
|
||||||
|
*/
|
||||||
|
auto ToolBase::getColor() const -> Color { return this->color; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param color Color of the tool for all drawing tools
|
||||||
|
*/
|
||||||
|
void ToolBase::setColor(Color color) { this->color = color; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Size of a drawing tool
|
||||||
|
*/
|
||||||
|
auto ToolBase::getSize() const -> ToolSize { return this->size; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param size Size of a drawing tool
|
||||||
|
*/
|
||||||
|
void ToolBase::setSize(ToolSize size) { this->size = size; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Draw special shape
|
||||||
|
*/
|
||||||
|
auto ToolBase::getDrawingType() const -> DrawingType { return this->drawingType; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param drawingType Draw special shape
|
||||||
|
*/
|
||||||
|
void ToolBase::setDrawingType(DrawingType drawingType) { this->drawingType = drawingType; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Eraser Type
|
||||||
|
*/
|
||||||
|
auto ToolBase::getEraserType() const -> EraserType { return this->eraserType; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param eraserType type of eraser
|
||||||
|
*/
|
||||||
|
void ToolBase::setEraserType(EraserType eraserType) { this->eraserType = eraserType; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Fill of the shape is enabled
|
||||||
|
*/
|
||||||
|
auto ToolBase::getFill() const -> bool { return this->fill; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fill Fill of the shape is enabled
|
||||||
|
*/
|
||||||
|
void ToolBase::setFill(bool fill) { this->fill = fill; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Alpha for fill
|
||||||
|
*/
|
||||||
|
auto ToolBase::getFillAlpha() const -> int { return this->fillAlpha; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fillAlpha Alpha for fill
|
||||||
|
*/
|
||||||
|
void ToolBase::setFillAlpha(int fillAlpha) { this->fillAlpha = fillAlpha; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style of the line drawing
|
||||||
|
*/
|
||||||
|
auto ToolBase::getLineStyle() const -> const LineStyle& { return this->lineStyle; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param style Style of the line drawing
|
||||||
|
*/
|
||||||
|
void ToolBase::setLineStyle(const LineStyle& style) { this->lineStyle = style; }
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Base class for a tool, which contains the tool configuration,
|
||||||
|
* this is also used for other classes, e.g. to store current tool configuration
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "model/LineStyle.h" // for LineStyle
|
||||||
|
#include "util/Color.h" // for Color
|
||||||
|
|
||||||
|
#include "ToolEnums.h" // for DrawingType, EraserType, ToolSize, DRAW...
|
||||||
|
|
||||||
|
|
||||||
|
class ToolBase {
|
||||||
|
public:
|
||||||
|
ToolBase();
|
||||||
|
virtual ~ToolBase();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void operator=(const ToolBase& t);
|
||||||
|
ToolBase(const ToolBase& t);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @return Color of the tool for all drawing tools
|
||||||
|
*/
|
||||||
|
Color getColor() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param color Color of the tool for all drawing tools
|
||||||
|
*/
|
||||||
|
virtual void setColor(Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Size of a drawing tool
|
||||||
|
*/
|
||||||
|
ToolSize getSize() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param size Size of a drawing tool
|
||||||
|
*/
|
||||||
|
void setSize(ToolSize size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Draw special shape
|
||||||
|
*/
|
||||||
|
DrawingType getDrawingType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param drawingType Draw special shape
|
||||||
|
*/
|
||||||
|
void setDrawingType(DrawingType drawingType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Eraser Type object
|
||||||
|
*
|
||||||
|
* @return EraserType
|
||||||
|
*/
|
||||||
|
EraserType getEraserType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Eraser Type object
|
||||||
|
*
|
||||||
|
* @param eraserType
|
||||||
|
*/
|
||||||
|
void setEraserType(EraserType eraserType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Fill of the shape is enabled
|
||||||
|
*/
|
||||||
|
bool getFill() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fill Fill of the shape is enabled
|
||||||
|
*/
|
||||||
|
void setFill(bool fill);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Alpha for fill
|
||||||
|
*/
|
||||||
|
int getFillAlpha() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fillAlpha Alpha for fill
|
||||||
|
*/
|
||||||
|
void setFillAlpha(int fillAlpha);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style of the line drawing
|
||||||
|
*/
|
||||||
|
const LineStyle& getLineStyle() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param style Style of the line drawing
|
||||||
|
*/
|
||||||
|
void setLineStyle(const LineStyle& style);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Color of the tool for all drawing tools
|
||||||
|
*/
|
||||||
|
Color color = Colors::black;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of a drawing tool
|
||||||
|
*/
|
||||||
|
ToolSize size = TOOL_SIZE_MEDIUM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw special shape
|
||||||
|
*/
|
||||||
|
DrawingType drawingType = DRAWING_TYPE_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eraser type
|
||||||
|
*/
|
||||||
|
EraserType eraserType = ERASER_TYPE_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill of the shape is enabled
|
||||||
|
*/
|
||||||
|
bool fill = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alpha for fill
|
||||||
|
*/
|
||||||
|
int fillAlpha = 128;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style of the line drawing
|
||||||
|
*/
|
||||||
|
LineStyle lineStyle;
|
||||||
|
};
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#include "ToolEnums.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for find, remove_if
|
||||||
|
|
||||||
|
#include "model/StrokeStyle.h"
|
||||||
|
#include "util/Assert.h"
|
||||||
|
|
||||||
|
|
||||||
|
auto toolSizeFromString(const std::string& size) -> ToolSize {
|
||||||
|
auto it = std::find(toolSizeNames.begin(), toolSizeNames.end(), size);
|
||||||
|
if (it != toolSizeNames.end()) {
|
||||||
|
return static_cast<ToolSize>(std::distance(toolSizeNames.begin(), it));
|
||||||
|
}
|
||||||
|
return TOOL_SIZE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto drawingTypeFromString(const std::string& type) -> DrawingType {
|
||||||
|
auto it = std::find(drawingTypeNames.begin(), drawingTypeNames.end(), type);
|
||||||
|
if (it != drawingTypeNames.end()) {
|
||||||
|
return static_cast<DrawingType>(std::distance(drawingTypeNames.begin(), it));
|
||||||
|
}
|
||||||
|
return DRAWING_TYPE_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isSelectToolType(ToolType type) -> bool {
|
||||||
|
return type == TOOL_SELECT_RECT || type == TOOL_SELECT_REGION || type == TOOL_SELECT_MULTILAYER_RECT ||
|
||||||
|
type == TOOL_SELECT_MULTILAYER_REGION || type == TOOL_SELECT_OBJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isSelectToolTypeSingleLayer(ToolType type) -> bool {
|
||||||
|
return type == TOOL_SELECT_RECT || type == TOOL_SELECT_REGION || type == TOOL_SELECT_OBJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto requiresClearedSelection(ToolType type) -> bool {
|
||||||
|
return type == TOOL_PEN || type == TOOL_HIGHLIGHTER || type == TOOL_ERASER || type == TOOL_TEXT ||
|
||||||
|
type == TOOL_IMAGE || type == TOOL_SELECT_PDF_TEXT_RECT || type == TOOL_SELECT_PDF_TEXT_RECT ||
|
||||||
|
type == TOOL_VERTICAL_SPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto toolTypeFromString(const std::string& type) -> ToolType {
|
||||||
|
auto it = std::find(toolNames.begin(), toolNames.end(), type);
|
||||||
|
if (it != toolNames.end()) {
|
||||||
|
return static_cast<ToolType>(std::distance(toolNames.begin(), it));
|
||||||
|
}
|
||||||
|
// recognize previous spelling of Highlighter, V1.0.19 (Dec 2020) and earlier
|
||||||
|
if (type == "hilighter") {
|
||||||
|
return TOOL_HIGHLIGHTER;
|
||||||
|
}
|
||||||
|
// recognize previous spelling of Ellipse, V1.1.0+dev (Jan 2021) and earlier
|
||||||
|
if (type == "drawCircle") {
|
||||||
|
return TOOL_DRAW_ELLIPSE;
|
||||||
|
}
|
||||||
|
return TOOL_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto opacityFeatureFromString(const std::string& feature) -> OpacityFeature {
|
||||||
|
auto it = std::find(opacityFeatureNames.begin(), opacityFeatureNames.end(), feature);
|
||||||
|
if (it != opacityFeatureNames.end()) {
|
||||||
|
return static_cast<OpacityFeature>(std::distance(opacityFeatureNames.begin(), it));
|
||||||
|
}
|
||||||
|
return OPACITY_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto eraserTypeFromString(const std::string& type) -> EraserType {
|
||||||
|
auto it = std::find(eraserTypeNames.begin(), eraserTypeNames.end(), type);
|
||||||
|
if (it != eraserTypeNames.end()) {
|
||||||
|
return static_cast<EraserType>(std::distance(eraserTypeNames.begin(), it));
|
||||||
|
}
|
||||||
|
return ERASER_TYPE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto strokeTypeToLineStyle(StrokeType type) -> LineStyle {
|
||||||
|
switch (type) {
|
||||||
|
case STROKE_TYPE_STANDARD:
|
||||||
|
return {};
|
||||||
|
case STROKE_TYPE_DASHED:
|
||||||
|
return StrokeStyle::parseStyle("dash");
|
||||||
|
case STROKE_TYPE_DASHDOTTED:
|
||||||
|
return StrokeStyle::parseStyle("dashdot");
|
||||||
|
case STROKE_TYPE_DOTTED:
|
||||||
|
return StrokeStyle::parseStyle("dot");
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto strokeTypeFromString(const std::string& type) -> StrokeType {
|
||||||
|
auto it = std::find(strokeTypeNames.begin(), strokeTypeNames.end(), type);
|
||||||
|
if (it != strokeTypeNames.end()) {
|
||||||
|
return static_cast<StrokeType>(std::distance(strokeTypeNames.begin(), it));
|
||||||
|
}
|
||||||
|
return STROKE_TYPE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool xoj::tool::isPdfSelectionTool(ToolType toolType) {
|
||||||
|
return toolType == TOOL_SELECT_PDF_TEXT_LINEAR || toolType == TOOL_SELECT_PDF_TEXT_RECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr auto makeToolCaps() {
|
||||||
|
std::array<std::underlying_type_t<ToolCapabilities>, TOOL_COUNT> caps;
|
||||||
|
|
||||||
|
caps[TOOL_PEN - TOOL_PEN] = TOOL_CAP_COLOR | TOOL_CAP_SIZE | TOOL_CAP_RULER | TOOL_CAP_RECTANGLE |
|
||||||
|
TOOL_CAP_ELLIPSE | TOOL_CAP_ARROW | TOOL_CAP_DOUBLE_ARROW | TOOL_CAP_SPLINE |
|
||||||
|
TOOL_CAP_RECOGNIZER | TOOL_CAP_FILL | TOOL_CAP_LINE_STYLE;
|
||||||
|
caps[TOOL_ERASER - TOOL_PEN] = TOOL_CAP_SIZE;
|
||||||
|
caps[TOOL_HIGHLIGHTER - TOOL_PEN] = TOOL_CAP_COLOR | TOOL_CAP_SIZE | TOOL_CAP_RULER | TOOL_CAP_RECTANGLE |
|
||||||
|
TOOL_CAP_ELLIPSE | TOOL_CAP_ARROW | TOOL_CAP_DOUBLE_ARROW | TOOL_CAP_SPLINE |
|
||||||
|
TOOL_CAP_RECOGNIZER | TOOL_CAP_FILL;
|
||||||
|
caps[TOOL_TEXT - TOOL_PEN] = TOOL_CAP_COLOR;
|
||||||
|
caps[TOOL_IMAGE - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_SELECT_RECT - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_SELECT_REGION - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_SELECT_MULTILAYER_RECT - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_SELECT_MULTILAYER_REGION - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_SELECT_OBJECT - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_VERTICAL_SPACE - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_HAND - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_PLAY_OBJECT - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_DRAW_RECT - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_DRAW_ELLIPSE - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_DRAW_ARROW - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_DRAW_DOUBLE_ARROW - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_DRAW_COORDINATE_SYSTEM - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_DRAW_SPLINE - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_FLOATING_TOOLBOX - TOOL_PEN] = TOOL_CAP_NONE;
|
||||||
|
caps[TOOL_SELECT_PDF_TEXT_LINEAR - TOOL_PEN] = TOOL_CAP_COLOR;
|
||||||
|
caps[TOOL_SELECT_PDF_TEXT_RECT - TOOL_PEN] = TOOL_CAP_COLOR;
|
||||||
|
caps[TOOL_LASER_POINTER_PEN - TOOL_PEN] = TOOL_CAP_COLOR | TOOL_CAP_SIZE;
|
||||||
|
caps[TOOL_LASER_POINTER_HIGHLIGHTER - TOOL_PEN] = TOOL_CAP_COLOR | TOOL_CAP_SIZE;
|
||||||
|
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto xoj::tool::typeToCapabilities(ToolType type) -> ToolCapabilities {
|
||||||
|
static constexpr auto caps = makeToolCaps();
|
||||||
|
xoj_assert(type >= TOOL_PEN && type - TOOL_PEN < TOOL_COUNT);
|
||||||
|
return static_cast<ToolCapabilities>(caps[type - TOOL_PEN]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Enum definition used for tools
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "model/LineStyle.h"
|
||||||
|
|
||||||
|
enum ToolSize {
|
||||||
|
TOOL_SIZE_VERY_FINE = 0,
|
||||||
|
TOOL_SIZE_FINE,
|
||||||
|
TOOL_SIZE_MEDIUM,
|
||||||
|
TOOL_SIZE_THICK,
|
||||||
|
TOOL_SIZE_VERY_THICK,
|
||||||
|
// None has to be at the end, because this enum is used as memory offset
|
||||||
|
TOOL_SIZE_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr std::array<std::string_view, 6> toolSizeNames{"veryThin", "thin", "medium",
|
||||||
|
"thick", "veryThick", "none"};
|
||||||
|
|
||||||
|
static constexpr std::string_view toolSizeToString(ToolSize size) {
|
||||||
|
return toolSizeNames.at(static_cast<size_t>(size));
|
||||||
|
}
|
||||||
|
ToolSize toolSizeFromString(const std::string& size);
|
||||||
|
|
||||||
|
|
||||||
|
enum DrawingType {
|
||||||
|
/**
|
||||||
|
* For config entry, don't change value
|
||||||
|
*/
|
||||||
|
DRAWING_TYPE_DONT_CHANGE = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default drawing, nothing special
|
||||||
|
*/
|
||||||
|
DRAWING_TYPE_DEFAULT,
|
||||||
|
DRAWING_TYPE_LINE,
|
||||||
|
DRAWING_TYPE_RECTANGLE,
|
||||||
|
DRAWING_TYPE_ELLIPSE,
|
||||||
|
DRAWING_TYPE_ARROW,
|
||||||
|
DRAWING_TYPE_DOUBLE_ARROW,
|
||||||
|
DRAWING_TYPE_COORDINATE_SYSTEM,
|
||||||
|
DRAWING_TYPE_SHAPE_RECOGNIZER,
|
||||||
|
DRAWING_TYPE_SPLINE
|
||||||
|
};
|
||||||
|
static constexpr std::array<std::string_view, 10> drawingTypeNames{
|
||||||
|
"dontChange", "default", "line", "rectangle", "ellipse", "arrow", "doubleArrow",
|
||||||
|
"drawCoordinateSystem", "strokeRecognizer", "spline"};
|
||||||
|
|
||||||
|
static constexpr std::string_view drawingTypeToString(DrawingType type) {
|
||||||
|
return drawingTypeNames.at(static_cast<size_t>(type));
|
||||||
|
}
|
||||||
|
DrawingType drawingTypeFromString(const std::string& type);
|
||||||
|
|
||||||
|
// The numbers must agree with the action's targets in ui/mainmenubar.xml
|
||||||
|
enum ToolType {
|
||||||
|
TOOL_NONE = 0,
|
||||||
|
|
||||||
|
// First valid tool, often used starting Index 0
|
||||||
|
TOOL_PEN = 1,
|
||||||
|
TOOL_ERASER = 2,
|
||||||
|
TOOL_HIGHLIGHTER = 3,
|
||||||
|
TOOL_TEXT = 4,
|
||||||
|
TOOL_IMAGE = 5,
|
||||||
|
TOOL_SELECT_RECT = 6,
|
||||||
|
TOOL_SELECT_REGION = 7,
|
||||||
|
TOOL_SELECT_MULTILAYER_RECT = 8,
|
||||||
|
TOOL_SELECT_MULTILAYER_REGION = 9,
|
||||||
|
TOOL_SELECT_OBJECT = 10,
|
||||||
|
TOOL_PLAY_OBJECT = 11,
|
||||||
|
TOOL_VERTICAL_SPACE = 12,
|
||||||
|
TOOL_HAND = 13,
|
||||||
|
TOOL_DRAW_RECT = 14,
|
||||||
|
TOOL_DRAW_ELLIPSE = 15,
|
||||||
|
TOOL_DRAW_ARROW = 16,
|
||||||
|
TOOL_DRAW_DOUBLE_ARROW = 17,
|
||||||
|
TOOL_DRAW_COORDINATE_SYSTEM = 18,
|
||||||
|
TOOL_FLOATING_TOOLBOX = 19,
|
||||||
|
TOOL_DRAW_SPLINE = 20,
|
||||||
|
TOOL_SELECT_PDF_TEXT_LINEAR = 21,
|
||||||
|
TOOL_SELECT_PDF_TEXT_RECT = 22,
|
||||||
|
TOOL_LASER_POINTER_PEN = 23,
|
||||||
|
TOOL_LASER_POINTER_HIGHLIGHTER = 24,
|
||||||
|
|
||||||
|
TOOL_END_ENTRY
|
||||||
|
};
|
||||||
|
static constexpr std::array<std::string_view, 25> toolNames{"none",
|
||||||
|
"pen",
|
||||||
|
"eraser",
|
||||||
|
"highlighter",
|
||||||
|
"text",
|
||||||
|
"image",
|
||||||
|
"selectRect",
|
||||||
|
"selectRegion",
|
||||||
|
"selectMultiLayerRect",
|
||||||
|
"selectMultiLayerRegion",
|
||||||
|
"selectObject",
|
||||||
|
"playObject",
|
||||||
|
"verticalSpace",
|
||||||
|
"hand",
|
||||||
|
"drawRect",
|
||||||
|
"drawEllipse",
|
||||||
|
"drawArrow",
|
||||||
|
"drawDoubleArrow",
|
||||||
|
"drawCoordinateSystem",
|
||||||
|
"showFloatingToolbox",
|
||||||
|
"drawSpline",
|
||||||
|
"selectPdfTextLinear",
|
||||||
|
"selectPdfTextRect",
|
||||||
|
"laserPointerPen",
|
||||||
|
"laserPointerHighlighter"};
|
||||||
|
|
||||||
|
auto isSelectToolType(ToolType type) -> bool;
|
||||||
|
auto isSelectToolTypeSingleLayer(ToolType type) -> bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether or not the tool needs the selection to be cleared when selected
|
||||||
|
*/
|
||||||
|
auto requiresClearedSelection(ToolType type) -> bool;
|
||||||
|
|
||||||
|
// The count of tools
|
||||||
|
#define TOOL_COUNT (TOOL_END_ENTRY - 1)
|
||||||
|
|
||||||
|
static constexpr std::string_view toolTypeToString(ToolType type) { return toolNames.at(static_cast<size_t>(type)); }
|
||||||
|
ToolType toolTypeFromString(const std::string& type);
|
||||||
|
|
||||||
|
|
||||||
|
enum OpacityFeature {
|
||||||
|
OPACITY_NONE,
|
||||||
|
OPACITY_FILL_PEN,
|
||||||
|
OPACITY_FILL_HIGHLIGHTER,
|
||||||
|
OPACITY_SELECT_PDF_TEXT_MARKER,
|
||||||
|
};
|
||||||
|
static constexpr std::array<std::string_view, 4> opacityFeatureNames{"none", "opacityFillPen", "opacityFillHighlighter",
|
||||||
|
"opacitySelectPdfTextMarker"};
|
||||||
|
|
||||||
|
static constexpr std::string_view opacityFeatureToString(OpacityFeature feature) {
|
||||||
|
return opacityFeatureNames.at(static_cast<size_t>(feature));
|
||||||
|
}
|
||||||
|
OpacityFeature opacityFeatureFromString(const std::string& feature);
|
||||||
|
|
||||||
|
enum EraserType { ERASER_TYPE_NONE = 0, ERASER_TYPE_DEFAULT, ERASER_TYPE_WHITEOUT, ERASER_TYPE_DELETE_STROKE };
|
||||||
|
static constexpr std::array<std::string_view, 4> eraserTypeNames{"none", "default", "whiteout", "deleteStroke"};
|
||||||
|
|
||||||
|
static constexpr std::string_view eraserTypeToString(EraserType type) {
|
||||||
|
return eraserTypeNames.at(static_cast<size_t>(type));
|
||||||
|
}
|
||||||
|
EraserType eraserTypeFromString(const std::string& type);
|
||||||
|
|
||||||
|
|
||||||
|
enum ToolCapabilities : unsigned int {
|
||||||
|
TOOL_CAP_NONE = 0,
|
||||||
|
TOOL_CAP_COLOR = 1 << 0,
|
||||||
|
TOOL_CAP_SIZE = 1 << 1,
|
||||||
|
TOOL_CAP_RULER = 1 << 2,
|
||||||
|
TOOL_CAP_RECTANGLE = 1 << 3,
|
||||||
|
TOOL_CAP_ELLIPSE = 1 << 4,
|
||||||
|
TOOL_CAP_ARROW = 1 << 5,
|
||||||
|
TOOL_CAP_DOUBLE_ARROW = 1 << 6,
|
||||||
|
TOOL_CAP_RECOGNIZER = 1 << 7,
|
||||||
|
TOOL_CAP_FILL = 1 << 8,
|
||||||
|
TOOL_CAP_COORDINATE_SYSTEM = 1 << 9,
|
||||||
|
TOOL_CAP_SPLINE = 1 << 10,
|
||||||
|
TOOL_CAP_LINE_STYLE = 1 << 11
|
||||||
|
};
|
||||||
|
|
||||||
|
enum StrokeType {
|
||||||
|
STROKE_TYPE_NONE = 0,
|
||||||
|
STROKE_TYPE_STANDARD = 1,
|
||||||
|
STROKE_TYPE_DASHED = 2,
|
||||||
|
STROKE_TYPE_DASHDOTTED = 3,
|
||||||
|
STROKE_TYPE_DOTTED = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr std::array<std::string_view, 5> strokeTypeNames{"none", "standard", "dashed", "dashdot", "dot"};
|
||||||
|
|
||||||
|
auto strokeTypeFromString(const std::string& type) -> StrokeType;
|
||||||
|
auto strokeTypeToLineStyle(StrokeType type) -> LineStyle;
|
||||||
|
static constexpr auto strokeTypeToString(StrokeType type) -> std::string_view {
|
||||||
|
return strokeTypeNames.at(static_cast<size_t>(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace xoj::tool {
|
||||||
|
/// \return Whether the provided tool is used for selecting objects on a PDF.
|
||||||
|
bool isPdfSelectionTool(ToolType toolType);
|
||||||
|
|
||||||
|
ToolCapabilities typeToCapabilities(ToolType type);
|
||||||
|
static inline bool hasCapability(ToolType type, ToolCapabilities cap) { return typeToCapabilities(type) & cap; }
|
||||||
|
} // namespace xoj::tool
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
#include "ToolHandler.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for clamp
|
||||||
|
#include <cstdint> // for uint32_t
|
||||||
|
#include <cstdio> // for size_t
|
||||||
|
#include <optional> // for nullopt, optional
|
||||||
|
#include <string> // for operator==, string, basic_string
|
||||||
|
#include <utility> // for move
|
||||||
|
|
||||||
|
#include <glib.h> // for g_warning, g_error
|
||||||
|
|
||||||
|
#include "control/Tool.h" // for Tool, Tool::toolSizes
|
||||||
|
#include "control/actions/ActionDatabase.h"
|
||||||
|
#include "control/settings/Settings.h" // for SElement, Settings
|
||||||
|
#include "model/StrokeStyle.h" // for StrokeStyle
|
||||||
|
#include "util/Assert.h"
|
||||||
|
#include "util/Color.h"
|
||||||
|
#include "util/Stacktrace.h" // for Stac...
|
||||||
|
#include "util/safe_casts.h" // for as_unsigned
|
||||||
|
|
||||||
|
|
||||||
|
class LineStyle;
|
||||||
|
|
||||||
|
|
||||||
|
ToolListener::~ToolListener() = default;
|
||||||
|
|
||||||
|
ToolHandler::ToolHandler(ToolListener* stateChangeListener, ActionDatabase* actionDB, Settings* settings):
|
||||||
|
stateChangeListener(stateChangeListener), actionDB(actionDB), settings(settings) {
|
||||||
|
initTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolSelectPDFText: public Tool {
|
||||||
|
public:
|
||||||
|
ToolSelectPDFText(std::string name, ToolType type, Color color): Tool(name, type, color, std::nullopt) {}
|
||||||
|
|
||||||
|
~ToolSelectPDFText() override{};
|
||||||
|
|
||||||
|
void setColor(Color color) override {
|
||||||
|
if (color.alpha == 0) {
|
||||||
|
color.alpha = DEFAULT_SELECT_PDF_TEXT_MARKER_OPACITY;
|
||||||
|
}
|
||||||
|
Tool::setColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const int DEFAULT_SELECT_PDF_TEXT_MARKER_OPACITY = 60;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ToolHandler::initTools() {
|
||||||
|
std::array<double, Tool::toolSizes> thickness;
|
||||||
|
// pen thicknesses = 0.15, 0.3, 0.5, 0.8, 2 mm
|
||||||
|
thickness[TOOL_SIZE_VERY_FINE] = 0.42;
|
||||||
|
thickness[TOOL_SIZE_FINE] = 0.85;
|
||||||
|
thickness[TOOL_SIZE_MEDIUM] = 1.41;
|
||||||
|
thickness[TOOL_SIZE_THICK] = 2.26;
|
||||||
|
thickness[TOOL_SIZE_VERY_THICK] = 5.67;
|
||||||
|
tools[TOOL_PEN - TOOL_PEN] = std::make_unique<Tool>("pen", TOOL_PEN, Colors::xopp_royalblue, thickness);
|
||||||
|
|
||||||
|
thickness[TOOL_SIZE_VERY_FINE] = 1;
|
||||||
|
thickness[TOOL_SIZE_FINE] = 2.83;
|
||||||
|
thickness[TOOL_SIZE_MEDIUM] = 8.50;
|
||||||
|
thickness[TOOL_SIZE_THICK] = 12;
|
||||||
|
thickness[TOOL_SIZE_VERY_THICK] = 18;
|
||||||
|
tools[TOOL_ERASER - TOOL_PEN] = std::make_unique<Tool>("eraser", TOOL_ERASER, Colors::black, thickness);
|
||||||
|
|
||||||
|
// highlighter thicknesses = 1, 3, 7 mm
|
||||||
|
thickness[TOOL_SIZE_VERY_FINE] = 1;
|
||||||
|
thickness[TOOL_SIZE_FINE] = 2.83;
|
||||||
|
thickness[TOOL_SIZE_MEDIUM] = 8.50;
|
||||||
|
thickness[TOOL_SIZE_THICK] = 19.84;
|
||||||
|
thickness[TOOL_SIZE_VERY_THICK] = 30;
|
||||||
|
tools[TOOL_HIGHLIGHTER - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("highlighter", TOOL_HIGHLIGHTER, Colors::yellow, thickness);
|
||||||
|
|
||||||
|
tools[TOOL_TEXT - TOOL_PEN] = std::make_unique<Tool>("text", TOOL_TEXT, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_IMAGE - TOOL_PEN] = std::make_unique<Tool>("image", TOOL_IMAGE, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_RECT - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("selectRect", TOOL_SELECT_RECT, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_REGION - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("selectRegion", TOOL_SELECT_REGION, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_MULTILAYER_RECT - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("selectMultiLayerRect", TOOL_SELECT_MULTILAYER_RECT, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_MULTILAYER_REGION - TOOL_PEN] = std::make_unique<Tool>(
|
||||||
|
"selectMultiLayerRegion", TOOL_SELECT_MULTILAYER_REGION, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_OBJECT - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("selectObject", TOOL_SELECT_OBJECT, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_VERTICAL_SPACE - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("verticalSpace", TOOL_VERTICAL_SPACE, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_HAND - TOOL_PEN] = std::make_unique<Tool>("hand", TOOL_HAND, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_PLAY_OBJECT - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("playObject", TOOL_PLAY_OBJECT, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_DRAW_RECT - TOOL_PEN] = std::make_unique<Tool>("drawRect", TOOL_DRAW_RECT, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_DRAW_ELLIPSE - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("drawEllipse", TOOL_DRAW_ELLIPSE, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_DRAW_ARROW - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("drawArrow", TOOL_DRAW_ARROW, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_DRAW_DOUBLE_ARROW - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("drawDoubleArrow", TOOL_DRAW_DOUBLE_ARROW, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_DRAW_COORDINATE_SYSTEM - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("drawCoordinateSystem", TOOL_DRAW_COORDINATE_SYSTEM, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_DRAW_SPLINE - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("drawSpline", TOOL_DRAW_SPLINE, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_FLOATING_TOOLBOX - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("showFloatingToolbox", TOOL_FLOATING_TOOLBOX, Colors::black, std::nullopt);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_PDF_TEXT_LINEAR - TOOL_PEN] =
|
||||||
|
std::make_unique<ToolSelectPDFText>("selectPdfTextLinear", TOOL_SELECT_PDF_TEXT_LINEAR, Colors::black);
|
||||||
|
|
||||||
|
tools[TOOL_SELECT_PDF_TEXT_RECT - TOOL_PEN] =
|
||||||
|
std::make_unique<ToolSelectPDFText>("selectPdfTextRect", TOOL_SELECT_PDF_TEXT_RECT, Colors::black);
|
||||||
|
|
||||||
|
thickness[TOOL_SIZE_VERY_FINE] = .7;
|
||||||
|
thickness[TOOL_SIZE_FINE] = 1.41;
|
||||||
|
thickness[TOOL_SIZE_MEDIUM] = 2.4;
|
||||||
|
thickness[TOOL_SIZE_THICK] = 4;
|
||||||
|
thickness[TOOL_SIZE_VERY_THICK] = 7;
|
||||||
|
tools[TOOL_LASER_POINTER_PEN - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("laserPointerPen", TOOL_LASER_POINTER_PEN, Colors::red, thickness);
|
||||||
|
thickness[TOOL_SIZE_VERY_FINE] = 1;
|
||||||
|
thickness[TOOL_SIZE_FINE] = 2.83;
|
||||||
|
thickness[TOOL_SIZE_MEDIUM] = 8.50;
|
||||||
|
thickness[TOOL_SIZE_THICK] = 19.84;
|
||||||
|
thickness[TOOL_SIZE_VERY_THICK] = 30;
|
||||||
|
tools[TOOL_LASER_POINTER_HIGHLIGHTER - TOOL_PEN] =
|
||||||
|
std::make_unique<Tool>("laserPointerHighlighter", TOOL_LASER_POINTER_HIGHLIGHTER, Colors::red, thickness);
|
||||||
|
|
||||||
|
this->toolbarSelectedTool = &getTool(TOOL_PEN);
|
||||||
|
this->activeTool = &getTool(TOOL_PEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolHandler::~ToolHandler() {
|
||||||
|
// Do not delete settings!
|
||||||
|
this->settings = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setEraserType(EraserType eraserType) {
|
||||||
|
Tool& tool = this->getTool(TOOL_ERASER);
|
||||||
|
tool.setEraserType(eraserType);
|
||||||
|
eraserTypeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setButtonEraserType(EraserType eraserType, Button button) {
|
||||||
|
Tool* tool = getButtonTool(button);
|
||||||
|
xoj_assert(tool);
|
||||||
|
tool->setEraserType(eraserType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::eraserTypeChanged() {
|
||||||
|
if (this->actionDB == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->actionDB->setActionState(Action::TOOL_ERASER_TYPE, this->getEraserType());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getEraserType() const -> EraserType {
|
||||||
|
// if active tool is eraser get its type
|
||||||
|
if (this->activeTool->type == TOOL_ERASER)
|
||||||
|
return this->activeTool->getEraserType();
|
||||||
|
else // otherwise get EraserType of Toolbar Eraser
|
||||||
|
return this->getTool(TOOL_ERASER).getEraserType();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::selectTool(ToolType type) {
|
||||||
|
if (type < 1 || type > TOOL_COUNT) {
|
||||||
|
g_warning("unknown tool selected: %i\n", type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->toolbarSelectedTool = &getTool(type);
|
||||||
|
// set activeTool is necessary for fireToolChanged()
|
||||||
|
// if called after this method
|
||||||
|
// to result in the correct Button shown as active
|
||||||
|
this->activeTool = this->toolbarSelectedTool;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::fireToolChanged() const {
|
||||||
|
for (auto&& listener: this->toolChangeListeners) { listener(this->activeTool->type); }
|
||||||
|
|
||||||
|
stateChangeListener->toolChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::addToolChangedListener(ToolChangedCallback listener) {
|
||||||
|
toolChangeListeners.emplace_back(std::move(listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getTool(ToolType type) const -> Tool& { return *(this->tools[type - TOOL_PEN]); }
|
||||||
|
|
||||||
|
auto ToolHandler::getActiveTool() const -> Tool* {return this->activeTool; }
|
||||||
|
|
||||||
|
auto ToolHandler::getToolType() const -> ToolType {
|
||||||
|
Tool* tool = this->activeTool;
|
||||||
|
return tool->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::hasCapability(ToolCapabilities cap, SelectedTool selectedTool) const -> bool {
|
||||||
|
Tool* tool = getSelectedTool(selectedTool);
|
||||||
|
return (tool->capabilities & cap) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::isDrawingTool() const -> bool {
|
||||||
|
Tool* tool = this->activeTool;
|
||||||
|
return tool->isDrawingTool();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getSize(SelectedTool selectedTool) const -> ToolSize {
|
||||||
|
Tool* tool = getSelectedTool(selectedTool);
|
||||||
|
return tool->getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getPenSize() const -> ToolSize { return tools[TOOL_PEN - TOOL_PEN]->getSize(); }
|
||||||
|
|
||||||
|
auto ToolHandler::getEraserSize() const -> ToolSize { return tools[TOOL_ERASER - TOOL_PEN]->getSize(); }
|
||||||
|
|
||||||
|
auto ToolHandler::getHighlighterSize() const -> ToolSize { return tools[TOOL_HIGHLIGHTER - TOOL_PEN]->getSize(); }
|
||||||
|
|
||||||
|
void ToolHandler::setPenSize(ToolSize size) {
|
||||||
|
this->tools[TOOL_PEN - TOOL_PEN]->setSize(size);
|
||||||
|
|
||||||
|
if (this->activeTool->type == TOOL_PEN) {
|
||||||
|
this->stateChangeListener->toolSizeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setEraserSize(ToolSize size) {
|
||||||
|
this->tools[TOOL_ERASER - TOOL_PEN]->setSize(size);
|
||||||
|
|
||||||
|
if (this->activeTool->type == TOOL_ERASER) {
|
||||||
|
this->stateChangeListener->toolSizeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setHighlighterSize(ToolSize size) {
|
||||||
|
this->tools[TOOL_HIGHLIGHTER - TOOL_PEN]->setSize(size);
|
||||||
|
|
||||||
|
if (this->activeTool->type == TOOL_HIGHLIGHTER) {
|
||||||
|
this->stateChangeListener->toolSizeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setPenFillEnabled(bool fill) {
|
||||||
|
this->tools[TOOL_PEN - TOOL_PEN]->setFill(fill);
|
||||||
|
|
||||||
|
if (this->activeTool->type == TOOL_PEN) {
|
||||||
|
this->stateChangeListener->toolFillChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getPenFillEnabled() const -> bool { return this->tools[TOOL_PEN - TOOL_PEN]->getFill(); }
|
||||||
|
|
||||||
|
void ToolHandler::setPenFill(int alpha) { this->tools[TOOL_PEN - TOOL_PEN]->setFillAlpha(alpha); }
|
||||||
|
|
||||||
|
auto ToolHandler::getPenFill() const -> int { return this->tools[TOOL_PEN - TOOL_PEN]->getFillAlpha(); }
|
||||||
|
|
||||||
|
void ToolHandler::setHighlighterFillEnabled(bool fill) {
|
||||||
|
this->tools[TOOL_HIGHLIGHTER - TOOL_PEN]->setFill(fill);
|
||||||
|
|
||||||
|
if (this->activeTool->type == TOOL_HIGHLIGHTER) {
|
||||||
|
this->stateChangeListener->toolFillChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getHighlighterFillEnabled() const -> bool {
|
||||||
|
return this->tools[TOOL_HIGHLIGHTER - TOOL_PEN]->getFill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setHighlighterFill(int alpha) { this->tools[TOOL_HIGHLIGHTER - TOOL_PEN]->setFillAlpha(alpha); }
|
||||||
|
|
||||||
|
auto ToolHandler::getHighlighterFill() const -> int { return this->tools[TOOL_HIGHLIGHTER - TOOL_PEN]->getFillAlpha(); }
|
||||||
|
|
||||||
|
static void inline setColorAlpha(Tool& tool, int alpha) {
|
||||||
|
if (tool.hasCapability(TOOL_CAP_COLOR)) {
|
||||||
|
Color color = tool.getColor();
|
||||||
|
color.alpha = static_cast<uint8_t>(alpha);
|
||||||
|
tool.setColor(color);
|
||||||
|
} else {
|
||||||
|
g_warning("setColorAlpha : tool='%s' has no color capability!", tool.getName().c_str());
|
||||||
|
Stacktrace::printStacktrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setSelectPDFTextMarkerOpacity(int alpha) {
|
||||||
|
// Use same marker opacity for 'select linear pdf text' or 'select pdf text in rectangle'
|
||||||
|
setColorAlpha(this->getTool(TOOL_SELECT_PDF_TEXT_LINEAR), alpha);
|
||||||
|
setColorAlpha(this->getTool(TOOL_SELECT_PDF_TEXT_RECT), alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getSelectPDFTextMarkerOpacity() const -> int {
|
||||||
|
// Use same marker opacity for 'select linear pdf text' or 'select pdf text in rectangle'
|
||||||
|
return this->getTool(TOOL_SELECT_PDF_TEXT_LINEAR).getColor().alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getThickness() const -> double {
|
||||||
|
Tool* tool = this->activeTool;
|
||||||
|
if (tool->thickness) {
|
||||||
|
return tool->thickness.value()[tool->getSize()];
|
||||||
|
}
|
||||||
|
|
||||||
|
g_warning("Request size of \"%s\"", tool->getName().c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setSize(ToolSize size) {
|
||||||
|
ToolSize clippedSize = std::clamp(size, TOOL_SIZE_VERY_FINE, TOOL_SIZE_VERY_THICK);
|
||||||
|
if (clippedSize != size)
|
||||||
|
g_warning("ToolHandler::setSize: Invalid size! %i", size);
|
||||||
|
|
||||||
|
Tool* tool = this->toolbarSelectedTool;
|
||||||
|
tool->setSize(clippedSize);
|
||||||
|
this->stateChangeListener->toolSizeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setButtonSize(ToolSize size, Button button) {
|
||||||
|
ToolSize clippedSize = std::clamp(size, TOOL_SIZE_VERY_FINE, TOOL_SIZE_VERY_THICK);
|
||||||
|
if (clippedSize != size)
|
||||||
|
g_warning("ToolHandler::setSize: Invalid size! %i", size);
|
||||||
|
|
||||||
|
Tool* tool = getButtonTool(button);
|
||||||
|
xoj_assert(tool);
|
||||||
|
tool->setSize(clippedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setLineStyle(const LineStyle& style) {
|
||||||
|
Tool* tool = this->toolbarSelectedTool;
|
||||||
|
tool->setLineStyle(style);
|
||||||
|
this->stateChangeListener->toolLineStyleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setColor(Color color, bool userSelection) {
|
||||||
|
if (this->activeTool != this->toolbarSelectedTool && !this->hasCapability(TOOL_CAP_COLOR, SelectedTool::active)) {
|
||||||
|
this->toolbarSelectedTool->setColor(color);
|
||||||
|
}
|
||||||
|
Tool* tool = this->activeTool;
|
||||||
|
int currentAlpha = tool->getColor().alpha;
|
||||||
|
tool->setColor(color);
|
||||||
|
this->stateChangeListener->toolColorChanged();
|
||||||
|
if (userSelection) {
|
||||||
|
// ensure that tool color alpha is re-applied on new selected color;
|
||||||
|
setColorAlpha(*tool, currentAlpha);
|
||||||
|
this->stateChangeListener->changeColorOfSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setButtonColor(Color color, Button button) {
|
||||||
|
Tool* tool = this->getButtonTool(button);
|
||||||
|
xoj_assert(tool);
|
||||||
|
tool->setColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getColor() const -> Color { return this->activeTool->getColor(); }
|
||||||
|
|
||||||
|
auto ToolHandler::getColorMaskAlpha() const -> Color {
|
||||||
|
Color c = this->activeTool->getColor();
|
||||||
|
c.alpha = 0xff;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setFillEnabled(bool fill) {
|
||||||
|
Tool* tool = this->toolbarSelectedTool;
|
||||||
|
tool->setFill(fill);
|
||||||
|
this->stateChangeListener->toolFillChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getFill() const -> int {
|
||||||
|
Tool* tool = this->activeTool;
|
||||||
|
if (!tool->getFill()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return tool->getFillAlpha();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getLineStyle() const -> const LineStyle& {
|
||||||
|
Tool* tool = this->activeTool;
|
||||||
|
return tool->getLineStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getDrawingType(SelectedTool selectedTool) const -> DrawingType {
|
||||||
|
Tool* tool = getSelectedTool(selectedTool);
|
||||||
|
return tool->getDrawingType();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setDrawingType(DrawingType drawingType) {
|
||||||
|
Tool* tool = this->toolbarSelectedTool;
|
||||||
|
tool->setDrawingType(drawingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setButtonDrawingType(DrawingType drawingType, Button button) {
|
||||||
|
Tool* tool = getButtonTool(button);
|
||||||
|
xoj_assert(tool);
|
||||||
|
tool->setDrawingType(drawingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setButtonStrokeType(StrokeType strokeType, Button button) {
|
||||||
|
this->setButtonStrokeType(strokeTypeToLineStyle(strokeType), button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::setButtonStrokeType(const LineStyle& lineStyle, Button button) {
|
||||||
|
Tool* tool = getButtonTool(button);
|
||||||
|
xoj_assert(tool);
|
||||||
|
tool->setLineStyle(lineStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getTools() const -> std::array<std::unique_ptr<Tool>, TOOL_COUNT> const& { return tools; }
|
||||||
|
|
||||||
|
void ToolHandler::saveSettings() const {
|
||||||
|
SElement& s = settings->getCustomElement("tools");
|
||||||
|
s.clear();
|
||||||
|
|
||||||
|
s.setString("current", this->activeTool->getName());
|
||||||
|
|
||||||
|
for (auto&& tool: tools) {
|
||||||
|
SElement& st = s.child(tool->getName());
|
||||||
|
if (tool->hasCapability(TOOL_CAP_COLOR)) {
|
||||||
|
st.setIntHex("color", int(uint32_t(tool->getColor())));
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr unsigned int SHAPE_CAPS = TOOL_CAP_RULER | TOOL_CAP_RECTANGLE | TOOL_CAP_ELLIPSE |
|
||||||
|
TOOL_CAP_ARROW | TOOL_CAP_DOUBLE_ARROW | TOOL_CAP_RECOGNIZER |
|
||||||
|
TOOL_CAP_SPLINE;
|
||||||
|
if (tool->capabilities & SHAPE_CAPS) {
|
||||||
|
st.setString("drawingType", drawingTypeToString(tool->getDrawingType()).data());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool->hasCapability(TOOL_CAP_SIZE)) {
|
||||||
|
std::string value;
|
||||||
|
switch (tool->getSize()) {
|
||||||
|
case TOOL_SIZE_VERY_FINE:
|
||||||
|
value = "VERY_FINE";
|
||||||
|
break;
|
||||||
|
case TOOL_SIZE_FINE:
|
||||||
|
value = "THIN";
|
||||||
|
break;
|
||||||
|
case TOOL_SIZE_MEDIUM:
|
||||||
|
value = "MEDIUM";
|
||||||
|
break;
|
||||||
|
case TOOL_SIZE_THICK:
|
||||||
|
value = "BIG";
|
||||||
|
break;
|
||||||
|
case TOOL_SIZE_VERY_THICK:
|
||||||
|
value = "VERY_BIG";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
st.setString("size", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool->hasCapability(TOOL_CAP_FILL)) {
|
||||||
|
st.setInt("fill", tool->getFill());
|
||||||
|
st.setInt("fillAlpha", tool->getFillAlpha());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool->type == TOOL_PEN) {
|
||||||
|
st.setString("style", StrokeStyle::formatStyle(tool->getLineStyle()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool->type == TOOL_ERASER) {
|
||||||
|
if (this->getEraserType() == ERASER_TYPE_DELETE_STROKE) {
|
||||||
|
st.setString("type", "deleteStroke");
|
||||||
|
} else if (this->getEraserType() == ERASER_TYPE_WHITEOUT) {
|
||||||
|
st.setString("type", "whiteout");
|
||||||
|
} else // ERASER_TYPE_DEFAULT
|
||||||
|
{
|
||||||
|
st.setString("type", "default");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings->customSettingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::loadSettings() {
|
||||||
|
SElement& s = settings->getCustomElement("tools");
|
||||||
|
|
||||||
|
std::string selectedTool;
|
||||||
|
if (s.getString("current", selectedTool)) {
|
||||||
|
for (auto&& tool: tools) {
|
||||||
|
SElement& st = s.child(tool->getName());
|
||||||
|
|
||||||
|
if (selectedTool == tool->getName()) {
|
||||||
|
this->activeTool = tool.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
int iColor{};
|
||||||
|
if (tool->hasCapability(TOOL_CAP_COLOR) && st.getInt("color", iColor)) {
|
||||||
|
tool->setColor(Color(as_unsigned(iColor)));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string drawingType;
|
||||||
|
if (st.getString("drawingType", drawingType)) {
|
||||||
|
tool->setDrawingType(drawingTypeFromString(drawingType));
|
||||||
|
}
|
||||||
|
|
||||||
|
int fill = -1;
|
||||||
|
if (st.getInt("fill", fill)) {
|
||||||
|
tool->setFill(fill);
|
||||||
|
}
|
||||||
|
int fillAlpha = -1;
|
||||||
|
if (st.getInt("fillAlpha", fillAlpha)) {
|
||||||
|
tool->setFillAlpha(fillAlpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string style;
|
||||||
|
if (st.getString("style", style)) {
|
||||||
|
tool->setLineStyle(StrokeStyle::parseStyle(style));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string value;
|
||||||
|
if (tool->hasCapability(TOOL_CAP_SIZE) && st.getString("size", value)) {
|
||||||
|
if (value == "VERY_FINE") {
|
||||||
|
tool->setSize(TOOL_SIZE_VERY_FINE);
|
||||||
|
} else if (value == "THIN") {
|
||||||
|
tool->setSize(TOOL_SIZE_FINE);
|
||||||
|
} else if (value == "MEDIUM") {
|
||||||
|
tool->setSize(TOOL_SIZE_MEDIUM);
|
||||||
|
} else if (value == "BIG") {
|
||||||
|
tool->setSize(TOOL_SIZE_THICK);
|
||||||
|
} else if (value == "VERY_BIG") {
|
||||||
|
tool->setSize(TOOL_SIZE_VERY_THICK);
|
||||||
|
} else {
|
||||||
|
g_warning("Settings::Unknown tool size: %s\n", value.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool->type == TOOL_ERASER) {
|
||||||
|
std::string type;
|
||||||
|
|
||||||
|
if (st.getString("type", type)) {
|
||||||
|
if (type == "deleteStroke") {
|
||||||
|
setEraserType(ERASER_TYPE_DELETE_STROKE);
|
||||||
|
} else if (type == "whiteout") {
|
||||||
|
setEraserType(ERASER_TYPE_WHITEOUT);
|
||||||
|
} else {
|
||||||
|
setEraserType(ERASER_TYPE_DEFAULT);
|
||||||
|
}
|
||||||
|
eraserTypeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ToolHandler::pointActiveToolToButtonTool(Button button) {
|
||||||
|
Tool* tool = getButtonTool(button);
|
||||||
|
if (!tool || this->activeTool == tool) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->activeTool = tool;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ToolHandler::pointActiveToolToToolbarTool() {
|
||||||
|
if (this->activeTool == this->toolbarSelectedTool)
|
||||||
|
return false;
|
||||||
|
this->activeTool = this->toolbarSelectedTool;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getToolThickness(ToolType type) const -> const double* {
|
||||||
|
return this->tools[type - TOOL_PEN]->thickness.value().data();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the selection tools capabilities, depending on the selected elements
|
||||||
|
*/
|
||||||
|
void ToolHandler::setSelectionEditTools(bool setColor, bool setSize, bool setFill, bool setLineStyle) {
|
||||||
|
// For all selection tools, apply the features
|
||||||
|
for (size_t i = TOOL_SELECT_RECT - TOOL_PEN; i <= TOOL_SELECT_OBJECT - TOOL_PEN; i++) {
|
||||||
|
Tool* t = tools[i].get();
|
||||||
|
t->setCapability(TOOL_CAP_COLOR, setColor);
|
||||||
|
t->setCapability(TOOL_CAP_SIZE, setSize);
|
||||||
|
t->setCapability(TOOL_CAP_FILL, setFill);
|
||||||
|
t->setCapability(TOOL_CAP_LINE_STYLE, setLineStyle);
|
||||||
|
t->setSize(TOOL_SIZE_NONE);
|
||||||
|
t->setColor(Colors::white);
|
||||||
|
t->setFill(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->activeTool->type == TOOL_SELECT_RECT || this->activeTool->type == TOOL_SELECT_REGION ||
|
||||||
|
this->activeTool->type == TOOL_SELECT_MULTILAYER_RECT || this->activeTool->type == TOOL_SELECT_MULTILAYER_REGION ||
|
||||||
|
this->activeTool->type == TOOL_SELECT_OBJECT || this->activeTool->type == TOOL_PLAY_OBJECT) {
|
||||||
|
this->stateChangeListener->toolColorChanged();
|
||||||
|
this->stateChangeListener->toolSizeChanged();
|
||||||
|
this->stateChangeListener->toolFillChanged();
|
||||||
|
this->stateChangeListener->toolLineStyleChanged();
|
||||||
|
this->fireToolChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::isSinglePageTool() const -> bool {
|
||||||
|
ToolType toolType = this->getToolType();
|
||||||
|
DrawingType drawingType = this->getDrawingType();
|
||||||
|
|
||||||
|
return ((toolType == TOOL_PEN || toolType == TOOL_HIGHLIGHTER) &&
|
||||||
|
(drawingType == DRAWING_TYPE_ARROW || drawingType == DRAWING_TYPE_DOUBLE_ARROW ||
|
||||||
|
drawingType == DRAWING_TYPE_ELLIPSE || drawingType == DRAWING_TYPE_COORDINATE_SYSTEM ||
|
||||||
|
drawingType == DRAWING_TYPE_LINE || drawingType == DRAWING_TYPE_RECTANGLE ||
|
||||||
|
drawingType == DRAWING_TYPE_SPLINE)) ||
|
||||||
|
toolType == TOOL_SELECT_RECT || toolType == TOOL_SELECT_REGION || toolType == TOOL_SELECT_MULTILAYER_RECT ||
|
||||||
|
toolType == TOOL_SELECT_MULTILAYER_REGION || toolType == TOOL_SELECT_OBJECT ||
|
||||||
|
toolType == TOOL_DRAW_RECT || toolType == TOOL_DRAW_ELLIPSE || toolType == TOOL_DRAW_COORDINATE_SYSTEM ||
|
||||||
|
toolType == TOOL_DRAW_ARROW || toolType == TOOL_DRAW_DOUBLE_ARROW || toolType == TOOL_FLOATING_TOOLBOX ||
|
||||||
|
toolType == TOOL_DRAW_SPLINE || toolType == TOOL_SELECT_PDF_TEXT_LINEAR ||
|
||||||
|
toolType == TOOL_SELECT_PDF_TEXT_RECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::acceptsOutOfPageEvents() const -> bool {
|
||||||
|
ToolType toolType = this->getToolType();
|
||||||
|
DrawingType drawingType = this->getDrawingType();
|
||||||
|
|
||||||
|
return ((toolType == TOOL_PEN || toolType == TOOL_HIGHLIGHTER) && (drawingType == DRAWING_TYPE_SPLINE)) ||
|
||||||
|
(toolType == TOOL_DRAW_SPLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::supportsTapFilter() const -> bool {
|
||||||
|
ToolType toolType = this->getToolType();
|
||||||
|
|
||||||
|
return toolType == TOOL_PEN || toolType == TOOL_HIGHLIGHTER || toolType == TOOL_LASER_POINTER_PEN ||
|
||||||
|
toolType == TOOL_LASER_POINTER_HIGHLIGHTER || toolType == TOOL_HAND || toolType == TOOL_DRAW_RECT ||
|
||||||
|
toolType == TOOL_DRAW_ELLIPSE || toolType == TOOL_DRAW_COORDINATE_SYSTEM || toolType == TOOL_DRAW_ARROW ||
|
||||||
|
toolType == TOOL_DRAW_DOUBLE_ARROW || toolType == TOOL_DRAW_SPLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getSelectedTool(SelectedTool selectedTool) const -> Tool* {
|
||||||
|
switch (selectedTool) {
|
||||||
|
case SelectedTool::active:
|
||||||
|
return this->activeTool;
|
||||||
|
case SelectedTool::toolbar:
|
||||||
|
return this->toolbarSelectedTool;
|
||||||
|
default:
|
||||||
|
g_error("This button does not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolHandler::getButtonTool(Button button) const -> Tool* {
|
||||||
|
switch (button) {
|
||||||
|
case Button::BUTTON_ERASER:
|
||||||
|
return this->eraserButtonTool.get();
|
||||||
|
case Button::BUTTON_STYLUS_ONE:
|
||||||
|
return this->stylusButton1Tool.get();
|
||||||
|
case Button::BUTTON_STYLUS_TWO:
|
||||||
|
return this->stylusButton2Tool.get();
|
||||||
|
case Button::BUTTON_MOUSE_LEFT:
|
||||||
|
return this->mouseLeftButtonTool.get();
|
||||||
|
case Button::BUTTON_MOUSE_MIDDLE:
|
||||||
|
return this->mouseMiddleButtonTool.get();
|
||||||
|
case Button::BUTTON_MOUSE_RIGHT:
|
||||||
|
return this->mouseRightButtonTool.get();
|
||||||
|
case Button::BUTTON_TOUCH:
|
||||||
|
return this->touchDrawingButtonTool.get();
|
||||||
|
default:
|
||||||
|
g_error("This button is not defined for ToolHandler.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolHandler::resetButtonTool(ToolType type, Button button) {
|
||||||
|
auto t = type == TOOL_NONE ? nullptr : std::make_unique<Tool>(*tools[type - TOOL_PEN]);
|
||||||
|
switch (button) {
|
||||||
|
case Button::BUTTON_ERASER:
|
||||||
|
this->eraserButtonTool = std::move(t);
|
||||||
|
break;
|
||||||
|
case Button::BUTTON_STYLUS_ONE:
|
||||||
|
this->stylusButton1Tool = std::move(t);
|
||||||
|
break;
|
||||||
|
case Button::BUTTON_STYLUS_TWO:
|
||||||
|
this->stylusButton2Tool = std::move(t);
|
||||||
|
break;
|
||||||
|
case Button::BUTTON_MOUSE_LEFT:
|
||||||
|
this->mouseLeftButtonTool = std::move(t);
|
||||||
|
break;
|
||||||
|
case Button::BUTTON_MOUSE_MIDDLE:
|
||||||
|
this->mouseMiddleButtonTool = std::move(t);
|
||||||
|
break;
|
||||||
|
case Button::BUTTON_MOUSE_RIGHT:
|
||||||
|
this->mouseRightButtonTool = std::move(t);
|
||||||
|
break;
|
||||||
|
case Button::BUTTON_TOUCH:
|
||||||
|
this->touchDrawingButtonTool = std::move(t);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
g_error("This button is not defined for ToolHandler.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Handles Tools
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array> // for array
|
||||||
|
#include <functional> // for function
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "control/ToolEnums.h" // for ToolSize, ToolType, Draw...
|
||||||
|
#include "control/settings/SettingsEnums.h" // for Button
|
||||||
|
#include "util/Color.h" // for Color
|
||||||
|
|
||||||
|
#include "Tool.h" // for Tool
|
||||||
|
|
||||||
|
class LineStyle;
|
||||||
|
class Settings;
|
||||||
|
class ActionDatabase;
|
||||||
|
|
||||||
|
|
||||||
|
// enum for ptrs that are dynamically pointing to different tools
|
||||||
|
/**
|
||||||
|
* @brief Enum for ptrs that are dynamically pointing to different tools
|
||||||
|
* - active: describes the currently active tool used for drawing
|
||||||
|
* - toolbar: describes the tool currently selected in the toolbar
|
||||||
|
*
|
||||||
|
* These tools are to be distinguished from ButtonTools which are mostly static
|
||||||
|
* apart from changes to the Config
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
enum SelectedTool { active, toolbar };
|
||||||
|
|
||||||
|
class ToolListener {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Update the Cursor and the Toolbar based on the active color
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
virtual void toolColorChanged() = 0;
|
||||||
|
/**
|
||||||
|
* @brief Change the color of the current selection based on the active Tool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
virtual void changeColorOfSelection() = 0;
|
||||||
|
virtual void toolSizeChanged() = 0;
|
||||||
|
virtual void toolFillChanged() = 0;
|
||||||
|
virtual void toolLineStyleChanged() = 0;
|
||||||
|
virtual void toolChanged() = 0;
|
||||||
|
|
||||||
|
virtual ~ToolListener();
|
||||||
|
};
|
||||||
|
|
||||||
|
class ToolHandler {
|
||||||
|
public:
|
||||||
|
using ToolChangedCallback = std::function<void(ToolType)>;
|
||||||
|
ToolHandler(ToolListener* stateChangedListener, ActionDatabase* actionDB, Settings* settings);
|
||||||
|
virtual ~ToolHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset the Button tool with a new tooltype
|
||||||
|
*
|
||||||
|
* @param type Tooltype to be set for the button
|
||||||
|
* @param button button which should be set
|
||||||
|
*/
|
||||||
|
void resetButtonTool(ToolType type, Button button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the color for the active tool and under certain circumstances toolbar selected tool
|
||||||
|
*
|
||||||
|
* If the current tool does not have the color capability but the toolbar selected tool has
|
||||||
|
* the color can be set for the toolbar selected tool.
|
||||||
|
* This is indicated by a little pen shown on top of the color in the UI.
|
||||||
|
*
|
||||||
|
* @param color Color
|
||||||
|
* @param userSelection
|
||||||
|
* true if the user selected the color
|
||||||
|
* false if the color is selected by a tool change
|
||||||
|
* and therefore should not be applied to a selection
|
||||||
|
*/
|
||||||
|
void setColor(Color color, bool userSelection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the color for a Button
|
||||||
|
* This is a separate function from `setColor` to prevent mixup of different usecases.
|
||||||
|
*
|
||||||
|
* @param color color to be set
|
||||||
|
* @param button button to set color for
|
||||||
|
*/
|
||||||
|
void setButtonColor(Color color, Button button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Color of the active tool
|
||||||
|
*
|
||||||
|
* @return Color of active tool
|
||||||
|
*/
|
||||||
|
Color getColor() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Color of the active tool except the alpha value is replaced by 0xFF
|
||||||
|
*/
|
||||||
|
Color getColorMaskAlpha() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enable/disable fill for the tool selected in the toolbar
|
||||||
|
*
|
||||||
|
* @param fill whether fill should be enabled
|
||||||
|
* @param fireEvent whether a toolFillChanged event should be fired
|
||||||
|
*/
|
||||||
|
void setFillEnabled(bool fill);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Fill of the active tool
|
||||||
|
*
|
||||||
|
* @return -1 if fill is disabled
|
||||||
|
* @return int > 0 otherwise
|
||||||
|
*/
|
||||||
|
int getFill() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Drawing Type one of selected tools
|
||||||
|
*
|
||||||
|
* @param selectedTool by the default the active one
|
||||||
|
* @return DrawingType
|
||||||
|
*/
|
||||||
|
DrawingType getDrawingType(SelectedTool selectedTool = SelectedTool::active) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Drawing Type of the toolbar selected tool
|
||||||
|
* @note It is safer to always set the toolbar tool as the active tool could be pointing to a button tool which
|
||||||
|
* could lead to hard to debug behaviour
|
||||||
|
*
|
||||||
|
* @param drawingType
|
||||||
|
*/
|
||||||
|
void setDrawingType(DrawingType drawingType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Button Drawing Type of the button tool
|
||||||
|
*
|
||||||
|
* @param drawingType
|
||||||
|
* @param button button tool to be selected
|
||||||
|
*/
|
||||||
|
void setButtonDrawingType(DrawingType drawingType, Button button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Stroke Type of a button tool
|
||||||
|
*
|
||||||
|
* @param strokeType The stroke type to apply
|
||||||
|
* @param button The button tool to change
|
||||||
|
*/
|
||||||
|
void setButtonStrokeType(StrokeType strokeType, Button button);
|
||||||
|
void setButtonStrokeType(const LineStyle& lineStyle, Button button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Line Style of active tool
|
||||||
|
*
|
||||||
|
* @return const LineStyle&
|
||||||
|
*/
|
||||||
|
const LineStyle& getLineStyle() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Size of one of the selected tools
|
||||||
|
*
|
||||||
|
* @param selectedTool
|
||||||
|
* @return ToolSize
|
||||||
|
*/
|
||||||
|
ToolSize getSize(SelectedTool selectedTool = SelectedTool::active) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Size of toolbar selected tool
|
||||||
|
* @note It is safer to always set the toolbar tool as the active tool could be pointing to a button tool which
|
||||||
|
* could lead to hard to debug behaviour
|
||||||
|
*
|
||||||
|
* @param size is clamped to be within the defined range [0,5)
|
||||||
|
*/
|
||||||
|
void setSize(ToolSize size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Button Size
|
||||||
|
*
|
||||||
|
* @param size is clamped to be within the defined range [0,5)
|
||||||
|
* @param button size will be applied to
|
||||||
|
*/
|
||||||
|
void setButtonSize(ToolSize size, Button button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Thickness of the active tool
|
||||||
|
*
|
||||||
|
* @return double
|
||||||
|
*/
|
||||||
|
double getThickness() const;
|
||||||
|
|
||||||
|
void setLineStyle(const LineStyle& style);
|
||||||
|
|
||||||
|
ToolSize getPenSize() const;
|
||||||
|
ToolSize getEraserSize() const;
|
||||||
|
ToolSize getHighlighterSize() const;
|
||||||
|
void setPenSize(ToolSize size);
|
||||||
|
void setEraserSize(ToolSize size);
|
||||||
|
void setHighlighterSize(ToolSize size);
|
||||||
|
|
||||||
|
void setPenFillEnabled(bool fill);
|
||||||
|
bool getPenFillEnabled() const;
|
||||||
|
void setPenFill(int alpha);
|
||||||
|
int getPenFill() const;
|
||||||
|
|
||||||
|
void setHighlighterFillEnabled(bool fill);
|
||||||
|
bool getHighlighterFillEnabled() const;
|
||||||
|
void setHighlighterFill(int alpha);
|
||||||
|
int getHighlighterFill() const;
|
||||||
|
|
||||||
|
void setSelectPDFTextMarkerOpacity(int alpha);
|
||||||
|
int getSelectPDFTextMarkerOpacity() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the toolbar selected tool to the type
|
||||||
|
* This will also point the active tool to the same tool as the toolbar selected tool.
|
||||||
|
* This ensure that the toolbar and the cursor are correctly updated right after selecting the tool in the toolbar.
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
void selectTool(ToolType type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Tool Type of active tool
|
||||||
|
*
|
||||||
|
* @return ToolType
|
||||||
|
*/
|
||||||
|
ToolType getToolType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the Toolbar and the cursor based on the active Tool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void fireToolChanged() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Listen for tool changes.
|
||||||
|
*
|
||||||
|
* Different from the listener given to the constructor -- [listener]
|
||||||
|
* here only listens for when the current tool is changed to another.
|
||||||
|
*
|
||||||
|
* @param listener A callback, called when the user/client
|
||||||
|
* changes tools.
|
||||||
|
*/
|
||||||
|
void addToolChangedListener(ToolChangedCallback listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Tool of a certain type
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* @return Tool&
|
||||||
|
*/
|
||||||
|
Tool& getTool(ToolType type) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the active Tool, returns nullptr if no tool is active
|
||||||
|
*
|
||||||
|
* @return Tool*
|
||||||
|
*/
|
||||||
|
Tool* getActiveTool() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Eraser Type of the Eraser in the toolbar
|
||||||
|
* @note Here the Eraser Tool in the toolbar is changed regardless of the the tool currently selected.
|
||||||
|
* This is necessary to allow users to change the Eraser type for their Button Tools while having another tool
|
||||||
|
* active. This is relevant in case of Eraser Type being set to "Don't Change" for the button.
|
||||||
|
*
|
||||||
|
* @param eraserType
|
||||||
|
*/
|
||||||
|
void setEraserType(EraserType eraserType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the Button Eraser Type
|
||||||
|
*
|
||||||
|
* @param eraserType
|
||||||
|
* @param button
|
||||||
|
*/
|
||||||
|
void setButtonEraserType(EraserType eraserType, Button button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Eraser Type
|
||||||
|
* If the currently active Tool is an eraser it's type is returned (relevant for Buttontools).
|
||||||
|
* If the currently active Tool is not a eraser the erasertype of the eraser in the toolbar is obtained.
|
||||||
|
*
|
||||||
|
* @param selectedTool
|
||||||
|
* @return EraserType
|
||||||
|
*/
|
||||||
|
EraserType getEraserType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the toolbar based on the Eraser type of the active tool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void eraserTypeChanged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the selectedTool has a certain capability
|
||||||
|
*
|
||||||
|
* @param cap
|
||||||
|
* @param selectedTool
|
||||||
|
* @return true if tool has the capability
|
||||||
|
* @return false if tool does not have the capability
|
||||||
|
*/
|
||||||
|
bool hasCapability(ToolCapabilities cap, SelectedTool selectedTool = SelectedTool::active) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the active tool is a Drawing tool.
|
||||||
|
* Drawing tools are considered all tools that directly change the canvas.
|
||||||
|
* Right now these are:
|
||||||
|
* - Highlighter
|
||||||
|
* - Pen
|
||||||
|
* - Eraser
|
||||||
|
*
|
||||||
|
* @return true if active tool is a drawing tool
|
||||||
|
* @return false if active tool is no drawing tool
|
||||||
|
*/
|
||||||
|
bool isDrawingTool() const;
|
||||||
|
|
||||||
|
void saveSettings() const;
|
||||||
|
void loadSettings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Point the active tool to the corresponding button tool if it is not already pointing to it
|
||||||
|
*
|
||||||
|
* @param button Button tool which should be pointed to
|
||||||
|
* @return true if the active toolpointer was changed
|
||||||
|
* @return false if the active toolpointer was not changed (it was already pointing to the right button)
|
||||||
|
*/
|
||||||
|
bool pointActiveToolToButtonTool(Button button);
|
||||||
|
/**
|
||||||
|
* @brief Point the active tool to tool selected in the toolbar
|
||||||
|
*
|
||||||
|
* @return true if the active toolpointer was changed
|
||||||
|
* @return false if the active toolpointer was not changed (it was already pointing to the toolbar-tool)
|
||||||
|
*/
|
||||||
|
bool pointActiveToolToToolbarTool();
|
||||||
|
|
||||||
|
[[maybe_unused]] std::array<std::unique_ptr<Tool>, TOOL_COUNT> const& getTools() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the selection tools capabilities, depending on the selected elements
|
||||||
|
*/
|
||||||
|
void setSelectionEditTools(bool setColor, bool setSize, bool setFill, bool setLineStyle);
|
||||||
|
|
||||||
|
const double* getToolThickness(ToolType type) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current tool will create an element that may only reside on a single page even when the
|
||||||
|
* pointer moves to another
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
bool isSinglePageTool() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current tool accepts events from outside the page input started in (i.e. events
|
||||||
|
* should not be clamped to the page)
|
||||||
|
*/
|
||||||
|
bool acceptsOutOfPageEvents() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the tool supports short taps filtering (for floating toolbox or selection)
|
||||||
|
* see Preferences->Drawing Area->Action on Tool tap
|
||||||
|
*/
|
||||||
|
bool supportsTapFilter() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initTools();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<std::unique_ptr<Tool>, TOOL_COUNT> tools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Button Tool pointer based on enum
|
||||||
|
*
|
||||||
|
* @param button
|
||||||
|
* @return Tool*
|
||||||
|
*/
|
||||||
|
Tool* getButtonTool(Button button) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Selected Tool pointer based on enum
|
||||||
|
*
|
||||||
|
* @param selectedTool
|
||||||
|
* @return Tool*
|
||||||
|
*/
|
||||||
|
Tool* getSelectedTool(SelectedTool selectedTool) const;
|
||||||
|
|
||||||
|
// active Tool which is used for drawing
|
||||||
|
Tool* activeTool = nullptr;
|
||||||
|
|
||||||
|
// tool which is selected in the toolbar
|
||||||
|
Tool* toolbarSelectedTool = nullptr;
|
||||||
|
|
||||||
|
// tools set for the different Buttons
|
||||||
|
std::unique_ptr<Tool> stylusButton1Tool;
|
||||||
|
std::unique_ptr<Tool> stylusButton2Tool;
|
||||||
|
std::unique_ptr<Tool> eraserButtonTool;
|
||||||
|
std::unique_ptr<Tool> mouseLeftButtonTool;
|
||||||
|
std::unique_ptr<Tool> mouseMiddleButtonTool;
|
||||||
|
std::unique_ptr<Tool> mouseRightButtonTool;
|
||||||
|
std::unique_ptr<Tool> touchDrawingButtonTool;
|
||||||
|
|
||||||
|
std::vector<ToolChangedCallback> toolChangeListeners;
|
||||||
|
|
||||||
|
ToolListener* stateChangeListener = nullptr;
|
||||||
|
ActionDatabase* actionDB = nullptr;
|
||||||
|
Settings* settings = nullptr;
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#include "UndoRedoController.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for copy, max
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <iterator> // for back_insert_iterator, back_...
|
||||||
|
#include <mutex> // for unique_lock
|
||||||
|
|
||||||
|
#include "control/tools/EditSelection.h" // for EditSelection
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/ElementInsertionPosition.h"
|
||||||
|
#include "model/Layer.h" // for Layer
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler
|
||||||
|
|
||||||
|
#include "Control.h" // for Control
|
||||||
|
|
||||||
|
class XojPageView;
|
||||||
|
|
||||||
|
UndoRedoController::UndoRedoController(Control* control): control(control) {}
|
||||||
|
|
||||||
|
UndoRedoController::~UndoRedoController() = default;
|
||||||
|
|
||||||
|
void UndoRedoController::before() {
|
||||||
|
EditSelection* selection = control->getWindow()->getXournal()->getSelection();
|
||||||
|
if (selection != nullptr) {
|
||||||
|
layer = selection->getSourceLayer();
|
||||||
|
elements = selection->getElementsView().clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UndoRedoController::after() {
|
||||||
|
// Restore selection, if any
|
||||||
|
|
||||||
|
if (layer == nullptr) {
|
||||||
|
// No layer - no selection
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
PageRef page = control->getCurrentPage();
|
||||||
|
std::unique_lock lock(*doc);
|
||||||
|
size_t pageNo = doc->indexOf(page);
|
||||||
|
XojPageView* view = control->getWindow()->getXournal()->getViewFor(pageNo);
|
||||||
|
|
||||||
|
if (!view || !page) {
|
||||||
|
// The page may have been undone
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InsertionOrderRef remainingElements;
|
||||||
|
for (const Element* e: elements) {
|
||||||
|
// Test, if the element has been removed since
|
||||||
|
if (auto pos = layer->indexOf(e); pos != -1) {
|
||||||
|
remainingElements.emplace_back(e, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!remainingElements.empty()) {
|
||||||
|
auto removedElements = layer->removeElementsAt(remainingElements);
|
||||||
|
lock.unlock(); // For all other paths, the lock is released via ~unique_lock()
|
||||||
|
auto [sel, bounds] =
|
||||||
|
SelectionFactory::createFromFloatingElements(control, page, layer, view, std::move(removedElements));
|
||||||
|
control->getWindow()->getXournal()->setSelection(sel.release());
|
||||||
|
page->fireRangeChanged(bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UndoRedoController::undo(Control* control) {
|
||||||
|
UndoRedoController handler(control);
|
||||||
|
handler.before();
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->undo();
|
||||||
|
|
||||||
|
handler.after();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UndoRedoController::redo(Control* control) {
|
||||||
|
UndoRedoController handler(control);
|
||||||
|
handler.before();
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->redo();
|
||||||
|
|
||||||
|
handler.after();
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Wrapper for undo / redo stuff, to move out the logic of the main Control class
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
class Layer;
|
||||||
|
class Element;
|
||||||
|
|
||||||
|
class UndoRedoController {
|
||||||
|
private:
|
||||||
|
UndoRedoController(Control* control);
|
||||||
|
virtual ~UndoRedoController();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void before();
|
||||||
|
void after();
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void undo(Control* control);
|
||||||
|
static void redo(Control* control);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Controller
|
||||||
|
*/
|
||||||
|
Control* control = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layer of the selection before change
|
||||||
|
*/
|
||||||
|
Layer* layer = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected elements
|
||||||
|
*/
|
||||||
|
std::vector<const Element*> elements;
|
||||||
|
};
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
#include "XournalMain.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for copy, sort, max
|
||||||
|
#include <array> // for array
|
||||||
|
#include <chrono> // for time_point, duration, hours...
|
||||||
|
#include <clocale> // for setlocale, LC_NUMERIC
|
||||||
|
#include <cstdio> // for printf
|
||||||
|
#include <cstdlib> // for exit, size_t
|
||||||
|
#include <exception> // for exception
|
||||||
|
#include <iostream> // for operator<<, endl, basic_...
|
||||||
|
#include <locale> // for locale
|
||||||
|
#include <memory> // for unique_ptr, allocator
|
||||||
|
#include <optional> // for optional, nullopt
|
||||||
|
#include <sstream> // for stringstream
|
||||||
|
#include <stdexcept> // for runtime_error
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <gio/gio.h> // for GApplication, G_APPLICATION
|
||||||
|
#include <glib-object.h> // for G_CALLBACK, g_signal_con...
|
||||||
|
#include <glib.h> // for GOptionEntry, gchar, G_O...
|
||||||
|
#include <libintl.h> // for bindtextdomain, textdomain
|
||||||
|
|
||||||
|
#include "control/RecentManager.h" // for RecentManager
|
||||||
|
#include "control/jobs/BaseExportJob.h" // for ExportBackgroundType
|
||||||
|
#include "control/jobs/XournalScheduler.h" // for XournalScheduler
|
||||||
|
#include "control/settings/LatexSettings.h" // for LatexSettings
|
||||||
|
#include "control/settings/Settings.h" // for Settings
|
||||||
|
#include "control/settings/SettingsEnums.h" // for ICON_THEME_COLOR, ICON_T...
|
||||||
|
#include "control/xojfile/LoadHandler.h" // for LoadHandler
|
||||||
|
#include "control/xojfile/SaveHandler.h" // for SaveHandler
|
||||||
|
#include "gui/GladeSearchpath.h" // for GladeSearchpath
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "undo/EmergencySaveRestore.h" // for EmergencySaveRestore
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler
|
||||||
|
#include "util/PathUtil.h" // for getConfigFolder, openFil...
|
||||||
|
#include "util/PlaceholderString.h" // for PlaceholderString
|
||||||
|
#include "util/Util.h" // for execInUiThread
|
||||||
|
#include "util/VersionInfo.h" // for getVersionInfo
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for _, FS, _F
|
||||||
|
|
||||||
|
#include "Control.h" // for Control
|
||||||
|
#include "ExportHelper.h" // for exportImg, exportPdf
|
||||||
|
#include "config-dev.h" // for ERRORLOG_DIR
|
||||||
|
#include "config-git.h" // for GIT_BRANCH, GIT_ORIGIN_O...
|
||||||
|
#include "config.h" // for GETTEXT_PACKAGE, ENABLE_NLS
|
||||||
|
#include "filesystem.h" // for path, operator/, exists
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto APP_FLAGS =
|
||||||
|
GApplicationFlags(G_APPLICATION_SEND_ENVIRONMENT | G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN);
|
||||||
|
|
||||||
|
/// Configuration migration status.
|
||||||
|
enum class MigrateStatus {
|
||||||
|
NotNeeded, ///< No migration was needed.
|
||||||
|
Success, ///< Migration was carried out successfully.
|
||||||
|
Failure, ///< Migration failed. */
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MigrateResult {
|
||||||
|
MigrateStatus status{};
|
||||||
|
std::string message; ///< Any additional information about the migration status.
|
||||||
|
};
|
||||||
|
|
||||||
|
auto migrateSettings() -> MigrateResult;
|
||||||
|
|
||||||
|
void initResourcePath(GladeSearchpath* gladePath, const gchar* relativePathAndFile, bool failIfNotFound = true);
|
||||||
|
|
||||||
|
void initCAndCoutLocales() {
|
||||||
|
/**
|
||||||
|
* Force numbers to be printed out and parsed by C libraries (cairo) in the "classic" locale.
|
||||||
|
* This avoids issue with tags when exporting to PDF, see #3551
|
||||||
|
*/
|
||||||
|
setlocale(LC_NUMERIC, "C");
|
||||||
|
|
||||||
|
std::cout.imbue(std::locale());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto migrateSettings() -> MigrateResult {
|
||||||
|
const fs::path newConfigPath = Util::getConfigFolder();
|
||||||
|
|
||||||
|
if (!fs::exists(newConfigPath)) {
|
||||||
|
const std::array oldPaths = {
|
||||||
|
Util::getConfigFolder().parent_path() / "com.github.xournalpp.xournalpp",
|
||||||
|
Util::getConfigFolder().parent_path() / "com.github.xournalpp.xournalpp.exe",
|
||||||
|
Util::GFilename(g_get_home_dir()).toPath().value_or(fs::path()) / ".xournalpp",
|
||||||
|
};
|
||||||
|
for (auto const& oldPath: oldPaths) {
|
||||||
|
if (!fs::is_directory(oldPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
g_message("Migrating configuration from %s to %s", oldPath.string().c_str(),
|
||||||
|
newConfigPath.string().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 its 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() % newConfigPath.u8string())};
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
constexpr auto msg =
|
||||||
|
"Due to a recent update, Xournal++ has changed where its 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", e.what());
|
||||||
|
return {MigrateStatus::Failure, FS(_F(msg) % oldPath.u8string() % newConfigPath.u8string() % e.what())};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {MigrateStatus::NotNeeded, ""};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void deleteFile(const fs::path& file, GtkWindow* win) {
|
||||||
|
std::error_code error;
|
||||||
|
if (!fs::remove(file, error)) {
|
||||||
|
std::stringstream msg;
|
||||||
|
msg << FS(_F("Failed to delete file: {1}") % file.u8string()) << std::endl;
|
||||||
|
msg << error << std::endl << error.message() << std::endl;
|
||||||
|
msg << FS(_F("Please delete the file manually"));
|
||||||
|
XojMsgBox::showErrorToUser(win, msg.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkForEmergencySave(Control* control) {
|
||||||
|
auto file = Util::getConfigFile("emergencysave.xopp");
|
||||||
|
|
||||||
|
if (!fs::exists(file)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string msg = _("Xournal++ crashed last time. Would you like to restore the last edited file?");
|
||||||
|
enum { DELETE_FILE = 1, RESTORE_FILE };
|
||||||
|
XojMsgBox::askQuestion(
|
||||||
|
control->getGtkWindow(), _("Recovery file detected"), msg,
|
||||||
|
{{_("Delete file"), DELETE_FILE}, {_("Restore file"), RESTORE_FILE}},
|
||||||
|
[file = std::move(file), ctrl = control](int response) mutable {
|
||||||
|
if (response == DELETE_FILE) {
|
||||||
|
deleteFile(file, ctrl->getGtkWindow());
|
||||||
|
} else if (response == RESTORE_FILE) {
|
||||||
|
ctrl->openFileWithoutSavingTheCurrentDocument(file, false, -1, [ctrl, file](bool) {
|
||||||
|
ctrl->getDocument()->setFilepath("");
|
||||||
|
|
||||||
|
// Todo Make sure the document is changed + ask for saving
|
||||||
|
ctrl->getUndoRedoHandler()->addUndoAction(std::make_unique<EmergencySaveRestore>());
|
||||||
|
deleteFile(file, ctrl->getGtkWindow());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void exitOnMissingPdfFileName(const LoadHandler& loader) {
|
||||||
|
if (!loader.getMissingPdfFilename().empty()) {
|
||||||
|
auto msg =
|
||||||
|
FS(_F("The background file \"{1}\" could not be found. It might have been moved, renamed or deleted.") %
|
||||||
|
loader.getMissingPdfFilename());
|
||||||
|
std::cerr << msg << std::endl;
|
||||||
|
exit(-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Export the input file as a bunch of image files (one per page)
|
||||||
|
* @param infile Path to the input file
|
||||||
|
* @param outfile Path to the output file(s)
|
||||||
|
* @param range Page range to be parsed. If range=nullptr, exports the whole file
|
||||||
|
* @param layerRange Layer range to be parsed. Will only export those layers, for every exported page.
|
||||||
|
* If a number is too high for the number of layers on a given page, it is just ignored.
|
||||||
|
* If range=nullptr, exports all layers.
|
||||||
|
* @param pngDpi Set dpi for Png files. Non positive values are ignored
|
||||||
|
* @param pngWidth Set the width for Png files. Non positive values are ignored
|
||||||
|
* @param pngHeight Set the height for Png files. Non positive values are ignored
|
||||||
|
* @param exportBackground If EXPORT_BACKGROUND_NONE, the exported image file has transparent background
|
||||||
|
*
|
||||||
|
* The priority is: pngDpi overwrites pngWidth overwrites pngHeight
|
||||||
|
*
|
||||||
|
* @return 0 on success
|
||||||
|
*
|
||||||
|
* Calls std::exit(-2) on failure opening the input file and std::exit(-3) on export failure
|
||||||
|
*/
|
||||||
|
auto exportImg(fs::path infile, fs::path outfile, const char* range, const char* layerRange, int pngDpi, int pngWidth,
|
||||||
|
int pngHeight, ExportBackgroundType exportBackground) -> int {
|
||||||
|
|
||||||
|
LoadHandler loader;
|
||||||
|
|
||||||
|
auto doc = loader.loadDocument(infile);
|
||||||
|
if (doc == nullptr) {
|
||||||
|
g_message("%s", loader.getLastError().c_str());
|
||||||
|
std::exit(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitOnMissingPdfFileName(loader);
|
||||||
|
|
||||||
|
const auto res = ExportHelper::exportImg(doc.get(), std::move(outfile), range, layerRange, pngDpi, pngWidth,
|
||||||
|
pngHeight, exportBackground);
|
||||||
|
if (res != 0) {
|
||||||
|
std::exit(res);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Save a xopp-file with given pdf-background
|
||||||
|
*
|
||||||
|
* @param infile Path to the input .pdf file
|
||||||
|
* @param outfile Path to the output .xopp file
|
||||||
|
* @return int 0 on success
|
||||||
|
*
|
||||||
|
* Calls std::exit(-2) on failure reading the input file and std::exit(-3) on save failure
|
||||||
|
*/
|
||||||
|
auto saveDoc(fs::path infile, fs::path outfile) -> int {
|
||||||
|
SaveHandler saver;
|
||||||
|
auto handler = std::make_unique<DocumentHandler>();
|
||||||
|
auto newDoc = std::make_unique<Document>(handler.get());
|
||||||
|
const bool res = newDoc->readPdf(infile, /*initPages=*/true, false);
|
||||||
|
if (!res) {
|
||||||
|
g_message("%s", FC(_F("Error: {1}") % newDoc->getLastErrorMsg().c_str()));
|
||||||
|
std::exit(-2);
|
||||||
|
}
|
||||||
|
const fs::path out = fs::absolute(outfile);
|
||||||
|
saver.prepareSave(newDoc.get(), out);
|
||||||
|
saver.saveTo(out);
|
||||||
|
|
||||||
|
if (!saver.getErrorMessage().empty()) {
|
||||||
|
g_message("%s", FC(_F("Error: {1}") % saver.getErrorMessage()));
|
||||||
|
std::exit(-3);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Export the input file as pdf
|
||||||
|
* @param infile Path to the input file
|
||||||
|
* @param outfile Path to the output file
|
||||||
|
* @param layerRange Layer range to be parsed. Will only export those layers, for every exported page.
|
||||||
|
* If a number is too high for the number of layers on a given page, it is just ignored.
|
||||||
|
* If range=nullptr, exports all layers.
|
||||||
|
* @param range Page range to be parsed. If range=nullptr, exports the whole file
|
||||||
|
* @param exportBackground If EXPORT_BACKGROUND_NONE, the exported pdf file has white background
|
||||||
|
* @param progressiveMode If true, then for each xournalpp page, instead of rendering one PDF page, the page layers are
|
||||||
|
* rendered one by one to produce as many pages as there are layers.
|
||||||
|
* @param backend The requested backend
|
||||||
|
*
|
||||||
|
* @return 0 on success
|
||||||
|
*
|
||||||
|
* Calls std::exit(-2) on failure opening the input file and std::exit(-3) on export failure
|
||||||
|
*/
|
||||||
|
auto exportPdf(fs::path infile, fs::path outfile, const char* range, const char* layerRange,
|
||||||
|
ExportBackgroundType exportBackground, bool progressiveMode, ExportBackend backend) -> int {
|
||||||
|
LoadHandler loader;
|
||||||
|
auto doc = loader.loadDocument(infile);
|
||||||
|
if (doc == nullptr) {
|
||||||
|
g_message("%s", loader.getLastError().c_str());
|
||||||
|
std::exit(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitOnMissingPdfFileName(loader);
|
||||||
|
|
||||||
|
const auto res =
|
||||||
|
ExportHelper::exportPdf(doc.get(), outfile, range, layerRange, exportBackground, progressiveMode, backend);
|
||||||
|
if (res != 0) {
|
||||||
|
std::exit(res);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XournalMainPrivate {
|
||||||
|
XournalMainPrivate() = default;
|
||||||
|
XournalMainPrivate(XournalMainPrivate&&) = delete;
|
||||||
|
XournalMainPrivate(XournalMainPrivate const&) = delete;
|
||||||
|
auto operator=(XournalMainPrivate&&) -> XournalMainPrivate = delete;
|
||||||
|
auto operator=(XournalMainPrivate const&) -> XournalMainPrivate = delete;
|
||||||
|
|
||||||
|
~XournalMainPrivate() {
|
||||||
|
g_strfreev(optFilename);
|
||||||
|
g_free(pdfFilename);
|
||||||
|
g_free(imgFilename);
|
||||||
|
g_free(docFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
gchar** optFilename{}; ///< Array of paths, in GFilename encoding
|
||||||
|
gchar* pdfFilename{}; ///< Single path, in GFilename encoding
|
||||||
|
gchar* imgFilename{}; ///< Single path, in GFilename encoding
|
||||||
|
gchar* docFilename{}; ///< Single path, in GFilename encoding
|
||||||
|
gboolean showVersion = false;
|
||||||
|
int openAtPageNumber = 0; // when no --page is used, the document opens at the page specified in the metadata file
|
||||||
|
gchar* exportRange{};
|
||||||
|
gchar* exportLayerRange{};
|
||||||
|
int exportPngDpi = -1;
|
||||||
|
int exportPngWidth = -1;
|
||||||
|
int exportPngHeight = -1;
|
||||||
|
gboolean exportNoBackground = false;
|
||||||
|
gboolean exportNoRuling = false;
|
||||||
|
gboolean progressiveMode = false;
|
||||||
|
gboolean disableAudio = false;
|
||||||
|
gboolean attachMode = false;
|
||||||
|
gchar* exportPdfBackend{};
|
||||||
|
std::unique_ptr<GladeSearchpath> gladePath;
|
||||||
|
std::unique_ptr<Control> control;
|
||||||
|
std::unique_ptr<MainWindow> win;
|
||||||
|
};
|
||||||
|
using XMPtr = XournalMainPrivate*;
|
||||||
|
|
||||||
|
/// Checks for input method compatibility and ensures it
|
||||||
|
void ensure_input_model_compatibility() {
|
||||||
|
const char* imModule = g_getenv("GTK_IM_MODULE");
|
||||||
|
if (imModule != nullptr) {
|
||||||
|
const std::string imModuleString{imModule};
|
||||||
|
if (imModuleString == "xim") {
|
||||||
|
g_warning("Unsupported input method: %s", imModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a file in a resource folder, and return the resource folder path
|
||||||
|
* Return an empty string, if the folder was not found
|
||||||
|
*/
|
||||||
|
auto findResourcePath(const fs::path& searchFile) -> fs::path {
|
||||||
|
auto search_for = [&searchFile](fs::path start) -> std::optional<fs::path> {
|
||||||
|
constexpr auto* postfix = "share/xournalpp";
|
||||||
|
/// 1. relative install
|
||||||
|
/// 2. windows install
|
||||||
|
/// 3. build dir
|
||||||
|
for (int i = 0; i < 3; ++i, start = start.parent_path()) {
|
||||||
|
if (auto target = start / searchFile; fs::exists(target)) {
|
||||||
|
return target.parent_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto folder = start / postfix / searchFile; fs::exists(folder)) {
|
||||||
|
return folder.parent_path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
};
|
||||||
|
/* /// relative execution path
|
||||||
|
if (auto path = search_for(fs::path{}); path) {
|
||||||
|
return *path;
|
||||||
|
}*/
|
||||||
|
/// real execution path
|
||||||
|
if (auto path = search_for(Util::getExePath().parent_path()); path) {
|
||||||
|
return *path;
|
||||||
|
}
|
||||||
|
// Not found
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
fs::path p = Util::getDataPath();
|
||||||
|
p /= relativePathAndFile;
|
||||||
|
|
||||||
|
if (fs::exists(p)) {
|
||||||
|
gladePath->addSearchDirectory(p.parent_path());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::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 % Util::getDataPath().u8string());
|
||||||
|
|
||||||
|
if (!failIfNotFound) {
|
||||||
|
msg += _("\n\nWill now attempt to run without this file.");
|
||||||
|
XojMsgBox::showErrorToUser(nullptr, msg);
|
||||||
|
} else {
|
||||||
|
XojMsgBox::showErrorAndQuit(msg, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_activate(GApplication*, XMPtr) {}
|
||||||
|
|
||||||
|
gint on_command_line(GApplication*, GApplicationCommandLine*, XMPtr) {
|
||||||
|
g_message("XournalMain::on_command_line: This should never happen, please file a bugreport with a detailed "
|
||||||
|
"description how to reproduce this message");
|
||||||
|
// Todo: implement this, if someone files the bug report
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_open_files(GApplication* application, gpointer f, gint numFiles, gchar* hint, XMPtr app_data) {
|
||||||
|
if (numFiles <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto* files = (GFile**)f;
|
||||||
|
if (numFiles != 1) {
|
||||||
|
const std::string msg = _("Sorry, Xournal++ can only open one file at once.\n"
|
||||||
|
"Others are ignored.");
|
||||||
|
XojMsgBox::showErrorToUser(GTK_WINDOW(app_data->win->getWindow()), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs::path p = Util::fromGFile(files[0]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs::exists(p)) {
|
||||||
|
app_data->control->openFile(fs::absolute(p));
|
||||||
|
} else {
|
||||||
|
const std::string msg = FS(_F("File {1} does not exist.") % p.u8string());
|
||||||
|
XojMsgBox::showErrorToUser(GTK_WINDOW(app_data->win->getWindow()), msg);
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
const std::string msg = FS(_F("Filesystem error: {1}\n"
|
||||||
|
"Sorry, Xournal++ cannot open the file: {2}\n"
|
||||||
|
"Consider copying the file to a local directory.") %
|
||||||
|
e.what() % p.u8string());
|
||||||
|
XojMsgBox::showErrorToUser(GTK_WINDOW(app_data->win->getWindow()), msg);
|
||||||
|
}
|
||||||
|
gtk_window_present(GTK_WINDOW(app_data->win->getWindow()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_startup(GApplication* application, XMPtr app_data) {
|
||||||
|
XournalMain::initLocalisation();
|
||||||
|
ensure_input_model_compatibility();
|
||||||
|
const MigrateResult migrateResult = migrateSettings();
|
||||||
|
|
||||||
|
app_data->gladePath = std::make_unique<GladeSearchpath>();
|
||||||
|
initResourcePath(app_data->gladePath.get(), "ui/about.glade");
|
||||||
|
initResourcePath(app_data->gladePath.get(), "ui/xournalpp.css", false);
|
||||||
|
initResourcePath(app_data->gladePath.get(), "ui/toolbar.ini", false);
|
||||||
|
|
||||||
|
app_data->control = std::make_unique<Control>(application, app_data->gladePath.get(), app_data->disableAudio);
|
||||||
|
|
||||||
|
auto& globalLatexTemplatePath = app_data->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());
|
||||||
|
app_data->control->getSettings()->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
app_data->win = std::make_unique<MainWindow>(app_data->gladePath.get(), app_data->control.get(),
|
||||||
|
GTK_APPLICATION(application));
|
||||||
|
app_data->control->initWindow(app_data->win.get());
|
||||||
|
app_data->win->populate(app_data->gladePath.get());
|
||||||
|
|
||||||
|
if (migrateResult.status != MigrateStatus::NotNeeded) {
|
||||||
|
Util::execInUiThread(
|
||||||
|
[=]() { XojMsgBox::showErrorToUser(app_data->control->getGtkWindow(), migrateResult.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_application_set_menubar(GTK_APPLICATION(application), app_data->win->getMenuModel());
|
||||||
|
// Do we want stuff in gtk_application_set_app_menu?
|
||||||
|
|
||||||
|
app_data->win->show(nullptr);
|
||||||
|
|
||||||
|
fs::path p;
|
||||||
|
if (app_data->optFilename) {
|
||||||
|
if (g_strv_length(app_data->optFilename) != 1) {
|
||||||
|
const std::string msg = _("Sorry, Xournal++ can only open one file at once.\n"
|
||||||
|
"Others are ignored.");
|
||||||
|
XojMsgBox::showErrorToUser(GTK_WINDOW(app_data->win->getWindow()), msg);
|
||||||
|
}
|
||||||
|
p = Util::fromGFilename(app_data->optFilename[0]);
|
||||||
|
try {
|
||||||
|
p = fs::absolute(p);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
g_warning("Unable to convert path %s to absolute path: %s", app_data->optFilename[0], e.what());
|
||||||
|
}
|
||||||
|
} else if (app_data->control->getSettings()->isAutoloadMostRecent()) {
|
||||||
|
auto most_recent = RecentManager::getMostRecent();
|
||||||
|
if (most_recent) {
|
||||||
|
if (auto opt = Util::fromUri(gtk_recent_info_get_uri(most_recent.get()))) {
|
||||||
|
p = opt.value();
|
||||||
|
}
|
||||||
|
if (std::error_code err; !fs::exists(p, err)) {
|
||||||
|
if (err) {
|
||||||
|
g_warning("Failed to determine if recent path exists \"%s\": %s", char_cast(p.u8string().c_str()),
|
||||||
|
err.message().c_str());
|
||||||
|
} else {
|
||||||
|
g_warning("Tried to open the most recent file but it no longer exists:\n\"%s\"",
|
||||||
|
char_cast(p.u8string().c_str()));
|
||||||
|
}
|
||||||
|
p = fs::path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app_data->control->openFileWithoutSavingTheCurrentDocument(
|
||||||
|
std::move(p), app_data->attachMode, app_data->openAtPageNumber - 1,
|
||||||
|
[ctrl = app_data->control.get(), app = GTK_APPLICATION(application)](bool) {
|
||||||
|
ctrl->getScheduler()->start();
|
||||||
|
|
||||||
|
checkForEmergencySave(ctrl);
|
||||||
|
|
||||||
|
// There is a timing issue with the layout
|
||||||
|
// This fixes it, see #405
|
||||||
|
Util::execInUiThread([ctrl]() { ctrl->getWindow()->getXournal()->layoutPages(); });
|
||||||
|
gtk_application_add_window(app, ctrl->getGtkWindow());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto on_handle_local_options(GApplication*, GVariantDict*, XMPtr app_data) -> gint {
|
||||||
|
initCAndCoutLocales();
|
||||||
|
|
||||||
|
auto print_version = [&] { std::cout << xoj::util::getVersionInfo() << std::endl; };
|
||||||
|
|
||||||
|
auto exec_guarded = [&](auto&& fun, auto&& s) {
|
||||||
|
try {
|
||||||
|
return fun();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error: " << e.what() << std::endl;
|
||||||
|
std::cerr << "In: " << s << std::endl;
|
||||||
|
print_version();
|
||||||
|
return (1);
|
||||||
|
} catch (...) {
|
||||||
|
std::cerr << "Error: Unknown exception" << std::endl;
|
||||||
|
std::cerr << "In: " << s << std::endl;
|
||||||
|
print_version();
|
||||||
|
return (1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (app_data->showVersion) {
|
||||||
|
print_version();
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app_data->pdfFilename && app_data->optFilename && *app_data->optFilename) {
|
||||||
|
return exec_guarded(
|
||||||
|
[&] {
|
||||||
|
return exportPdf(Util::fromGFilename(*app_data->optFilename),
|
||||||
|
Util::fromGFilename(app_data->pdfFilename), app_data->exportRange,
|
||||||
|
app_data->exportLayerRange,
|
||||||
|
app_data->exportNoBackground ? EXPORT_BACKGROUND_NONE :
|
||||||
|
app_data->exportNoRuling ? EXPORT_BACKGROUND_UNRULED :
|
||||||
|
EXPORT_BACKGROUND_ALL,
|
||||||
|
app_data->progressiveMode, ExportBackend::fromString(app_data->exportPdfBackend));
|
||||||
|
},
|
||||||
|
"exportPdf");
|
||||||
|
}
|
||||||
|
if (app_data->imgFilename && app_data->optFilename && *app_data->optFilename) {
|
||||||
|
return exec_guarded(
|
||||||
|
[&] {
|
||||||
|
return exportImg(Util::fromGFilename(*app_data->optFilename),
|
||||||
|
Util::fromGFilename(app_data->imgFilename), app_data->exportRange,
|
||||||
|
app_data->exportLayerRange, app_data->exportPngDpi, app_data->exportPngWidth,
|
||||||
|
app_data->exportPngHeight,
|
||||||
|
app_data->exportNoBackground ? EXPORT_BACKGROUND_NONE :
|
||||||
|
app_data->exportNoRuling ? EXPORT_BACKGROUND_UNRULED :
|
||||||
|
EXPORT_BACKGROUND_ALL);
|
||||||
|
},
|
||||||
|
"exportImg");
|
||||||
|
}
|
||||||
|
if (app_data->docFilename && app_data->optFilename && *app_data->optFilename) {
|
||||||
|
return exec_guarded(
|
||||||
|
[&] {
|
||||||
|
return saveDoc(Util::fromGFilename(*app_data->optFilename),
|
||||||
|
Util::fromGFilename(app_data->docFilename));
|
||||||
|
},
|
||||||
|
"saveDocument");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_shutdown(GApplication*, XMPtr app_data) {
|
||||||
|
app_data->control->saveSettings();
|
||||||
|
app_data->win->getXournal()->clearSelection();
|
||||||
|
app_data->control->getScheduler()->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
void XournalMain::initLocalisation() {
|
||||||
|
#ifdef ENABLE_NLS
|
||||||
|
fs::path localeDir = Util::getGettextFilepath(Util::getLocalePath());
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
wbindtextdomain(GETTEXT_PACKAGE, localeDir.wstring().c_str());
|
||||||
|
#else
|
||||||
|
bindtextdomain(GETTEXT_PACKAGE, char_cast(localeDir.u8string().c_str()));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
textdomain(GETTEXT_PACKAGE);
|
||||||
|
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
|
||||||
|
|
||||||
|
#endif // ENABLE_NLS
|
||||||
|
|
||||||
|
// Not working on GNU g++(mingww) forWindows! Only working on Linux/macOS and with msvc
|
||||||
|
try {
|
||||||
|
std::locale::global(std::locale("")); // "" - system default locale
|
||||||
|
} catch (const std::runtime_error& e) {
|
||||||
|
g_warning("XournalMain: System default locale could not be set.\n - Caused by: %s\n - Note that it is not "
|
||||||
|
"supported to set the locale using mingw-w64 on windows.\n - This could be solved by compiling "
|
||||||
|
"xournalpp with msvc",
|
||||||
|
e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
initCAndCoutLocales();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto XournalMain::run(int argc, char** argv) -> int {
|
||||||
|
|
||||||
|
XournalMainPrivate app_data;
|
||||||
|
GtkApplication* app = gtk_application_new("com.github.xournalpp.xournalpp", APP_FLAGS);
|
||||||
|
g_object_set(G_OBJECT(app), "register-session", true, nullptr); // Needed for opening files on MacOS from Finder
|
||||||
|
g_set_prgname("com.github.xournalpp.xournalpp");
|
||||||
|
g_signal_connect(app, "activate", G_CALLBACK(&on_activate), &app_data);
|
||||||
|
g_signal_connect(app, "command-line", G_CALLBACK(&on_command_line), &app_data);
|
||||||
|
g_signal_connect(app, "open", G_CALLBACK(&on_open_files), &app_data);
|
||||||
|
g_signal_connect(app, "startup", G_CALLBACK(&on_startup), &app_data);
|
||||||
|
g_signal_connect(app, "shutdown", G_CALLBACK(&on_shutdown), &app_data);
|
||||||
|
g_signal_connect(app, "handle-local-options", G_CALLBACK(&on_handle_local_options), &app_data);
|
||||||
|
|
||||||
|
std::array options = {GOptionEntry{"page", 'n', 0, G_OPTION_ARG_INT, &app_data.openAtPageNumber,
|
||||||
|
_("Jump to Page (first Page: 1)"), "N"},
|
||||||
|
GOptionEntry{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &app_data.optFilename,
|
||||||
|
"<input>", nullptr},
|
||||||
|
GOptionEntry{"version", 0, 0, G_OPTION_ARG_NONE, &app_data.showVersion,
|
||||||
|
_("Get version of xournalpp"), nullptr},
|
||||||
|
GOptionEntry{"disable-audio", 0, 0, G_OPTION_ARG_NONE, &app_data.disableAudio,
|
||||||
|
_("Disable audio for this session"), nullptr},
|
||||||
|
GOptionEntry{"attach-mode", 0, 0, G_OPTION_ARG_NONE, &app_data.attachMode,
|
||||||
|
_("Open PDF in attach mode\n"
|
||||||
|
" Ignored if no PDF file is specified."),
|
||||||
|
nullptr},
|
||||||
|
GOptionEntry{"save", 's', 0, G_OPTION_ARG_FILENAME, &app_data.docFilename,
|
||||||
|
_("Save xopp-file with the background PDF specified as FILE"), "XOPPFILE"},
|
||||||
|
GOptionEntry{nullptr}}; // Must be terminated by a nullptr. See gtk doc
|
||||||
|
g_application_add_main_option_entries(G_APPLICATION(app), options.data());
|
||||||
|
|
||||||
|
std::string pdfbackendMessage =
|
||||||
|
FS(_F("Use the provided backend for PDF exports.\n"
|
||||||
|
" Available backends: {1}\n"
|
||||||
|
" No effect without -p/--create-pdf=foo.pdf") %
|
||||||
|
ExportBackend::listAvailableBackends());
|
||||||
|
/**
|
||||||
|
* Export related options
|
||||||
|
*/
|
||||||
|
std::array exportOptions = {
|
||||||
|
GOptionEntry{"create-pdf", 'p', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_FILENAME, &app_data.pdfFilename,
|
||||||
|
_("Export FILE as PDF"), "PDFFILE"},
|
||||||
|
GOptionEntry{
|
||||||
|
"create-img", 'i', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_FILENAME, &app_data.imgFilename,
|
||||||
|
_("Export FILE as image files (one per page)\n"
|
||||||
|
" Guess the output format from the extension of IMGFILE\n"
|
||||||
|
" Supported formats: .png, .svg"),
|
||||||
|
"IMGFILE"},
|
||||||
|
GOptionEntry{
|
||||||
|
"export-no-background", 0, 0, G_OPTION_ARG_NONE, &app_data.exportNoBackground,
|
||||||
|
_("Export without background\n"
|
||||||
|
" The exported file has transparent or white background,\n"
|
||||||
|
" depending on what its format supports\n"),
|
||||||
|
0},
|
||||||
|
GOptionEntry{"export-no-ruling", 0, 0, G_OPTION_ARG_NONE, &app_data.exportNoRuling,
|
||||||
|
_("Export without ruling\n"
|
||||||
|
" The exported file has no paper ruling\n"),
|
||||||
|
0},
|
||||||
|
GOptionEntry{
|
||||||
|
"export-layers-progressively", 0, 0, G_OPTION_ARG_NONE, &app_data.progressiveMode,
|
||||||
|
_("Export layers progressively\n"
|
||||||
|
" In PDF export, Render layers progressively one by one.\n"
|
||||||
|
" This results in N export pages per page with N layers,\n"
|
||||||
|
" building up the layer stack progressively.\n"
|
||||||
|
" The resulting PDF file can be used for a "
|
||||||
|
"presentation.\n"),
|
||||||
|
0},
|
||||||
|
GOptionEntry{
|
||||||
|
"export-range", 0, 0, G_OPTION_ARG_STRING, &app_data.exportRange,
|
||||||
|
_("Only export the pages specified by RANGE (e.g. \"2-3,5,7-\")\n"
|
||||||
|
" No effect without -p/--create-pdf or -i/--create-img"),
|
||||||
|
"RANGE"},
|
||||||
|
GOptionEntry{
|
||||||
|
"export-layer-range", 0, 0, G_OPTION_ARG_STRING, &app_data.exportLayerRange,
|
||||||
|
_("Only export the layers specified by RANGE (e.g. \"2-3,5,7-\")\n"
|
||||||
|
" No effect without -p/--create-pdf or -i/--create-img"),
|
||||||
|
"RANGE"},
|
||||||
|
GOptionEntry{"export-png-dpi", 0, 0, G_OPTION_ARG_INT, &app_data.exportPngDpi,
|
||||||
|
_("Set DPI for PNG exports. Default is 300\n"
|
||||||
|
" No effect without -i/--create-img=foo.png"),
|
||||||
|
"N"},
|
||||||
|
GOptionEntry{"export-png-width", 0, 0, G_OPTION_ARG_INT, &app_data.exportPngWidth,
|
||||||
|
_("Set page width for PNG exports\n"
|
||||||
|
" No effect without -i/--create-img=foo.png\n"
|
||||||
|
" Ignored if --export-png-dpi is used"),
|
||||||
|
"N"},
|
||||||
|
GOptionEntry{"export-png-height", 0, 0, G_OPTION_ARG_INT, &app_data.exportPngHeight,
|
||||||
|
_("Set page height for PNG exports\n"
|
||||||
|
" No effect without -i/--create-img=foo.png\n"
|
||||||
|
" Ignored if --export-png-dpi or --export-png-width "
|
||||||
|
"is used"),
|
||||||
|
"N"},
|
||||||
|
GOptionEntry{"export-pdf-backend", 0, 0, G_OPTION_ARG_STRING, &app_data.exportPdfBackend,
|
||||||
|
pdfbackendMessage.c_str(), "BACKEND"},
|
||||||
|
GOptionEntry{nullptr}}; // Must be terminated by a nullptr. See gtk doc
|
||||||
|
GOptionGroup* exportGroup = g_option_group_new("export", _("Advanced export options"),
|
||||||
|
_("Display advanced export options"), nullptr, nullptr);
|
||||||
|
g_option_group_add_entries(exportGroup, exportOptions.data());
|
||||||
|
g_application_add_option_group(G_APPLICATION(app), exportGroup);
|
||||||
|
|
||||||
|
auto rv = g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
|
g_object_unref(app);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Xournal main entry, commandline parser
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace XournalMain {
|
||||||
|
auto run(int argc, char** argv) -> int;
|
||||||
|
void initLocalisation();
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
#include "ActionDatabase.h"
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <gobject/gsignal.h>
|
||||||
|
|
||||||
|
#include "control/Control.h"
|
||||||
|
#include "gui/MainWindow.h"
|
||||||
|
#include "util/GVariantTemplate.h"
|
||||||
|
#include "util/safe_casts.h" // for to_underlying Todo(cpp20) remove
|
||||||
|
|
||||||
|
#include "ActionProperties.h"
|
||||||
|
|
||||||
|
#ifdef DEBUG_ACTION_DB
|
||||||
|
#define START_ROW " * " << std::left << std::setw(30) << std::boolalpha << Action_toString(a)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
template <Action a, class U = void>
|
||||||
|
struct InitiallyEnabled {
|
||||||
|
static inline void setup(ActionDatabase* db, Control*) { db->enableAction(a, true); }
|
||||||
|
};
|
||||||
|
template <Action a>
|
||||||
|
struct InitiallyEnabled<a, std::void_t<decltype(&ActionProperties<a>::initiallyEnabled)>> {
|
||||||
|
static inline void setup(ActionDatabase* db, Control* ctrl) {
|
||||||
|
db->enableAction(a, ActionProperties<a>::initiallyEnabled(ctrl));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class ActionDatabase::Populator {
|
||||||
|
/// Choose the right action namespace
|
||||||
|
template <Action a, class U = void>
|
||||||
|
struct ActionNamespace {
|
||||||
|
static constexpr auto ACTION_NAMESPACE = "win.";
|
||||||
|
static void addToActionMap(ActionDatabase* db) {
|
||||||
|
g_action_map_add_action(G_ACTION_MAP(db->win), G_ACTION(db->gActions[a].get()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
template <Action a>
|
||||||
|
struct ActionNamespace<
|
||||||
|
a, std::enable_if_t<std::is_same_v<typename ActionProperties<a>::app_namespace, std::true_type>, void>> {
|
||||||
|
static constexpr auto ACTION_NAMESPACE = "app.";
|
||||||
|
static void addToActionMap(ActionDatabase* db) {
|
||||||
|
g_action_map_add_action(G_ACTION_MAP(gtk_window_get_application(GTK_WINDOW(db->win))),
|
||||||
|
G_ACTION(db->gActions[a].get()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accelerators<a>::setup(ctrl); will setup the accelerators listed in ActionProperties<a>::accelerators (if any)
|
||||||
|
*/
|
||||||
|
template <Action a, class U = void>
|
||||||
|
struct Accelerators {
|
||||||
|
static inline void setup(Control*) {}
|
||||||
|
};
|
||||||
|
template <Action a>
|
||||||
|
struct Accelerators<a, std::void_t<decltype(&ActionProperties<a>::accelerators)>> {
|
||||||
|
static_assert(std::is_array_v<decltype(ActionProperties<a>::accelerators)>);
|
||||||
|
static_assert(
|
||||||
|
std::is_same_v<std::remove_extent_t<decltype(ActionProperties<a>::accelerators)>, const char* const>);
|
||||||
|
static_assert(
|
||||||
|
ActionProperties<a>::accelerators[std::extent_v<decltype(ActionProperties<a>::accelerators)> - 1] ==
|
||||||
|
nullptr,
|
||||||
|
"ActionProperties<a>::accelerators must be null terminated");
|
||||||
|
|
||||||
|
static inline void setup(Control* ctrl) {
|
||||||
|
// Todo(cpp20) constexpr this concatenation
|
||||||
|
std::string fullActionName = ActionNamespace<a>::ACTION_NAMESPACE;
|
||||||
|
fullActionName += Action_toString(a);
|
||||||
|
gtk_application_set_accels_for_action(GTK_APPLICATION(gtk_window_get_application(ctrl->getGtkWindow())),
|
||||||
|
fullActionName.c_str(), ActionProperties<a>::accelerators);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <Action a>
|
||||||
|
static inline void finishSetup(ActionDatabase* db, const char* signal) {
|
||||||
|
db->signalIds[a] = g_signal_connect(G_OBJECT(db->gActions[a].get()), signal,
|
||||||
|
G_CALLBACK(ActionProperties<a>::callback), db->control);
|
||||||
|
ActionNamespace<a>::addToActionMap(db);
|
||||||
|
// g_action_map_add_action(G_ACTION_MAP(db->win), G_ACTION(db->gActions[a].get()));
|
||||||
|
Accelerators<a>::setup(db->control);
|
||||||
|
InitiallyEnabled<a>::setup(db, db->control);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions without state or parameter
|
||||||
|
template <Action a, std::enable_if_t<!has_param<a>() && !has_state<a>(), bool> = true>
|
||||||
|
static void assign(ActionDatabase* db) {
|
||||||
|
|
||||||
|
ACTIONDB_PRINT_DEBUG(START_ROW << " | | |");
|
||||||
|
|
||||||
|
db->gActions[a].reset(g_simple_action_new(Action_toString(a), nullptr), xoj::util::adopt);
|
||||||
|
|
||||||
|
finishSetup<a>(db, "activate");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions with parameter but no state
|
||||||
|
template <Action a, std::enable_if_t<has_param<a>() && !has_state<a>(), bool> = true>
|
||||||
|
static void assign(ActionDatabase* db) {
|
||||||
|
|
||||||
|
ACTIONDB_PRINT_DEBUG(START_ROW << " | | type = \""
|
||||||
|
<< (const char*)gVariantType<typename ActionProperties<a>::parameter_type>()
|
||||||
|
<< "\" |");
|
||||||
|
|
||||||
|
db->gActions[a].reset(
|
||||||
|
g_simple_action_new(Action_toString(a), gVariantType<typename ActionProperties<a>::parameter_type>()),
|
||||||
|
xoj::util::adopt);
|
||||||
|
|
||||||
|
finishSetup<a>(db, "activate");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions with a state but no parameter
|
||||||
|
template <Action a, std::enable_if_t<!has_param<a>() && has_state<a>(), bool> = true>
|
||||||
|
static void assign(ActionDatabase* db) {
|
||||||
|
|
||||||
|
ACTIONDB_PRINT_DEBUG(START_ROW << " | \""
|
||||||
|
<< (const char*)gVariantType<typename ActionProperties<a>::state_type>()
|
||||||
|
<< "\" = " << std::setw(7) << ActionProperties<a>::initialState(db->control)
|
||||||
|
<< " | |");
|
||||||
|
|
||||||
|
db->gActions[a].reset(g_simple_action_new_stateful(Action_toString(a), nullptr,
|
||||||
|
makeGVariant<typename ActionProperties<a>::state_type>(
|
||||||
|
ActionProperties<a>::initialState(db->control))),
|
||||||
|
xoj::util::adopt);
|
||||||
|
|
||||||
|
finishSetup<a>(db, "change-state");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions with both state and parameter (with matching type)
|
||||||
|
template <Action a, std::enable_if_t<has_param<a>() && has_state<a>(), bool> = true>
|
||||||
|
static void assign(ActionDatabase* db) {
|
||||||
|
static_assert(
|
||||||
|
std::is_same_v<typename ActionProperties<a>::state_type, typename ActionProperties<a>::parameter_type>);
|
||||||
|
|
||||||
|
ACTIONDB_PRINT_DEBUG(
|
||||||
|
START_ROW << " | \"" << (const char*)gVariantType<typename ActionProperties<a>::state_type>()
|
||||||
|
<< "\" = " << std::setw(7) << ActionProperties<a>::initialState(db->control) << " | type = \""
|
||||||
|
<< (const char*)gVariantType<typename ActionProperties<a>::state_type>() << "\" |");
|
||||||
|
|
||||||
|
db->gActions[a].reset(g_simple_action_new_stateful(Action_toString(a),
|
||||||
|
gVariantType<typename ActionProperties<a>::state_type>(),
|
||||||
|
makeGVariant<typename ActionProperties<a>::state_type>(
|
||||||
|
ActionProperties<a>::initialState(db->control))),
|
||||||
|
xoj::util::adopt);
|
||||||
|
|
||||||
|
finishSetup<a>(db, "change-state");
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t... As>
|
||||||
|
static void populateImpl(std::index_sequence<As...>, ActionDatabase* db) {
|
||||||
|
((assign<static_cast<Action>(As)>(db)), ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void populate(ActionDatabase* db);
|
||||||
|
};
|
||||||
|
|
||||||
|
void ActionDatabase::Populator::populate(ActionDatabase* db) {
|
||||||
|
ACTIONDB_PRINT_DEBUG("Populating ActionDatabase:");
|
||||||
|
ACTIONDB_PRINT_DEBUG(" ACTION NAME: | STATE INIT | PARAM TYPE |");
|
||||||
|
|
||||||
|
populateImpl(std::make_index_sequence<xoj::to_underlying(Action::ENUMERATOR_COUNT)>(), db);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionDatabase::ActionDatabase(Control* control):
|
||||||
|
control(control), win(GTK_APPLICATION_WINDOW(control->getWindow()->getWindow())) {
|
||||||
|
Populator::populate(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionDatabase::~ActionDatabase() {
|
||||||
|
auto sig = signalIds.begin();
|
||||||
|
for (auto& a: gActions) {
|
||||||
|
/**
|
||||||
|
* The GAction's might still be referenced elsewhere (e.g. by the GtkApplicationWindow), so we need to
|
||||||
|
* disconnect the signals in case a callback is called and the Control instance has been destroyed
|
||||||
|
*/
|
||||||
|
g_signal_handler_disconnect(a.get(), *sig++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ActionDatabase::enableAction(Action action, bool enable) {
|
||||||
|
xoj_assert(gActions[action]);
|
||||||
|
g_simple_action_set_enabled(gActions[action].get(), enable);
|
||||||
|
ACTIONDB_PRINT_DEBUG((enable ? "Enabling Action \"" : "Disabling Action\"") << Action_toString(action) << "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ActionDatabase::getAction(Action a) const -> ActionRef { return gActions[a]; }
|
||||||
|
|
||||||
|
bool ActionDatabase::isActionEnabled(Action a) const {
|
||||||
|
xoj_assert(gActions[a]);
|
||||||
|
return g_action_get_enabled(G_ACTION(gActions[a].get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActionDatabase::disableAll() {
|
||||||
|
for (auto&& a: gActions) {
|
||||||
|
g_simple_action_set_enabled(a.get(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t... As>
|
||||||
|
static void resetEnableStatusImpl(std::index_sequence<As...>, ActionDatabase* db, Control* ctrl) {
|
||||||
|
((InitiallyEnabled<static_cast<Action>(As)>::setup(db, ctrl)), ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActionDatabase::resetEnableStatus() {
|
||||||
|
resetEnableStatusImpl(std::make_index_sequence<xoj::to_underlying(Action::ENUMERATOR_COUNT)>(), this, control);
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A database for GSimpleAction corresponding to enum/Action.enum.h
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gio/gio.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include "enums/Action.enum.h"
|
||||||
|
#include "util/Assert.h"
|
||||||
|
#include "util/EnumIndexedArray.h"
|
||||||
|
#include "util/GVariantTemplate.h"
|
||||||
|
#include "util/raii/GObjectSPtr.h"
|
||||||
|
|
||||||
|
#include "ActionRef.h"
|
||||||
|
#include "config-debug.h" // for DEBUG_ACTION_DB
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
#ifdef DEBUG_ACTION_DB
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "util/Color.h"
|
||||||
|
#include "util/safe_casts.h"
|
||||||
|
#include "util/serdesstream.h"
|
||||||
|
#define ACTIONDB_PRINT_DEBUG(f) std::cout << f << std::endl
|
||||||
|
template <class T, typename = void>
|
||||||
|
struct to_stream {
|
||||||
|
to_stream(const T& t): t(t) {}
|
||||||
|
const T& t;
|
||||||
|
friend std::ostream& operator<<(std::ostream& str, const to_stream& self) { return str << self.t; }
|
||||||
|
};
|
||||||
|
template <class T>
|
||||||
|
struct to_stream<T, std::enable_if_t<std::is_enum_v<T>, void>> {
|
||||||
|
to_stream(const T& t): t(t) {}
|
||||||
|
const T& t;
|
||||||
|
friend std::ostream& operator<<(std::ostream& str, const to_stream& self) {
|
||||||
|
return str << xoj::to_underlying(self.t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
template <>
|
||||||
|
struct to_stream<Color, void> {
|
||||||
|
to_stream(const Color& t): t(t) {}
|
||||||
|
const Color& t;
|
||||||
|
friend std::ostream& operator<<(std::ostream& str, const to_stream& self) {
|
||||||
|
auto s = serdes_stream<std::stringstream>();
|
||||||
|
s << std::hex << std::showbase << static_cast<uint32_t>(self.t);
|
||||||
|
return str << s.str();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
#define ACTIONDB_PRINT_DEBUG(f)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class ActionDatabase {
|
||||||
|
public:
|
||||||
|
ActionDatabase(Control* control);
|
||||||
|
~ActionDatabase();
|
||||||
|
|
||||||
|
void enableAction(Action a, bool enable);
|
||||||
|
ActionRef getAction(Action a) const;
|
||||||
|
bool isActionEnabled(Action a) const;
|
||||||
|
|
||||||
|
/// Disables every action
|
||||||
|
void disableAll();
|
||||||
|
/// Set the actions enabled flag depending on ActionProperties::initiallyEnabled()
|
||||||
|
void resetEnableStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the action's state, without triggering callbacks
|
||||||
|
*/
|
||||||
|
template <typename state_type>
|
||||||
|
inline void setActionState(Action a, state_type state) {
|
||||||
|
xoj_assert(gActions[a]);
|
||||||
|
g_simple_action_set_state(gActions[a].get(), makeGVariant(state));
|
||||||
|
ACTIONDB_PRINT_DEBUG("Set action \"" << Action_toString(a) << "\" state to " << to_stream(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Change the action's state, triggering callbacks
|
||||||
|
*/
|
||||||
|
template <typename state_type>
|
||||||
|
inline void fireChangeActionState(Action a, state_type state) {
|
||||||
|
xoj_assert(gActions[a]);
|
||||||
|
g_action_change_state(G_ACTION(gActions[a].get()), makeGVariant(state));
|
||||||
|
ACTIONDB_PRINT_DEBUG("Fire action \"" << Action_toString(a) << "\" state change to " << to_stream(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activate the action, triggering callbacks
|
||||||
|
*/
|
||||||
|
inline void fireActivateAction(Action a) {
|
||||||
|
xoj_assert(gActions[a]);
|
||||||
|
g_action_activate(G_ACTION(gActions[a].get()), nullptr);
|
||||||
|
ACTIONDB_PRINT_DEBUG("Fire action \"" << Action_toString(a) << "\" activate");
|
||||||
|
}
|
||||||
|
template <typename param_type>
|
||||||
|
inline void fireActivateAction(Action a, param_type param) {
|
||||||
|
xoj_assert(gActions[a]);
|
||||||
|
g_action_activate(G_ACTION(gActions[a].get()), makeGVariant(param));
|
||||||
|
ACTIONDB_PRINT_DEBUG("Fire action \"" << Action_toString(a) << "\" activate with param " << to_stream(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
EnumIndexedArray<ActionRef, Action> gActions;
|
||||||
|
EnumIndexedArray<gulong, Action> signalIds;
|
||||||
|
Control* control;
|
||||||
|
GtkApplicationWindow* win;
|
||||||
|
|
||||||
|
class Populator;
|
||||||
|
friend class Populator;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gio/gio.h>
|
||||||
|
|
||||||
|
#include "util/raii/GObjectSPtr.h"
|
||||||
|
|
||||||
|
using ActionRef = xoj::util::GObjectSPtr<GSimpleAction>;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "AutosaveJob.h"
|
||||||
|
|
||||||
|
#include <glib.h> // for g_message, g_warning
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/jobs/Job.h" // for JOB_TYPE_AUTOSAVE, JobType
|
||||||
|
#include "control/xojfile/SaveHandler.h" // for SaveHandler
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler
|
||||||
|
#include "util/PathUtil.h" // for clearExtensions, getAutosav...
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for FS, _F
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
AutosaveJob::AutosaveJob(Control* control): control(control) {}
|
||||||
|
|
||||||
|
AutosaveJob::~AutosaveJob() = default;
|
||||||
|
|
||||||
|
void AutosaveJob::afterRun() {
|
||||||
|
std::string msg = FS(_F("Error while autosaving: {1}") % this->error);
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutosaveJob::run() {
|
||||||
|
SaveHandler handler;
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->documentAutosaved();
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
doc->lock_shared();
|
||||||
|
auto filepath = doc->getFilepath();
|
||||||
|
|
||||||
|
if (filepath.empty()) {
|
||||||
|
filepath = Util::getAutosaveFilepath();
|
||||||
|
} else {
|
||||||
|
filepath.replace_filename(fs::path(".") += filepath.filename());
|
||||||
|
}
|
||||||
|
Util::clearExtensions(filepath);
|
||||||
|
filepath += ".autosave.xopp";
|
||||||
|
|
||||||
|
handler.prepareSave(doc, filepath);
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
g_message("%s", FS(_F("Autosaving to {1}") % filepath.string()).c_str());
|
||||||
|
|
||||||
|
fs::path tempfile = filepath;
|
||||||
|
tempfile += u8"~";
|
||||||
|
handler.saveTo(tempfile);
|
||||||
|
|
||||||
|
doc->lock();
|
||||||
|
handler.updateDocumentInfo(doc);
|
||||||
|
doc->unlock();
|
||||||
|
|
||||||
|
this->error = handler.getErrorMessage();
|
||||||
|
if (!this->error.empty()) {
|
||||||
|
callAfterRun();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (fs::exists(filepath)) {
|
||||||
|
fs::path swaptmpfile = filepath;
|
||||||
|
swaptmpfile += u8".swap";
|
||||||
|
Util::safeRenameFile(filepath, swaptmpfile);
|
||||||
|
Util::safeRenameFile(tempfile, filepath);
|
||||||
|
// All went well, we can delete the old autosave file
|
||||||
|
fs::remove(swaptmpfile);
|
||||||
|
} else {
|
||||||
|
Util::safeRenameFile(tempfile, filepath);
|
||||||
|
}
|
||||||
|
control->setLastAutosaveFile(filepath);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
auto fmtstr = _F("Could not rename autosave file from \"{1}\" to \"{2}\": {3}");
|
||||||
|
this->error = FS(fmtstr % tempfile.u8string() % filepath.u8string() % e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AutosaveJob::getType() -> JobType { return JOB_TYPE_AUTOSAVE; }
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Autosave job
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "Job.h" // for Job, JobType
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
class AutosaveJob: public Job {
|
||||||
|
public:
|
||||||
|
AutosaveJob(Control* control);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~AutosaveJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void run() override;
|
||||||
|
void afterRun() override;
|
||||||
|
|
||||||
|
JobType getType() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Control* control = nullptr;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#include "BaseExportJob.h"
|
||||||
|
|
||||||
|
#include <utility> // for move
|
||||||
|
|
||||||
|
#include <glib.h> // for g_warning
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#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 clearExtensions
|
||||||
|
#include "util/PopupWindowWrapper.h" // for PopupWindowWrapper
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/glib_casts.h" // for wrap_for_g_callback_v
|
||||||
|
#include "util/i18n.h" // for _, FS, _F
|
||||||
|
|
||||||
|
BaseExportJob::BaseExportJob(Control* control, const std::string& name): BlockingJob(control, name) {}
|
||||||
|
|
||||||
|
BaseExportJob::~BaseExportJob() = default;
|
||||||
|
|
||||||
|
void BaseExportJob::addFileFilterToDialog(GtkFileChooser* dialog, const std::string& name, const std::string& mime) {
|
||||||
|
GtkFileFilter* filter = gtk_file_filter_new();
|
||||||
|
gtk_file_filter_set_name(filter, name.c_str());
|
||||||
|
gtk_file_filter_add_mime_type(filter, mime.c_str());
|
||||||
|
gtk_file_chooser_add_filter(dialog, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BaseExportJob::checkOverwriteBackgroundPDF(fs::path const& file) const -> bool {
|
||||||
|
auto backgroundPDF = control->getDocument()->getPdfFilepath();
|
||||||
|
try {
|
||||||
|
if (backgroundPDF.empty() || !fs::exists(backgroundPDF)) {
|
||||||
|
// If there is no background, we can return
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), msg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseExportJob::showFileChooser(std::function<void()> onFileSelected, std::function<void()> onCancel) {
|
||||||
|
Settings* settings = control->getSettings();
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
doc->lock_shared();
|
||||||
|
fs::path suggestedPath = doc->createSaveFoldername(settings->getLastSavePath());
|
||||||
|
suggestedPath /=
|
||||||
|
doc->createSaveFilename(Document::PDF, settings->getDefaultSaveName(), settings->getDefaultPdfExportName());
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
auto pathValidation = [job = this](fs::path& p, const char* filterName) {
|
||||||
|
job->setExtensionFromFilter(p, filterName);
|
||||||
|
return job->testAndSetFilepath(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto popup = xoj::popup::PopupWindowWrapper<xoj::SaveExportDialog>(control->getSettings(), std::move(suggestedPath),
|
||||||
|
_("Export File"), _("Export"),
|
||||||
|
std::move(pathValidation), std::move(callback));
|
||||||
|
|
||||||
|
auto* fc = GTK_FILE_CHOOSER(popup.getPopup()->getWindow());
|
||||||
|
addFilterToDialog(fc);
|
||||||
|
|
||||||
|
popup.show(GTK_WINDOW(this->control->getWindow()->getWindow()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BaseExportJob::testAndSetFilepath(const fs::path& file) -> bool {
|
||||||
|
try {
|
||||||
|
if (!file.empty() && fs::is_directory(file.parent_path()) && checkOverwriteBackgroundPDF(file)) {
|
||||||
|
this->filepath = file;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
std::string msg = FS(_F("Failed to resolve path with the following error:\n{1}") % e.what());
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), msg);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseExportJob::afterRun() {
|
||||||
|
if (!this->errorMsg.empty()) {
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), this->errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Base class for Exports
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <gtk/gtk.h> // for GtkWidget
|
||||||
|
|
||||||
|
#include "BlockingJob.h" // for BlockingJob
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief List of types for the export of background components.
|
||||||
|
* The order must agree with the corresponding listBackgroundType in ui/exportSettings.glade.
|
||||||
|
* It is constructed so that one can check for intermediate types using comparison.
|
||||||
|
*/
|
||||||
|
enum ExportBackgroundType { EXPORT_BACKGROUND_NONE, EXPORT_BACKGROUND_UNRULED, EXPORT_BACKGROUND_ALL };
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
class BaseExportJob: public BlockingJob {
|
||||||
|
public:
|
||||||
|
BaseExportJob(Control* control, const std::string& name);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~BaseExportJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void afterRun() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void showFileChooser(std::function<void()> onFileSelected, std::function<void()> onCancel);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void addFilterToDialog(GtkFileChooser* dialog) = 0;
|
||||||
|
static void addFileFilterToDialog(GtkFileChooser* dialog, const std::string& name, const std::string& mimetype);
|
||||||
|
bool checkOverwriteBackgroundPDF(fs::path const& file) const;
|
||||||
|
|
||||||
|
virtual void setExtensionFromFilter(fs::path& p, const char* filterName) const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the given path is valid and records the path for the instance's later export tasks.
|
||||||
|
*
|
||||||
|
* Returns true if the path is validated.
|
||||||
|
*/
|
||||||
|
bool testAndSetFilepath(const fs::path& file);
|
||||||
|
|
||||||
|
private:
|
||||||
|
protected:
|
||||||
|
fs::path filepath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error message to show to the user
|
||||||
|
*/
|
||||||
|
std::string errorMsg;
|
||||||
|
|
||||||
|
class ExportType {
|
||||||
|
public:
|
||||||
|
std::string extension;
|
||||||
|
std::string mimeType;
|
||||||
|
|
||||||
|
ExportType(std::string ext, std::string mime): extension(std::move(ext)), mimeType(std::move(mime)) {}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include "BlockingJob.h"
|
||||||
|
|
||||||
|
#include <glib.h> // for g_idle_add
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/jobs/Job.h" // for JOB_TYPE_BLOCKING, JobType
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "util/Util.h" // for execInUiThread
|
||||||
|
#include "util/glib_casts.h" // for wrap_for_once_v
|
||||||
|
|
||||||
|
BlockingJob::BlockingJob(Control* control, const std::string& name): control(control) { control->block(name); }
|
||||||
|
|
||||||
|
BlockingJob::~BlockingJob() { this->control = nullptr; }
|
||||||
|
|
||||||
|
void BlockingJob::execute() {
|
||||||
|
this->run();
|
||||||
|
|
||||||
|
Util::execInUiThread([control = this->control]() {
|
||||||
|
control->unblock();
|
||||||
|
control->getWindow()->getXournal()->requestFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BlockingJob::getType() -> JobType { return JOB_TYPE_BLOCKING; }
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A job which is done in the GTK main thread, but the application is blocked
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "Job.h" // for Job, JobType
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
class BlockingJob: public Job {
|
||||||
|
public:
|
||||||
|
BlockingJob(Control* control, const std::string& name);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~BlockingJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void execute() override;
|
||||||
|
|
||||||
|
JobType getType() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Control* control;
|
||||||
|
};
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#include "CustomExportJob.h"
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <utility> // for move, pair
|
||||||
|
|
||||||
|
#include <gtk/gtk.h> // for GTK_WINDOW
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/jobs/BaseExportJob.h" // for BaseExportJob::ExportType
|
||||||
|
#include "control/xojfile/XojExportHandler.h" // for XojExportHandler
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/dialog/ExportDialog.h" // for ExportDialog
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "pdf/base/XojPdfExport.h" // for XojPdfExport
|
||||||
|
#include "pdf/base/XojPdfExportFactory.h" // for XojPdfExportFactory
|
||||||
|
#include "util/PathUtil.h" // for clearExtensions
|
||||||
|
#include "util/PopupWindowWrapper.h" // for PopupWindowWrapper
|
||||||
|
#include "util/Util.h" // for execInUiThread
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for _, FS, _F
|
||||||
|
|
||||||
|
#include "ImageExport.h" // for ImageExport, EXPORT_GR...
|
||||||
|
#include "SaveJob.h" // for SaveJob
|
||||||
|
#include "XournalScheduler.h"
|
||||||
|
|
||||||
|
|
||||||
|
CustomExportJob::CustomExportJob(Control* control): BaseExportJob(control, _("Custom Export")) {
|
||||||
|
// Supported filters
|
||||||
|
filters.insert({_("PDF files"), ExportType(".pdf", "application/pdf")});
|
||||||
|
filters.insert({_("PNG graphics"), ExportType(".png", "image/png")});
|
||||||
|
filters.insert({_("SVG graphics"), ExportType(".svg", "image/svg+xml")});
|
||||||
|
filters.insert({_("Xournal (Compatibility)"), ExportType(".xoj", "application/x-xojpp")});
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomExportJob::~CustomExportJob() = default;
|
||||||
|
|
||||||
|
void CustomExportJob::addFilterToDialog(GtkFileChooser* dialog) {
|
||||||
|
// Runs on every filter inside the filters map
|
||||||
|
for (auto& filter: filters) {
|
||||||
|
addFileFilterToDialog(dialog, filter.first, filter.second.mimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomExportJob::setExtensionFromFilter(fs::path& file, const char* filterName) const {
|
||||||
|
// Extract the file filter selected
|
||||||
|
const auto& chosenFilter = filters.at(filterName);
|
||||||
|
|
||||||
|
// Remove any pre-existing extension and adds the chosen one
|
||||||
|
Util::clearExtensions(file, chosenFilter.extension);
|
||||||
|
file += chosenFilter.extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomExportJob::showDialogAndRun() {
|
||||||
|
|
||||||
|
auto onFileSelected = [job = this]() {
|
||||||
|
Util::execInUiThread([job]() {
|
||||||
|
if (job->filepath.extension() == ".xoj") {
|
||||||
|
job->exportTypeXoj = true;
|
||||||
|
job->control->getScheduler()->addJob(job, JOB_PRIORITY_NONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto ext = job->filepath.extension(); ext == ".pdf") {
|
||||||
|
job->format = EXPORT_GRAPHICS_PDF;
|
||||||
|
} else if (ext == ".svg") {
|
||||||
|
job->format = EXPORT_GRAPHICS_SVG;
|
||||||
|
} else if (ext == ".png") {
|
||||||
|
job->format = EXPORT_GRAPHICS_PNG;
|
||||||
|
} else {
|
||||||
|
g_warning("Unknown extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* ctrl = job->control;
|
||||||
|
xoj::popup::PopupWindowWrapper<xoj::popup::ExportDialog> popup(
|
||||||
|
ctrl->getGladeSearchPath(), job->format, ctrl->getCurrentPageNo() + 1,
|
||||||
|
ctrl->getDocument()->getPageCount(), !ctrl->getDocument()->getPdfFilepath().empty(),
|
||||||
|
[job](const xoj::popup::ExportDialog& dialog) {
|
||||||
|
if (dialog.isConfirmed()) {
|
||||||
|
job->exportRange = dialog.getRange();
|
||||||
|
job->progressiveMode = dialog.progressiveModeSelected();
|
||||||
|
job->exportBackground = dialog.getBackgroundType();
|
||||||
|
job->pdfExportBackend = dialog.getPdfExportBackend();
|
||||||
|
|
||||||
|
if (job->format == EXPORT_GRAPHICS_PNG) {
|
||||||
|
job->pngQualityParameter = dialog.getPngQualityParameter();
|
||||||
|
}
|
||||||
|
|
||||||
|
job->control->getScheduler()->addJob(job, JOB_PRIORITY_NONE);
|
||||||
|
} else {
|
||||||
|
// The job blocked, so we have to unblock, because the job
|
||||||
|
// unblocks only after run
|
||||||
|
job->control->unblock();
|
||||||
|
}
|
||||||
|
job->unref();
|
||||||
|
});
|
||||||
|
popup.show(GTK_WINDOW(job->control->getWindow()->getWindow()));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto onCancel = [job = this]() {
|
||||||
|
job->control->unblock();
|
||||||
|
job->unref();
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseExportJob::showFileChooser(std::move(onFileSelected), std::move(onCancel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create one Graphics file per page
|
||||||
|
*/
|
||||||
|
void CustomExportJob::exportGraphics() {
|
||||||
|
ImageExport imgExport(control->getDocument(), filepath, format, exportBackground, exportRange);
|
||||||
|
if (format == EXPORT_GRAPHICS_PNG) {
|
||||||
|
imgExport.setQualityParameter(pngQualityParameter);
|
||||||
|
}
|
||||||
|
imgExport.exportGraphics(control);
|
||||||
|
errorMsg = imgExport.getLastErrorMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomExportJob::run() {
|
||||||
|
if (exportTypeXoj) {
|
||||||
|
SaveJob::updatePreview(control);
|
||||||
|
Document* doc = this->control->getDocument();
|
||||||
|
|
||||||
|
XojExportHandler h;
|
||||||
|
doc->lock_shared();
|
||||||
|
h.prepareSave(doc, filepath);
|
||||||
|
h.saveTo(filepath, this->control);
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
if (!h.getErrorMessage().empty()) {
|
||||||
|
this->lastError = FS(_F("Save file error: {1}") % h.getErrorMessage());
|
||||||
|
|
||||||
|
callAfterRun();
|
||||||
|
}
|
||||||
|
} else if (format == EXPORT_GRAPHICS_PDF) {
|
||||||
|
// don't lock the page here for the whole flow, else we get a dead lock...
|
||||||
|
// the ui is blocked, so there should be no changes...
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
std::unique_ptr<XojPdfExport> pdfe = XojPdfExportFactory::createExport(doc, control, pdfExportBackend);
|
||||||
|
|
||||||
|
pdfe->setExportBackground(exportBackground);
|
||||||
|
|
||||||
|
if (!pdfe->createPdf(this->filepath, exportRange, progressiveMode)) {
|
||||||
|
this->errorMsg = pdfe->getLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
exportGraphics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomExportJob::afterRun() {
|
||||||
|
if (!this->lastError.empty()) {
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), this->lastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A customized export
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map> // for map
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "pdf/base/PdfExportBackend.h"
|
||||||
|
#include "util/ElementRange.h" // for PageRangeVector
|
||||||
|
|
||||||
|
#include "BaseExportJob.h" // for BaseExportJob, EXPORT_BACKGROUND_ALL
|
||||||
|
#include "ImageExport.h" // for RasterImageQualityParameter, EXPORT_GRAP...
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
|
||||||
|
class CustomExportJob: public BaseExportJob {
|
||||||
|
public:
|
||||||
|
CustomExportJob(Control* control);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~CustomExportJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void run() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void showDialogAndRun();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void afterRun() override;
|
||||||
|
|
||||||
|
void addFilterToDialog(GtkFileChooser* dialog) override;
|
||||||
|
void setExtensionFromFilter(fs::path& p, const char* filterName) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create one Graphics file per page
|
||||||
|
*/
|
||||||
|
void exportGraphics();
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* The range to export
|
||||||
|
*/
|
||||||
|
PageRangeVector exportRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Quality parameter for PNG exports
|
||||||
|
*/
|
||||||
|
RasterImageQualityParameter pngQualityParameter = RasterImageQualityParameter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export graphics format
|
||||||
|
*/
|
||||||
|
ExportGraphicsFormat format = EXPORT_GRAPHICS_UNDEFINED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XOJ Export, else PNG Export
|
||||||
|
*/
|
||||||
|
bool exportTypeXoj = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background export type
|
||||||
|
*/
|
||||||
|
ExportBackgroundType exportBackground = EXPORT_BACKGROUND_ALL;
|
||||||
|
|
||||||
|
/// Backend used for exporting the PDF background to PDF
|
||||||
|
ExportBackend pdfExportBackend = ExportBackend::DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all Layers progressively
|
||||||
|
*/
|
||||||
|
bool progressiveMode = false;
|
||||||
|
|
||||||
|
std::string lastError;
|
||||||
|
|
||||||
|
std::string chosenFilterName;
|
||||||
|
|
||||||
|
std::map<std::string, ExportType> filters;
|
||||||
|
};
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
#include "ImageExport.h"
|
||||||
|
|
||||||
|
#include <cmath> // for round
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <memory> // for __shared_ptr_access, allocat...
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <cairo-svg.h> // for cairo_svg_surface_create
|
||||||
|
|
||||||
|
#include "control/jobs/BaseExportJob.h" // for EXPORT_BACKGROUND_NONE, EXPO...
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "model/PageType.h" // for PageType
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfPageSPtr, XojPdfPage
|
||||||
|
#include "util/StringUtils.h" // for char_cast
|
||||||
|
#include "util/Util.h" // for DPI_NORMALIZATION_FACTOR
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
#include "view/DocumentView.h" // for DocumentView
|
||||||
|
#include "view/LayerView.h"
|
||||||
|
#include "view/View.h"
|
||||||
|
#include "view/background/BackgroundView.h"
|
||||||
|
|
||||||
|
#include "ProgressListener.h" // for ProgressListener
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
|
||||||
|
|
||||||
|
ImageExport::ImageExport(Document* doc, fs::path file, ExportGraphicsFormat format,
|
||||||
|
ExportBackgroundType exportBackground, const PageRangeVector& exportRange):
|
||||||
|
doc(doc), file(std::move(file)), format(format), exportBackground(exportBackground), exportRange(exportRange) {}
|
||||||
|
|
||||||
|
ImageExport::~ImageExport() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a quality level for PNG exports
|
||||||
|
* @param qParam A quality parameter for the export
|
||||||
|
*/
|
||||||
|
void ImageExport::setQualityParameter(RasterImageQualityParameter qParam) { this->qualityParameter = qParam; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a quality level for PNG exports
|
||||||
|
* @param criterion A quality criterion for the export
|
||||||
|
* @param value The target value of this criterion
|
||||||
|
*/
|
||||||
|
void ImageExport::setQualityParameter(ExportQualityCriterion criterion, int value) {
|
||||||
|
this->qualityParameter = RasterImageQualityParameter(criterion, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select layers to export by parsing str
|
||||||
|
* @param rangeStr A string parsed to get a list of layers
|
||||||
|
*/
|
||||||
|
void ImageExport::setLayerRange(const char* rangeStr) {
|
||||||
|
if (rangeStr) {
|
||||||
|
// Use no upper bound for layer indices, as the maximal value can vary between pages
|
||||||
|
layerRange =
|
||||||
|
std::make_unique<LayerRangeVector>(ElementRange::parse(rangeStr, std::numeric_limits<size_t>::max()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the last error message
|
||||||
|
* @return The last error message to show to the user
|
||||||
|
*/
|
||||||
|
auto ImageExport::getLastErrorMsg() const -> string { return lastError; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create Cairo surface for a given page
|
||||||
|
* @param width the width of the page being exported
|
||||||
|
* @param height the height of the page being exported
|
||||||
|
* @param id the id of the page being exported
|
||||||
|
* @param zoomRatio the zoom ratio for PNG exports with fixed DPI
|
||||||
|
*
|
||||||
|
* @return the zoom ratio of the current page if the export type is PNG, 0.0 otherwise
|
||||||
|
* The return value may differ from that of the parameter zoomRatio if the export has fixed page width or
|
||||||
|
* height (in pixels). In this case, the zoomRatio (and the DPI) is page-dependent as soon as the document has pages of
|
||||||
|
* different sizes.
|
||||||
|
*/
|
||||||
|
auto ImageExport::createSurface(double width, double height, size_t id, double zoomRatio) -> double {
|
||||||
|
switch (this->format) {
|
||||||
|
case EXPORT_GRAPHICS_PNG:
|
||||||
|
switch (this->qualityParameter.getQualityCriterion()) {
|
||||||
|
case EXPORT_QUALITY_WIDTH:
|
||||||
|
zoomRatio = ((double)this->qualityParameter.getValue()) / width;
|
||||||
|
this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, this->qualityParameter.getValue(),
|
||||||
|
(int)std::round(height * zoomRatio));
|
||||||
|
break;
|
||||||
|
case EXPORT_QUALITY_HEIGHT:
|
||||||
|
zoomRatio = ((double)this->qualityParameter.getValue()) / height;
|
||||||
|
this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int)std::round(width * zoomRatio),
|
||||||
|
this->qualityParameter.getValue());
|
||||||
|
break;
|
||||||
|
case EXPORT_QUALITY_DPI: // Use the zoomRatio given as argument
|
||||||
|
this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int)std::round(width * zoomRatio),
|
||||||
|
(int)std::round(height * zoomRatio));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->cr = cairo_create(this->surface);
|
||||||
|
cairo_scale(this->cr, zoomRatio, zoomRatio);
|
||||||
|
return zoomRatio;
|
||||||
|
case EXPORT_GRAPHICS_SVG:
|
||||||
|
this->surface =
|
||||||
|
cairo_svg_surface_create(char_cast(getFilenameWithNumber(id).u8string().c_str()), width, height);
|
||||||
|
cairo_svg_surface_restrict_to_version(this->surface, CAIRO_SVG_VERSION_1_2);
|
||||||
|
this->cr = cairo_create(this->surface);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->lastError = _("Unsupported graphics format: ") + std::to_string(this->format);
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Free / store the surface
|
||||||
|
*/
|
||||||
|
auto ImageExport::freeSurface(size_t id) -> bool {
|
||||||
|
cairo_destroy(this->cr);
|
||||||
|
|
||||||
|
cairo_status_t status = CAIRO_STATUS_SUCCESS;
|
||||||
|
if (format == EXPORT_GRAPHICS_PNG) {
|
||||||
|
auto filepath = getFilenameWithNumber(id);
|
||||||
|
status = cairo_surface_write_to_png(surface, char_cast(filepath.u8string().c_str()));
|
||||||
|
}
|
||||||
|
cairo_surface_destroy(surface);
|
||||||
|
|
||||||
|
// we ignore this problem
|
||||||
|
return status == CAIRO_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get a filename with a (page) number appended
|
||||||
|
* @param no The appended number. If no==-1, does not append anything.
|
||||||
|
* e.g. .../export-2.png, if the no is -1, return .../export.png
|
||||||
|
*
|
||||||
|
* @return The filename
|
||||||
|
*/
|
||||||
|
auto ImageExport::getFilenameWithNumber(size_t no) const -> fs::path {
|
||||||
|
if (no == SINGLE_PAGE) {
|
||||||
|
// No number to add
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ext = file.extension();
|
||||||
|
auto path(file);
|
||||||
|
path.replace_extension();
|
||||||
|
(path += (std::string("-") + std::to_string(no))) += ext;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Export a single PNG/SVG page
|
||||||
|
* @param pageId The index of the page being exported
|
||||||
|
* @param id The number of the page being exported
|
||||||
|
* @param zoomRatio The zoom ratio for PNG exports with fixed DPI
|
||||||
|
* @param format The format of the exported image
|
||||||
|
* @param view A DocumentView for drawing the page
|
||||||
|
*/
|
||||||
|
void ImageExport::exportImagePage(size_t pageId, size_t id, double zoomRatio, ExportGraphicsFormat format,
|
||||||
|
DocumentView& view) {
|
||||||
|
std::shared_lock<Document> lock(*doc);
|
||||||
|
ConstPageRef page = doc->getPage(pageId);
|
||||||
|
|
||||||
|
zoomRatio = createSurface(page->getWidth(), page->getHeight(), id, zoomRatio);
|
||||||
|
|
||||||
|
cairo_status_t state = cairo_surface_status(this->surface);
|
||||||
|
if (state != CAIRO_STATUS_SUCCESS) {
|
||||||
|
this->lastError = _("Error save image #1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page->getBackgroundType().isPdfPage() && (exportBackground != EXPORT_BACKGROUND_NONE)) {
|
||||||
|
// Handle the pdf page separately, to call renderForPrinting for better quality.
|
||||||
|
auto pgNo = page->getPdfPageNr();
|
||||||
|
XojPdfPageSPtr popplerPage = doc->getPdfPage(pgNo);
|
||||||
|
if (!popplerPage) {
|
||||||
|
this->lastError = _("Error while exporting the pdf background: I cannot find the pdf page number ");
|
||||||
|
this->lastError += std::to_string(pgNo);
|
||||||
|
} else if (format == EXPORT_GRAPHICS_PNG) {
|
||||||
|
popplerPage->render(cr);
|
||||||
|
} else {
|
||||||
|
popplerPage->renderForPrinting(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xoj::view::BackgroundFlags flags;
|
||||||
|
flags.showPDF = xoj::view::HIDE_PDF_BACKGROUND; // Already exported (if any)
|
||||||
|
flags.showImage = exportBackground == EXPORT_BACKGROUND_NONE ? xoj::view::HIDE_IMAGE_BACKGROUND :
|
||||||
|
xoj::view::SHOW_IMAGE_BACKGROUND;
|
||||||
|
flags.showRuling = exportBackground <= EXPORT_BACKGROUND_UNRULED ? xoj::view::HIDE_RULING_BACKGROUND :
|
||||||
|
xoj::view::SHOW_RULING_BACKGROUND;
|
||||||
|
|
||||||
|
if (layerRange) {
|
||||||
|
view.drawLayersOfPage(*layerRange, page, this->cr, true /* dont render eraseable */, flags);
|
||||||
|
} else {
|
||||||
|
view.drawPage(page, this->cr, true /* dont render eraseable */, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!freeSurface(id)) {
|
||||||
|
// could not create this file...
|
||||||
|
this->lastError = _("Error save image #2");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create one Graphics file per page
|
||||||
|
* @param stateListener A listener to track the export progress
|
||||||
|
*/
|
||||||
|
void ImageExport::exportGraphics(ProgressListener* stateListener) {
|
||||||
|
// don't lock the page here for the whole flow, else we get a dead lock...
|
||||||
|
// the ui is blocked, so there should be no changes...
|
||||||
|
auto count = doc->getPageCount();
|
||||||
|
|
||||||
|
bool onePage = ((this->exportRange.size() == 1) && (this->exportRange[0].first == this->exportRange[0].last));
|
||||||
|
|
||||||
|
std::vector<char> selectedPages(count, 0);
|
||||||
|
size_t selectedCount = 0;
|
||||||
|
for (PageRangeEntry const& e: this->exportRange) {
|
||||||
|
for (size_t x = e.first; x <= e.last; x++) {
|
||||||
|
selectedPages[x] = true;
|
||||||
|
selectedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stateListener->setMaximumState(selectedCount);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute the zoomRatio only once if using DPI as a PNG quality criterion
|
||||||
|
*/
|
||||||
|
double zoomRatio = 1.0;
|
||||||
|
if ((this->format == EXPORT_GRAPHICS_PNG) && (this->qualityParameter.getQualityCriterion() == EXPORT_QUALITY_DPI)) {
|
||||||
|
zoomRatio = ((double)this->qualityParameter.getValue()) / Util::DPI_NORMALIZATION_FACTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentView view;
|
||||||
|
size_t current = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
auto id = i + 1;
|
||||||
|
if (onePage) {
|
||||||
|
id = SINGLE_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPages[i]) {
|
||||||
|
exportImagePage(i, id, zoomRatio, format, view);
|
||||||
|
stateListener->setCurrentState(++current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RasterImageQualityParameter::RasterImageQualityParameter() = default;
|
||||||
|
RasterImageQualityParameter::RasterImageQualityParameter(ExportQualityCriterion criterion, int value):
|
||||||
|
qualityCriterion(criterion), value(value) {}
|
||||||
|
RasterImageQualityParameter::~RasterImageQualityParameter() = default;
|
||||||
|
|
||||||
|
auto RasterImageQualityParameter::getQualityCriterion() -> ExportQualityCriterion { return qualityCriterion; }
|
||||||
|
|
||||||
|
auto RasterImageQualityParameter::getValue() -> int { return value; }
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Image export implementation
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_surface_t, cairo_t
|
||||||
|
|
||||||
|
#include "util/ElementRange.h" // for PageRangeVector, LayerRangeVector
|
||||||
|
|
||||||
|
#include "BaseExportJob.h" // for ExportBackgroundType, EXPORT_BACKGROUND_ALL
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Document;
|
||||||
|
class ProgressListener;
|
||||||
|
class DocumentView;
|
||||||
|
|
||||||
|
enum ExportGraphicsFormat { EXPORT_GRAPHICS_UNDEFINED, EXPORT_GRAPHICS_PDF, EXPORT_GRAPHICS_PNG, EXPORT_GRAPHICS_SVG };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief List of available criterion for determining a PNG export quality.
|
||||||
|
* The order must agree with the corresponding listAvailableCriterion in ui/exportSettings.glade
|
||||||
|
*/
|
||||||
|
enum ExportQualityCriterion { EXPORT_QUALITY_DPI, EXPORT_QUALITY_WIDTH, EXPORT_QUALITY_HEIGHT };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A class storing the available quality parameters for PNG export
|
||||||
|
*/
|
||||||
|
class RasterImageQualityParameter {
|
||||||
|
public:
|
||||||
|
RasterImageQualityParameter(ExportQualityCriterion criterion, int value);
|
||||||
|
RasterImageQualityParameter();
|
||||||
|
~RasterImageQualityParameter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the quality criterion of this parameter
|
||||||
|
* @return The quality criterion
|
||||||
|
*/
|
||||||
|
ExportQualityCriterion getQualityCriterion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the target value of this parameter
|
||||||
|
* @return The target value
|
||||||
|
*/
|
||||||
|
int getValue();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Default quality is: DPI=300
|
||||||
|
*/
|
||||||
|
ExportQualityCriterion qualityCriterion = EXPORT_QUALITY_DPI;
|
||||||
|
int value = 300;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A class handling export as images
|
||||||
|
*/
|
||||||
|
class ImageExport {
|
||||||
|
public:
|
||||||
|
ImageExport(Document* doc, fs::path file, ExportGraphicsFormat format, ExportBackgroundType exportBackground,
|
||||||
|
const PageRangeVector& exportRange);
|
||||||
|
virtual ~ImageExport();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Get the last error message
|
||||||
|
* @return The last error message to show to the user
|
||||||
|
*/
|
||||||
|
std::string getLastErrorMsg() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create one Graphics file per page
|
||||||
|
* @param stateListener A listener to track the progress
|
||||||
|
*/
|
||||||
|
void exportGraphics(ProgressListener* stateListener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a quality level for PNG exports
|
||||||
|
* @param qParam A quality parameter for the export
|
||||||
|
*/
|
||||||
|
void setQualityParameter(RasterImageQualityParameter qParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a quality level for PNG exports
|
||||||
|
* @param criterion A quality criterion for the export
|
||||||
|
* @param value The target value of this criterion
|
||||||
|
*/
|
||||||
|
void setQualityParameter(ExportQualityCriterion criterion, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select layers to export by parsing str
|
||||||
|
* @param str A string parsed to get a list of layers
|
||||||
|
*/
|
||||||
|
void setLayerRange(const char* str);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Create Cairo surface for a given page
|
||||||
|
* @param width the width of the page being exported
|
||||||
|
* @param height the height of the page being exported
|
||||||
|
* @param id the id of the page being exported
|
||||||
|
* @param zoomRatio the zoom ratio for PNG exports with fixed DPI
|
||||||
|
*
|
||||||
|
* @return the zoom ratio of the current page if the export type is PNG, 0.0 otherwise
|
||||||
|
* The return value may differ from that of the parameter zoomRatio
|
||||||
|
* if the export has fixed page width or height (in pixels)
|
||||||
|
*/
|
||||||
|
double createSurface(double width, double height, size_t id, double zoomRatio);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Free / store the surface
|
||||||
|
*/
|
||||||
|
bool freeSurface(size_t id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get a filename with a (page) number appended
|
||||||
|
* @param no The appended number. If no==-1, does not append anything.
|
||||||
|
* e.g. .../export-2.png, if the no is -1, return .../export.png
|
||||||
|
*
|
||||||
|
* @return The filename
|
||||||
|
*/
|
||||||
|
fs::path getFilenameWithNumber(size_t no) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Export a single PNG/SVG page
|
||||||
|
* @param pageId The index of the page being exported
|
||||||
|
* @param id The number of the page being exported
|
||||||
|
* @param zoomRatio The zoom ratio for PNG exports with fixed DPI
|
||||||
|
* @param format The format of the exported image
|
||||||
|
* @param view A DocumentView for drawing the page
|
||||||
|
*/
|
||||||
|
void exportImagePage(size_t pageId, size_t id, double zoomRatio, ExportGraphicsFormat format, DocumentView& view);
|
||||||
|
|
||||||
|
static constexpr size_t SINGLE_PAGE = size_t(-1);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Document to export
|
||||||
|
*/
|
||||||
|
Document* doc = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filename for export
|
||||||
|
*/
|
||||||
|
fs::path file;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export graphics format
|
||||||
|
*/
|
||||||
|
ExportGraphicsFormat format = EXPORT_GRAPHICS_UNDEFINED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not export the Background
|
||||||
|
*/
|
||||||
|
ExportBackgroundType exportBackground = EXPORT_BACKGROUND_ALL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The range to export
|
||||||
|
*/
|
||||||
|
const PageRangeVector& exportRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A pointer to a range of layers to export (the same for every exported pages)
|
||||||
|
*/
|
||||||
|
std::unique_ptr<LayerRangeVector> layerRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The export quality parameters, used if format==EXPORT_GRAPHICS_PNG
|
||||||
|
*/
|
||||||
|
RasterImageQualityParameter qualityParameter = RasterImageQualityParameter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export surface
|
||||||
|
*/
|
||||||
|
cairo_surface_t* surface = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cairo context
|
||||||
|
*/
|
||||||
|
cairo_t* cr = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last error message to show to the user
|
||||||
|
*/
|
||||||
|
std::string lastError;
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include "Job.h"
|
||||||
|
|
||||||
|
#include <gdk/gdk.h> // for gdk_threads_add_idle
|
||||||
|
#include <glib.h> // for g_source_remove
|
||||||
|
|
||||||
|
#include "util/glib_casts.h" // for wrap_for_once_v
|
||||||
|
|
||||||
|
|
||||||
|
Job::Job(): refCount(1) {}
|
||||||
|
|
||||||
|
Job::~Job() = default;
|
||||||
|
|
||||||
|
void Job::unref() {
|
||||||
|
if (refCount.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Job::ref() {
|
||||||
|
refCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Job::deleteJob() {
|
||||||
|
this->onDelete();
|
||||||
|
|
||||||
|
if (this->afterRunId) {
|
||||||
|
g_source_remove(this->afterRunId);
|
||||||
|
this->unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Job::onDelete() {}
|
||||||
|
|
||||||
|
void Job::execute() { this->run(); }
|
||||||
|
|
||||||
|
auto Job::getSource() -> void* { return nullptr; }
|
||||||
|
|
||||||
|
auto Job::callAfterCallback(Job* job) -> bool {
|
||||||
|
job->afterRun();
|
||||||
|
|
||||||
|
job->afterRunId = 0;
|
||||||
|
job->unref();
|
||||||
|
return false; // do not call again
|
||||||
|
}
|
||||||
|
|
||||||
|
void Job::callAfterRun() {
|
||||||
|
if (this->afterRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->ref();
|
||||||
|
this->afterRunId = gdk_threads_add_idle(xoj::util::wrap_for_once_v<Job::callAfterCallback>, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Job::afterRun() {}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A job which is done
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
enum JobType { JOB_TYPE_BLOCKING, JOB_TYPE_PREVIEW, JOB_TYPE_RENDER, JOB_TYPE_AUTOSAVE };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manually ref-counted class representing an asynchronous job to be used with
|
||||||
|
* the Xournal++ scheduler.
|
||||||
|
*
|
||||||
|
* Instances of Job (and its subclasses) should be constructed exclusively using
|
||||||
|
* the `new` keyword.
|
||||||
|
*/
|
||||||
|
class Job {
|
||||||
|
public:
|
||||||
|
Job();
|
||||||
|
Job(const Job&) = delete;
|
||||||
|
Job& operator=(const Job&) = delete;
|
||||||
|
Job(Job&&) = delete;
|
||||||
|
Job& operator=(Job&&) = delete;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~Job();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Unref the Job, the initial refcount is set to 1 on creation
|
||||||
|
*/
|
||||||
|
void unref();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase the refcount
|
||||||
|
*/
|
||||||
|
void ref();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Job because e.g. the source was removed
|
||||||
|
*/
|
||||||
|
void deleteJob();
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual JobType getType() = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* this method is called
|
||||||
|
*/
|
||||||
|
virtual void execute();
|
||||||
|
|
||||||
|
virtual void* getSource();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/**
|
||||||
|
* override this method
|
||||||
|
*/
|
||||||
|
virtual void run() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should be called as _last_ operation in run
|
||||||
|
*
|
||||||
|
* If you call it in another position the object will be deleted before run is finished!
|
||||||
|
*/
|
||||||
|
void callAfterRun();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After run will be called from UI Thread after the Job is finished
|
||||||
|
*
|
||||||
|
* All UI Stuff should happen here
|
||||||
|
*/
|
||||||
|
virtual void afterRun();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the job is to be removed from the
|
||||||
|
* scheduler.
|
||||||
|
*/
|
||||||
|
virtual void onDelete();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Internal callback sent to the GLib main loop which invokes `afterRun`.
|
||||||
|
*/
|
||||||
|
static bool callAfterCallback(Job* job);
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned int afterRunId = 0;
|
||||||
|
|
||||||
|
std::atomic<unsigned int> refCount;
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include "PdfExportJob.h"
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr, allocator
|
||||||
|
#include <string> // for string
|
||||||
|
#include <utility> // for move
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/jobs/BaseExportJob.h" // for BaseExportJob
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "pdf/base/XojPdfExport.h" // for XojPdfExport
|
||||||
|
#include "pdf/base/XojPdfExportFactory.h" // for XojPdfExportFactory
|
||||||
|
#include "util/PathUtil.h" // for clearExtensions
|
||||||
|
#include "util/i18n.h" // for _
|
||||||
|
|
||||||
|
PdfExportJob::PdfExportJob(Control* control): BaseExportJob(control, _("PDF Export")) {}
|
||||||
|
|
||||||
|
PdfExportJob::~PdfExportJob() = default;
|
||||||
|
|
||||||
|
void PdfExportJob::addFilterToDialog(GtkFileChooser* dialog) {
|
||||||
|
addFileFilterToDialog(dialog, _("PDF files"), "application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PdfExportJob::setExtensionFromFilter(fs::path& file, const char* /*filterName*/) const {
|
||||||
|
// Remove any pre-existing extension and adds .pdf
|
||||||
|
Util::clearExtensions(file, ".pdf");
|
||||||
|
file += ".pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PdfExportJob::run() {
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
|
||||||
|
doc->lock_shared();
|
||||||
|
std::unique_ptr<XojPdfExport> pdfe = XojPdfExportFactory::createExport(doc, control);
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
if (!pdfe->createPdf(this->filepath, false)) {
|
||||||
|
this->errorMsg = pdfe->getLastError();
|
||||||
|
if (control->getWindow()) {
|
||||||
|
callAfterRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A job to export PDF
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BaseExportJob.h" // for BaseExportJob
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
class PdfExportJob: public BaseExportJob {
|
||||||
|
public:
|
||||||
|
PdfExportJob(Control* control);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~PdfExportJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void run() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void addFilterToDialog(GtkFileChooser* dialog) override;
|
||||||
|
void setExtensionFromFilter(fs::path& p, const char* filterName) const override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "PreviewJob.h"
|
||||||
|
|
||||||
|
#include <memory> // for __s...
|
||||||
|
#include <mutex> // for mutex
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <glib-object.h> // for g_o...
|
||||||
|
#include <gtk/gtk.h> // for Gtk...
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Con...
|
||||||
|
#include "control/jobs/Job.h" // for JOB...
|
||||||
|
#include "gui/Shadow.h" // for Shadow
|
||||||
|
#include "gui/sidebar/previews/base/SidebarPreviewBase.h" // for Sid...
|
||||||
|
#include "gui/sidebar/previews/base/SidebarPreviewBaseEntry.h" // for Sid...
|
||||||
|
#include "gui/sidebar/previews/layer/SidebarPreviewLayerEntry.h" // for Sid...
|
||||||
|
#include "model/Document.h" // for Doc...
|
||||||
|
#include "model/Layer.h" // for Layer
|
||||||
|
#include "model/PageRef.h" // for Pag...
|
||||||
|
#include "model/XojPage.h" // for Xoj...
|
||||||
|
#include "util/Util.h" // for exe...
|
||||||
|
#include "view/DocumentView.h" // for Doc...
|
||||||
|
#include "view/LayerView.h" // for Lay...
|
||||||
|
#include "view/View.h" // for Con...
|
||||||
|
#include "view/background/BackgroundFlags.h" // for BAC...
|
||||||
|
|
||||||
|
PreviewJob::PreviewJob(SidebarPreviewBaseEntry* sidebar): sidebarPreview(sidebar) {}
|
||||||
|
|
||||||
|
PreviewJob::~PreviewJob() { this->sidebarPreview = nullptr; }
|
||||||
|
|
||||||
|
void PreviewJob::onDelete() { this->sidebarPreview = nullptr; }
|
||||||
|
|
||||||
|
auto PreviewJob::getSource() -> void* { return this->sidebarPreview; }
|
||||||
|
|
||||||
|
auto PreviewJob::getType() -> JobType { return JOB_TYPE_PREVIEW; }
|
||||||
|
|
||||||
|
void PreviewJob::initGraphics() {
|
||||||
|
auto w = this->sidebarPreview->imageWidth;
|
||||||
|
auto h = this->sidebarPreview->imageHeight;
|
||||||
|
auto DPIscaling = this->sidebarPreview->DPIscaling;
|
||||||
|
buffer.reset(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w * DPIscaling, h * DPIscaling), xoj::util::adopt);
|
||||||
|
cairo_surface_set_device_scale(buffer.get(), DPIscaling, DPIscaling);
|
||||||
|
cr.reset(cairo_create(buffer.get()), xoj::util::adopt);
|
||||||
|
double zoom = this->sidebarPreview->sidebar->getZoom();
|
||||||
|
cairo_translate(cr.get(), Shadow::getShadowTopLeftSize() + 2, Shadow::getShadowTopLeftSize() + 2);
|
||||||
|
cairo_scale(cr.get(), zoom, zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewJob::finishPaint() {
|
||||||
|
auto lock = std::lock_guard(this->sidebarPreview->drawingMutex);
|
||||||
|
this->sidebarPreview->buffer = std::move(this->buffer);
|
||||||
|
Util::execInUiThread([btn = this->sidebarPreview->button]() { gtk_widget_queue_draw(btn.get()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewJob::drawPage() {
|
||||||
|
ConstPageRef page = this->sidebarPreview->page;
|
||||||
|
Document* doc = this->sidebarPreview->sidebar->getControl()->getDocument();
|
||||||
|
DocumentView view;
|
||||||
|
view.setPdfCache(this->sidebarPreview->sidebar->getCache());
|
||||||
|
PreviewRenderType type = this->sidebarPreview->getRenderType();
|
||||||
|
Layer::Index layer = 0;
|
||||||
|
|
||||||
|
doc->lock_shared();
|
||||||
|
|
||||||
|
// getLayer is not defined for page preview
|
||||||
|
if (type != RENDER_TYPE_PAGE_PREVIEW) {
|
||||||
|
layer = (dynamic_cast<SidebarPreviewLayerEntry*>(this->sidebarPreview))->getLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto context = xoj::view::Context::createDefault(cr.get());
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case RENDER_TYPE_PAGE_PREVIEW:
|
||||||
|
// render all layers
|
||||||
|
view.drawPage(page, cr.get(), true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RENDER_TYPE_PAGE_LAYER:
|
||||||
|
// render single layer
|
||||||
|
view.initDrawing(page, cr.get(), true);
|
||||||
|
if (layer == 0) {
|
||||||
|
auto flags = xoj::view::BACKGROUND_SHOW_ALL;
|
||||||
|
flags.forceVisible = xoj::view::FORCE_VISIBLE;
|
||||||
|
view.drawBackground(flags);
|
||||||
|
} else {
|
||||||
|
view.drawBackground(xoj::view::BACKGROUND_FORCE_PAINT_BACKGROUND_COLOR_ONLY);
|
||||||
|
const Layer* drawLayer = page->getLayersView()[layer - 1];
|
||||||
|
xoj::view::LayerView layerView(drawLayer);
|
||||||
|
layerView.draw(context);
|
||||||
|
}
|
||||||
|
view.finializeDrawing();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RENDER_TYPE_PAGE_LAYERSTACK: {
|
||||||
|
// render all layers up to layer
|
||||||
|
view.initDrawing(page, cr.get(), true);
|
||||||
|
auto flags = xoj::view::BACKGROUND_SHOW_ALL;
|
||||||
|
flags.forceVisible = xoj::view::FORCE_VISIBLE;
|
||||||
|
view.drawBackground(flags);
|
||||||
|
const auto layers = page->getLayersView();
|
||||||
|
for (Layer::Index i = 0; i < layer; i++) {
|
||||||
|
const Layer* drawLayer = layers[i];
|
||||||
|
xoj::view::LayerView layerView(drawLayer);
|
||||||
|
layerView.draw(context);
|
||||||
|
}
|
||||||
|
view.finializeDrawing();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unknown type
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc->unlock_shared();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewJob::clipToPage() {
|
||||||
|
// Only render within the preview page. Without this, the when preview jobs attempt
|
||||||
|
// to clear the display, we fill a region larger than the inside of the preview page!
|
||||||
|
cairo_rectangle(cr.get(), 0, 0, this->sidebarPreview->page->getWidth(), this->sidebarPreview->page->getHeight());
|
||||||
|
cairo_clip(cr.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewJob::run() {
|
||||||
|
if (this->sidebarPreview == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initGraphics();
|
||||||
|
clipToPage();
|
||||||
|
drawPage();
|
||||||
|
finishPaint();
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A job which handles preview repaint
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_surface_t, cairo_t
|
||||||
|
|
||||||
|
#include "util/raii/CairoWrappers.h"
|
||||||
|
|
||||||
|
#include "Job.h" // for Job, JobType
|
||||||
|
|
||||||
|
class SidebarPreviewBaseEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A Job which renders a SidebarPreviewPage
|
||||||
|
*/
|
||||||
|
class PreviewJob: public Job {
|
||||||
|
public:
|
||||||
|
PreviewJob(SidebarPreviewBaseEntry* sidebar);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onDelete() override;
|
||||||
|
~PreviewJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void* getSource() override;
|
||||||
|
|
||||||
|
void run() override;
|
||||||
|
|
||||||
|
JobType getType() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initGraphics();
|
||||||
|
void clipToPage();
|
||||||
|
void finishPaint();
|
||||||
|
void drawPage();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Graphics buffer
|
||||||
|
*/
|
||||||
|
xoj::util::CairoSurfaceSPtr buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graphics drawing
|
||||||
|
*/
|
||||||
|
xoj::util::CairoSPtr cr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidebar preview
|
||||||
|
*/
|
||||||
|
SidebarPreviewBaseEntry* sidebarPreview = nullptr;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Interface for progress state
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
class ProgressListener {
|
||||||
|
public:
|
||||||
|
virtual void setMaximumState(size_t max) = 0;
|
||||||
|
virtual void setCurrentState(size_t state) = 0;
|
||||||
|
|
||||||
|
virtual ~ProgressListener(){};
|
||||||
|
};
|
||||||
|
|
||||||
|
class DummyProgressListener: public ProgressListener {
|
||||||
|
public:
|
||||||
|
void setMaximumState(size_t max) override{};
|
||||||
|
void setCurrentState(size_t state) override{};
|
||||||
|
|
||||||
|
~DummyProgressListener() override{};
|
||||||
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#include "RenderJob.h"
|
||||||
|
|
||||||
|
#include <mutex> // for mutex
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_create, cairo_destroy, cairo_...
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/ToolEnums.h" // for TOOL_PLAY_OBJECT
|
||||||
|
#include "control/ToolHandler.h" // for ToolHandler
|
||||||
|
#include "control/jobs/Job.h" // for JOB_TYPE_RENDER, JobType
|
||||||
|
#include "gui/PageView.h" // for XojPageView
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "gui/widgets/XournalWidget.h" // for gtk_xournal_repaint_area
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/XojPage.h" // for Page
|
||||||
|
#include "util/Assert.h" // for xoj_assert
|
||||||
|
#include "util/Rectangle.h" // for Rectangle
|
||||||
|
#include "util/Util.h" // for execInUiThread
|
||||||
|
#include "util/raii/CairoWrappers.h" // for CairoSurfaceSPtr, CairoSPtr
|
||||||
|
#include "util/safe_casts.h" // for strict_cast, as_signed, as_si...
|
||||||
|
#include "view/DocumentView.h" // for DocumentView
|
||||||
|
#include "view/Mask.h" // for Mask
|
||||||
|
|
||||||
|
#if defined(__has_cpp_attribute) && __has_cpp_attribute(likely)
|
||||||
|
#define XOJ_CPP20_UNLIKELY [[unlikely]]
|
||||||
|
#else
|
||||||
|
#define XOJ_CPP20_UNLIKELY
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using xoj::util::Rectangle;
|
||||||
|
|
||||||
|
RenderJob::RenderJob(XojPageView* view): view(view) {}
|
||||||
|
|
||||||
|
auto RenderJob::getSource() -> void* { return this->view; }
|
||||||
|
|
||||||
|
void RenderJob::rerenderRectangle(Rectangle<double> const& rect) {
|
||||||
|
/**
|
||||||
|
* Padding seems to be necessary to prevent artefacts of most strokes.
|
||||||
|
* These artefacts are most pronounced when using the stroke deletion
|
||||||
|
* tool on ellipses, but also occur occasionally when removing regular
|
||||||
|
* strokes.
|
||||||
|
**/
|
||||||
|
constexpr int RENDER_PADDING = 1;
|
||||||
|
|
||||||
|
Range maskRange(rect);
|
||||||
|
maskRange.addPadding(RENDER_PADDING);
|
||||||
|
xoj::view::Mask newMask(view->xournal->getDpiScaleFactor(), maskRange, view->xournal->getZoom(),
|
||||||
|
CAIRO_CONTENT_COLOR_ALPHA);
|
||||||
|
|
||||||
|
renderToBuffer(newMask.get());
|
||||||
|
|
||||||
|
std::lock_guard lock(this->view->drawingMutex);
|
||||||
|
if (!view->buffer.isInitialized()) {
|
||||||
|
// Todo: the buffer must not be uninitializable here, either by moving it into the job or by locking it at job
|
||||||
|
// creation a shared prt may also be suffice.
|
||||||
|
XOJ_CPP20_UNLIKELY return;
|
||||||
|
}
|
||||||
|
newMask.paintTo(view->buffer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderJob::run() {
|
||||||
|
this->view->repaintRectMutex.lock();
|
||||||
|
|
||||||
|
bool rerenderComplete = std::exchange(this->view->rerenderComplete, false);
|
||||||
|
bool sizeChanged = std::exchange(this->view->sizeChanged, false);
|
||||||
|
auto rerenderRects = std::move(this->view->rerenderRects);
|
||||||
|
|
||||||
|
this->view->repaintRectMutex.unlock();
|
||||||
|
|
||||||
|
if (rerenderComplete) {
|
||||||
|
xoj::view::Mask newMask(view->xournal->getDpiScaleFactor(),
|
||||||
|
Range(0, 0, view->page->getWidth(), view->page->getHeight()), view->xournal->getZoom(),
|
||||||
|
CAIRO_CONTENT_COLOR_ALPHA);
|
||||||
|
|
||||||
|
renderToBuffer(newMask.get());
|
||||||
|
{
|
||||||
|
std::lock_guard lock(this->view->drawingMutex);
|
||||||
|
std::swap(this->view->buffer, newMask);
|
||||||
|
}
|
||||||
|
if (sizeChanged) {
|
||||||
|
// We do not have any control on what portion of the widget needs to be redrawn. Redraw it all.
|
||||||
|
Util::execInUiThread([w = view->xournal->getWidget()]() { gtk_widget_queue_draw(w); });
|
||||||
|
} else {
|
||||||
|
repaintPage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Rectangle<double> const& rect: rerenderRects) {
|
||||||
|
rerenderRectangle(rect);
|
||||||
|
repaintPageArea(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void repaintWidgetArea(GtkWidget* widget, int x1, int y1, int x2, int y2) {
|
||||||
|
Util::execInUiThread([=]() { gtk_xournal_repaint_area(widget, x1, y1, x2, y2); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderJob::repaintPage() const { repaintPageArea(0, 0, view->getWidth(), view->getHeight()); }
|
||||||
|
|
||||||
|
void RenderJob::repaintPageArea(double x1, double y1, double x2, double y2) const {
|
||||||
|
double zoom = view->xournal->getZoom();
|
||||||
|
int x = view->getX();
|
||||||
|
int y = view->getY();
|
||||||
|
repaintWidgetArea(view->xournal->getWidget(), x + floor_cast<int>(zoom * x1), y + floor_cast<int>(zoom * y1),
|
||||||
|
x + ceil_cast<int>(zoom * x2), y + ceil_cast<int>(zoom * y2));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderJob::renderToBuffer(cairo_t* cr) const {
|
||||||
|
DocumentView localView;
|
||||||
|
localView.setMarkAudioStroke(this->view->getXournal()->getControl()->getToolHandler()->getToolType() ==
|
||||||
|
TOOL_PLAY_OBJECT);
|
||||||
|
localView.setPdfCache(this->view->xournal->getCache());
|
||||||
|
|
||||||
|
std::shared_lock<Document> lock(*this->view->xournal->getDocument());
|
||||||
|
localView.drawPage(this->view->page, cr, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto RenderJob::getType() -> JobType { return JOB_TYPE_RENDER; }
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A job which redraws a page or a page region
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_surface_t
|
||||||
|
#include <gtk/gtk.h> // for GtkWidget
|
||||||
|
|
||||||
|
#include "Job.h" // for Job, JobType
|
||||||
|
|
||||||
|
class XojPageView;
|
||||||
|
namespace xoj::util {
|
||||||
|
template <class T>
|
||||||
|
class Rectangle;
|
||||||
|
} // namespace xoj::util
|
||||||
|
|
||||||
|
class RenderJob: public Job {
|
||||||
|
public:
|
||||||
|
RenderJob(XojPageView* view);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~RenderJob() override = default;
|
||||||
|
|
||||||
|
public:
|
||||||
|
JobType getType() override;
|
||||||
|
|
||||||
|
void* getSource() override;
|
||||||
|
|
||||||
|
void run() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void repaintPage() const;
|
||||||
|
|
||||||
|
void repaintPageArea(double x1, double y1, double x2, double y2) const;
|
||||||
|
|
||||||
|
void rerenderRectangle(xoj::util::Rectangle<double> const& rect);
|
||||||
|
|
||||||
|
void renderToBuffer(cairo_t* cr) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
XojPageView* view;
|
||||||
|
};
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#include "SaveJob.h"
|
||||||
|
|
||||||
|
#include <memory> // for __shared_ptr_access
|
||||||
|
|
||||||
|
#include <cairo.h> // for cairo_create, cairo_destroy
|
||||||
|
#include <glib.h> // for g_warning, g_error
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/jobs/BlockingJob.h" // for BlockingJob
|
||||||
|
#include "control/xojfile/SaveHandler.h" // for SaveHandler
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/PageRef.h" // for PageRef
|
||||||
|
#include "model/PageType.h" // for PageType
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "pdf/base/XojPdfPage.h" // for XojPdfPageSPtr, XojPdfPage
|
||||||
|
#include "util/PathUtil.h" // for clearExtensions, safeRename...
|
||||||
|
#include "util/XojMsgBox.h" // for XojMsgBox
|
||||||
|
#include "util/i18n.h" // for FS, _, _F
|
||||||
|
#include "view/DocumentView.h" // for DocumentView
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path, filesystem_error, remove
|
||||||
|
|
||||||
|
|
||||||
|
SaveJob::SaveJob(Control* control, std::function<void(bool)> callback):
|
||||||
|
BlockingJob(control, _("Save")), callback(std::move(callback)) {}
|
||||||
|
|
||||||
|
SaveJob::~SaveJob() = default;
|
||||||
|
|
||||||
|
void SaveJob::run() {
|
||||||
|
save();
|
||||||
|
|
||||||
|
if (this->control->getWindow()) {
|
||||||
|
callAfterRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveJob::afterRun() {
|
||||||
|
if (!this->lastError.empty()) {
|
||||||
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), this->lastError);
|
||||||
|
callback(false);
|
||||||
|
} else {
|
||||||
|
this->control->resetSavedStatus();
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveJob::updatePreview(Control* control) {
|
||||||
|
const int previewSize = 128;
|
||||||
|
|
||||||
|
Document* doc = control->getDocument();
|
||||||
|
xoj::util::CairoSurfaceSPtr crBuffer;
|
||||||
|
|
||||||
|
doc->lock_shared();
|
||||||
|
if (doc->getPageCount() > 0) {
|
||||||
|
PageRef page = doc->getPage(0);
|
||||||
|
|
||||||
|
double width = page->getWidth();
|
||||||
|
double height = page->getHeight();
|
||||||
|
|
||||||
|
double zoom = 1;
|
||||||
|
|
||||||
|
if (width < height) {
|
||||||
|
zoom = previewSize / height;
|
||||||
|
} else {
|
||||||
|
zoom = previewSize / width;
|
||||||
|
}
|
||||||
|
width *= zoom;
|
||||||
|
height *= zoom;
|
||||||
|
|
||||||
|
crBuffer.reset(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ceil_cast<int>(width), ceil_cast<int>(height)),
|
||||||
|
xoj::util::adopt);
|
||||||
|
|
||||||
|
cairo_t* cr = cairo_create(crBuffer.get());
|
||||||
|
cairo_scale(cr, zoom, zoom);
|
||||||
|
|
||||||
|
xoj::view::BackgroundFlags flags = xoj::view::BACKGROUND_SHOW_ALL;
|
||||||
|
|
||||||
|
// We don't have access to a PdfCache on which DocumentView relies for PDF backgrounds.
|
||||||
|
// We thus print the PDF background by hand.
|
||||||
|
if (page->getBackgroundType().isPdfPage()) {
|
||||||
|
auto pgNo = page->getPdfPageNr();
|
||||||
|
XojPdfPageSPtr popplerPage = doc->getPdfPage(pgNo);
|
||||||
|
if (popplerPage) {
|
||||||
|
popplerPage->render(cr);
|
||||||
|
}
|
||||||
|
flags.showPDF = xoj::view::HIDE_PDF_BACKGROUND; // Already printed (if any)
|
||||||
|
} else {
|
||||||
|
flags.forceBackgroundColor = xoj::view::FORCE_AT_LEAST_BACKGROUND_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentView view;
|
||||||
|
view.drawPage(page, cr, true /* don't render erasable */, flags);
|
||||||
|
cairo_destroy(cr);
|
||||||
|
}
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
doc->lock();
|
||||||
|
doc->setPreview(std::move(crBuffer));
|
||||||
|
doc->unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SaveJob::save() -> bool {
|
||||||
|
updatePreview(control);
|
||||||
|
Document* doc = this->control->getDocument();
|
||||||
|
SaveHandler h;
|
||||||
|
|
||||||
|
doc->lock_shared();
|
||||||
|
fs::path target = doc->getFilepath();
|
||||||
|
Util::safeReplaceExtension(target, "xopp");
|
||||||
|
|
||||||
|
h.prepareSave(doc, target);
|
||||||
|
doc->unlock_shared();
|
||||||
|
|
||||||
|
auto const createBackup = doc->shouldCreateBackupOnSave();
|
||||||
|
|
||||||
|
if (createBackup) {
|
||||||
|
try {
|
||||||
|
// Note: The backup must be created for the target as this is the filepath
|
||||||
|
// which will be written to. Do not use the `filepath` variable!
|
||||||
|
Util::safeRenameFile(target, fs::path{target} += "~");
|
||||||
|
} catch (const fs::filesystem_error& fe) {
|
||||||
|
g_warning("Could not create backup! Failed with %s", fe.what());
|
||||||
|
this->lastError = FS(_F("Save file error, can't backup: {1}") % std::string(fe.what()));
|
||||||
|
if (!control->getWindow()) {
|
||||||
|
g_error("%s", this->lastError.c_str());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.saveTo(target, this->control);
|
||||||
|
|
||||||
|
doc->lock();
|
||||||
|
h.updateDocumentInfo(doc);
|
||||||
|
doc->setFilepath(target);
|
||||||
|
doc->unlock();
|
||||||
|
|
||||||
|
if (!h.getErrorMessage().empty()) {
|
||||||
|
this->lastError = FS(_F("Save file error: {1}") % h.getErrorMessage());
|
||||||
|
if (!control->getWindow()) {
|
||||||
|
g_error("%s", this->lastError.c_str());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (createBackup) {
|
||||||
|
try {
|
||||||
|
// If a backup was created it can be removed now since no error occured during the save
|
||||||
|
fs::remove(fs::path{target} += "~");
|
||||||
|
} catch (const fs::filesystem_error& fe) {
|
||||||
|
g_warning("Could not delete backup! Failed with %s", fe.what());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc->setCreateBackupOnSave(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A job which saves a Document
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "BlockingJob.h" // for BlockingJob
|
||||||
|
|
||||||
|
class Control;
|
||||||
|
|
||||||
|
|
||||||
|
class SaveJob: public BlockingJob {
|
||||||
|
public:
|
||||||
|
SaveJob(Control* control, std::function<void(bool)> = [](bool) {});
|
||||||
|
|
||||||
|
protected:
|
||||||
|
~SaveJob() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void run() override;
|
||||||
|
|
||||||
|
bool save();
|
||||||
|
|
||||||
|
static void updatePreview(Control* control);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void afterRun() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string lastError;
|
||||||
|
/// Called after saving, with boolean parameter true on success, false on failure (error)
|
||||||
|
std::function<void(bool)> callback;
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
#include "Scheduler.h"
|
||||||
|
|
||||||
|
#include <cinttypes> // for PRId64
|
||||||
|
#include <cstdint> // for uint64_t
|
||||||
|
|
||||||
|
#include "control/jobs/Job.h" // for Job, JOB_TYPE_RENDER
|
||||||
|
#include "util/Assert.h" // for xoj_assert
|
||||||
|
#include "util/glib_casts.h" // for wrap_for_once_v
|
||||||
|
|
||||||
|
#include "config-debug.h" // for DEBUG_SCHEDULER
|
||||||
|
|
||||||
|
#ifdef DEBUG_SCHEDULER
|
||||||
|
#define SDEBUG g_message
|
||||||
|
#else
|
||||||
|
#define SDEBUG(msg, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Scheduler::Scheduler() {
|
||||||
|
this->name = "Scheduler";
|
||||||
|
|
||||||
|
// Queue
|
||||||
|
this->jobQueue[JOB_PRIORITY_URGENT] = &this->queueUrgent;
|
||||||
|
this->jobQueue[JOB_PRIORITY_HIGH] = &this->queueHigh;
|
||||||
|
this->jobQueue[JOB_PRIORITY_LOW] = &this->queueLow;
|
||||||
|
this->jobQueue[JOB_PRIORITY_NONE] = &this->queueNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scheduler::~Scheduler() {
|
||||||
|
SDEBUG("Destroy scheduler");
|
||||||
|
|
||||||
|
if (auto id = this->jobRenderThreadTimerId.exchange(0); id != 0) {
|
||||||
|
g_source_remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop();
|
||||||
|
|
||||||
|
Job* job = nullptr;
|
||||||
|
while ((job = getNextJobUnlocked()) != nullptr) {
|
||||||
|
job->unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scheduler::start() {
|
||||||
|
SDEBUG("Starting scheduler");
|
||||||
|
g_return_if_fail(this->thread == nullptr);
|
||||||
|
|
||||||
|
this->thread = g_thread_new(name.c_str(), reinterpret_cast<GThreadFunc>(jobThreadCallback), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scheduler::stop() {
|
||||||
|
SDEBUG("Stopping scheduler");
|
||||||
|
|
||||||
|
if (!this->threadRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->threadRunning = false;
|
||||||
|
this->jobQueueCond.notify_all();
|
||||||
|
|
||||||
|
if (this->thread) {
|
||||||
|
g_thread_join(this->thread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scheduler::addJob(Job* job, JobPriority priority) {
|
||||||
|
SDEBUG("Adding job...");
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard lock{this->jobQueueMutex};
|
||||||
|
|
||||||
|
job->ref();
|
||||||
|
this->jobQueue[priority]->push_back(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDEBUG("add job: %" PRId64 "; type: %" PRId64, (uint64_t)job, (uint64_t)job->getType());
|
||||||
|
this->jobQueueCond.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Scheduler::getNextJobUnlocked(bool onlyNotRender, bool* hasRenderJobs) -> Job* {
|
||||||
|
Job* job = nullptr;
|
||||||
|
|
||||||
|
for (size_t i = JOB_PRIORITY_URGENT; i < JOB_N_PRIORITIES; i++) {
|
||||||
|
std::deque<Job*>& queue = *this->jobQueue[i];
|
||||||
|
|
||||||
|
if (onlyNotRender) {
|
||||||
|
for (auto it = queue.begin(); it != queue.end(); ++it) {
|
||||||
|
job = *it;
|
||||||
|
|
||||||
|
if (job->getType() != JOB_TYPE_RENDER) {
|
||||||
|
queue.erase(it);
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRenderJobs != nullptr) {
|
||||||
|
*hasRenderJobs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!queue.empty()) {
|
||||||
|
job = queue.front();
|
||||||
|
queue.pop_front();
|
||||||
|
xoj_assert(job != nullptr);
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the complete scheduler
|
||||||
|
*/
|
||||||
|
void Scheduler::lock() { this->schedulerMutex.lock(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlocks the complete scheduler
|
||||||
|
*/
|
||||||
|
void Scheduler::unlock() { this->schedulerMutex.unlock(); }
|
||||||
|
|
||||||
|
#define ZOOM_WAIT_US_TIMEOUT 300000 // 0.3s
|
||||||
|
|
||||||
|
void Scheduler::blockRerenderZoom() { this->blockRenderZoomTime = g_get_monotonic_time() + ZOOM_WAIT_US_TIMEOUT; }
|
||||||
|
|
||||||
|
void Scheduler::unblockRerenderZoom() {
|
||||||
|
this->blockRenderZoomTime = 0;
|
||||||
|
|
||||||
|
if (auto id = this->jobRenderThreadTimerId.exchange(0); id != 0) {
|
||||||
|
g_source_remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->jobQueueCond.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the Scheduler is blocking because we are zooming and there are only render jobs
|
||||||
|
* we need to wakeup it later
|
||||||
|
*/
|
||||||
|
auto Scheduler::jobRenderThreadTimer(Scheduler* scheduler) -> bool {
|
||||||
|
scheduler->jobRenderThreadTimerId = 0;
|
||||||
|
scheduler->blockRenderZoomTime = 0;
|
||||||
|
|
||||||
|
scheduler->jobQueueCond.notify_all();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Scheduler::jobThreadCallback(Scheduler* scheduler) -> gpointer {
|
||||||
|
while (scheduler->threadRunning) {
|
||||||
|
// lock the whole scheduler
|
||||||
|
std::unique_lock schedulerLock{scheduler->schedulerMutex};
|
||||||
|
SDEBUG("Job Thread: Blocked scheduler.");
|
||||||
|
|
||||||
|
bool onlyNonRenderJobs = false;
|
||||||
|
gint64 diff = 1000;
|
||||||
|
if (scheduler->blockRenderZoomTime) {
|
||||||
|
SDEBUG("Zoom re-render blocking.");
|
||||||
|
|
||||||
|
diff = (scheduler->blockRenderZoomTime - g_get_monotonic_time()) / 1000;
|
||||||
|
if (diff <= 0) {
|
||||||
|
scheduler->blockRenderZoomTime = 0;
|
||||||
|
SDEBUG("Ended zoom re-render blocking.");
|
||||||
|
} else {
|
||||||
|
onlyNonRenderJobs = true;
|
||||||
|
SDEBUG("Rendering blocked: Only running non-rendering jobs.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Job* job;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock jobLock{scheduler->jobQueueMutex};
|
||||||
|
SDEBUG("Job Thread: Locked job queue.");
|
||||||
|
|
||||||
|
bool hasOnlyRenderJobs = false;
|
||||||
|
job = scheduler->getNextJobUnlocked(onlyNonRenderJobs, &hasOnlyRenderJobs);
|
||||||
|
if (job != nullptr) {
|
||||||
|
hasOnlyRenderJobs = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDEBUG("get job: %" PRId64, (uint64_t)job);
|
||||||
|
|
||||||
|
if (job == nullptr) {
|
||||||
|
// unlock the whole scheduler
|
||||||
|
schedulerLock.unlock();
|
||||||
|
|
||||||
|
if (hasOnlyRenderJobs) {
|
||||||
|
if (auto id = scheduler->jobRenderThreadTimerId.exchange(g_timeout_add(
|
||||||
|
static_cast<guint>(diff), xoj::util::wrap_for_once_v<jobRenderThreadTimer>, scheduler));
|
||||||
|
id != 0) {
|
||||||
|
g_source_remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler->jobQueueCond.wait(jobLock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the job.
|
||||||
|
{
|
||||||
|
std::lock_guard lock{scheduler->jobRunningMutex};
|
||||||
|
SDEBUG("do job: %" PRId64, (uint64_t)job);
|
||||||
|
job->execute();
|
||||||
|
job->unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDEBUG("next");
|
||||||
|
}
|
||||||
|
|
||||||
|
SDEBUG("finished");
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A scheduler for background jobs
|
||||||
|
*
|
||||||
|
* Some code from Evince project
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array> // for array
|
||||||
|
#include <atomic> // for atomic
|
||||||
|
#include <condition_variable> // for condition_variable
|
||||||
|
#include <deque> // for deque
|
||||||
|
#include <mutex> // for mutex
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include <glib.h> // for GThread, GTimeVal, gpointer
|
||||||
|
|
||||||
|
class Job;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file Scheduler.h
|
||||||
|
* @brief A file containing the definition of the Scheduler
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum JobPriority
|
||||||
|
*
|
||||||
|
* The priority of the job affects the order of execution:
|
||||||
|
* Jobs with higher priority are processed before the
|
||||||
|
* lower ones.
|
||||||
|
*/
|
||||||
|
enum JobPriority {
|
||||||
|
/**
|
||||||
|
* Urgent: used for rendering the current page
|
||||||
|
*/
|
||||||
|
JOB_PRIORITY_URGENT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High: used for rendering thumbnail ranges
|
||||||
|
*/
|
||||||
|
JOB_PRIORITY_HIGH,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low: used for rendering of pages not in the current range
|
||||||
|
*/
|
||||||
|
JOB_PRIORITY_LOW,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* None: used for any other job (loading / saving / printing...)
|
||||||
|
*/
|
||||||
|
JOB_PRIORITY_NONE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of priorities
|
||||||
|
*/
|
||||||
|
JOB_N_PRIORITIES
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler {
|
||||||
|
public:
|
||||||
|
Scheduler();
|
||||||
|
virtual ~Scheduler();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Adds a Job to the Scheduler
|
||||||
|
*
|
||||||
|
* @param Job the job
|
||||||
|
* @param priority the desired priority
|
||||||
|
*
|
||||||
|
* The Job is now owned by the scheduler, and automatically freed if it is done
|
||||||
|
*/
|
||||||
|
void addJob(Job* job, JobPriority priority);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the complete scheduler
|
||||||
|
*/
|
||||||
|
void lock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlocks the complete scheduler
|
||||||
|
*/
|
||||||
|
void unlock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't render the next X ms so the scrolling performance is better
|
||||||
|
*/
|
||||||
|
void blockRerenderZoom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the blocked rendering manually
|
||||||
|
*/
|
||||||
|
void unblockRerenderZoom();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static auto jobThreadCallback(Scheduler* scheduler) -> gpointer;
|
||||||
|
auto getNextJobUnlocked(bool onlyNotRender = false, bool* hasRenderJobs = nullptr) -> Job*;
|
||||||
|
|
||||||
|
static auto jobRenderThreadTimer(Scheduler* scheduler) -> bool;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool threadRunning = true;
|
||||||
|
|
||||||
|
GThread* thread = nullptr;
|
||||||
|
|
||||||
|
std::condition_variable jobQueueCond{};
|
||||||
|
std::mutex jobQueueMutex{};
|
||||||
|
std::mutex schedulerMutex{};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is need to be sure there is no job running if we delete a page.
|
||||||
|
* If a job is, we may access deleted memory.
|
||||||
|
*/
|
||||||
|
std::mutex jobRunningMutex{};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jobs of each priority. New jobs
|
||||||
|
* are added to the back of each queue.
|
||||||
|
*/
|
||||||
|
std::deque<Job*> queueUrgent{};
|
||||||
|
std::deque<Job*> queueHigh{};
|
||||||
|
std::deque<Job*> queueLow{};
|
||||||
|
std::deque<Job*> queueNone{};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map (JobPriority) -> queue of that priority.
|
||||||
|
* For example,
|
||||||
|
* jobQueue[JOB_PRIORITY_URGENT] is &queueUrgent.
|
||||||
|
*/
|
||||||
|
std::array<std::deque<Job*>*, JOB_N_PRIORITIES> jobQueue{};
|
||||||
|
|
||||||
|
std::atomic<gint64> blockRenderZoomTime = 0;
|
||||||
|
std::atomic<guint> jobRenderThreadTimerId = 0;
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#include "XournalScheduler.h"
|
||||||
|
|
||||||
|
#include <array> // for array
|
||||||
|
#include <deque> // for _Deque_iterator, deque, operator!=
|
||||||
|
#include <mutex> // for lock_guard
|
||||||
|
#include <string> // for string
|
||||||
|
|
||||||
|
#include "control/jobs/Scheduler.h" // for JOB_PRIORITY_URGENT, JOB_PRIORIT...
|
||||||
|
|
||||||
|
#include "PreviewJob.h" // for PreviewJob
|
||||||
|
#include "RenderJob.h" // for RenderJob
|
||||||
|
|
||||||
|
class SidebarPreviewBaseEntry;
|
||||||
|
class XojPageView;
|
||||||
|
|
||||||
|
XournalScheduler::XournalScheduler() { this->name = "XournalScheduler"; }
|
||||||
|
|
||||||
|
XournalScheduler::~XournalScheduler() = default;
|
||||||
|
|
||||||
|
void XournalScheduler::removeSidebar(SidebarPreviewBaseEntry* preview) {
|
||||||
|
// Wait for running jobs to finish: Currently running jobs may still be
|
||||||
|
// using `preview`, and, as such, it is not completely removed.
|
||||||
|
bool waitForTaskCompletion = true;
|
||||||
|
removeSource(preview, JOB_TYPE_PREVIEW, JOB_PRIORITY_HIGH, waitForTaskCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XournalScheduler::removePage(XojPageView* view) { removeSource(view, JOB_TYPE_RENDER, JOB_PRIORITY_URGENT); }
|
||||||
|
|
||||||
|
void XournalScheduler::removeAllJobs() {
|
||||||
|
std::lock_guard lock{this->jobQueueMutex};
|
||||||
|
|
||||||
|
for (size_t priority = JOB_PRIORITY_URGENT; priority < JOB_N_PRIORITIES; priority++) {
|
||||||
|
std::deque<Job*>& queue = *this->jobQueue[priority];
|
||||||
|
auto it = queue.begin();
|
||||||
|
|
||||||
|
while (it != queue.end()) {
|
||||||
|
Job* job = *it;
|
||||||
|
|
||||||
|
// Only remove PREVIEW and RENDER jobs; we aren't
|
||||||
|
// responsible for other types of jobs.
|
||||||
|
JobType type = job->getType();
|
||||||
|
if (type == JOB_TYPE_PREVIEW || type == JOB_TYPE_RENDER) {
|
||||||
|
job->deleteJob();
|
||||||
|
|
||||||
|
it = queue.erase(it);
|
||||||
|
|
||||||
|
job->unref();
|
||||||
|
job = nullptr;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XournalScheduler::finishTask() { std::lock_guard lock{this->jobRunningMutex}; }
|
||||||
|
|
||||||
|
void XournalScheduler::removeSource(void* source, JobType type, JobPriority priority, bool awaitFinishTask) {
|
||||||
|
{
|
||||||
|
std::lock_guard lock{this->jobQueueMutex};
|
||||||
|
std::deque<Job*>& queue = *this->jobQueue[priority];
|
||||||
|
|
||||||
|
auto it = queue.begin();
|
||||||
|
|
||||||
|
while (it != queue.end()) {
|
||||||
|
Job* job = *it;
|
||||||
|
|
||||||
|
if (job->getType() == type && job->getSource() == source) {
|
||||||
|
it = queue.erase(it);
|
||||||
|
|
||||||
|
job->deleteJob();
|
||||||
|
job->unref();
|
||||||
|
job = nullptr;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until the last job is done
|
||||||
|
// we can be sure we don't access "source"
|
||||||
|
if (awaitFinishTask) {
|
||||||
|
finishTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto XournalScheduler::existsSource(void* source, JobType type, JobPriority priority) -> bool {
|
||||||
|
bool exists = false;
|
||||||
|
std::lock_guard lock{this->jobQueueMutex};
|
||||||
|
|
||||||
|
for (Job* job: *this->jobQueue[priority]) {
|
||||||
|
if (job->getType() == type && job->getSource() == source) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XournalScheduler::addRepaintSidebar(SidebarPreviewBaseEntry* preview) {
|
||||||
|
if (existsSource(preview, JOB_TYPE_PREVIEW, JOB_PRIORITY_HIGH)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* job = new PreviewJob(preview);
|
||||||
|
addJob(job, JOB_PRIORITY_HIGH);
|
||||||
|
job->unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
void XournalScheduler::addRerenderPage(XojPageView* view) {
|
||||||
|
if (existsSource(view, JOB_TYPE_RENDER, JOB_PRIORITY_URGENT)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* job = new RenderJob(view);
|
||||||
|
addJob(job, JOB_PRIORITY_URGENT);
|
||||||
|
job->unref();
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* A scheduler for Xournal background tasks
|
||||||
|
*
|
||||||
|
* Some code from Evince project
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "control/jobs/Job.h" // for JobType
|
||||||
|
|
||||||
|
#include "Scheduler.h" // for JobPriority, Scheduler
|
||||||
|
|
||||||
|
class SidebarPreviewBaseEntry;
|
||||||
|
class XojPageView;
|
||||||
|
|
||||||
|
class XournalScheduler: public Scheduler {
|
||||||
|
public:
|
||||||
|
XournalScheduler();
|
||||||
|
~XournalScheduler() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Remove source, e.g. if a page is removed they don't need to repaint.
|
||||||
|
* Blocks until all jobs that could be using the source have finished.
|
||||||
|
*/
|
||||||
|
void removeSidebar(SidebarPreviewBaseEntry* preview);
|
||||||
|
void removePage(XojPageView* view);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all PreviewJob%s / RenderJob%s scheduled to be run
|
||||||
|
*/
|
||||||
|
void removeAllJobs();
|
||||||
|
|
||||||
|
void addRepaintSidebar(SidebarPreviewBaseEntry* preview);
|
||||||
|
void addRerenderPage(XojPageView* view);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks until all currently running Job%s have been executed
|
||||||
|
*/
|
||||||
|
void finishTask();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Remove source, e.g. if a page is removed they don't need to repaint
|
||||||
|
*/
|
||||||
|
void removeSource(void* source, JobType type, JobPriority priority, bool awaitFinishTask = true);
|
||||||
|
|
||||||
|
bool existsSource(void* source, JobType type, JobPriority priority);
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#include "LatexGenerator.h"
|
||||||
|
|
||||||
|
#include <regex> // for smatch, sregex_iterator
|
||||||
|
#include <sstream> // for ostringstream
|
||||||
|
#include <string_view> // for string_view
|
||||||
|
|
||||||
|
#include <glib.h> // for GError, gchar, g_error_free
|
||||||
|
#include <poppler.h> // for g_object_unref
|
||||||
|
|
||||||
|
#include "control/settings/LatexSettings.h" // for LatexSettings
|
||||||
|
#include "util/PathUtil.h" // for getLongPath
|
||||||
|
#include "util/PlaceholderString.h" // for PlaceholderString
|
||||||
|
#include "util/Util.h" // for Util
|
||||||
|
#include "util/i18n.h" // for FS, _F
|
||||||
|
#include "util/raii/GLibGuards.h" // for GErrorGuard, GStrvGuard
|
||||||
|
#include "util/raii/GObjectSPtr.h" // for GObjectSptr
|
||||||
|
#include "util/safe_casts.h" // for as_signed
|
||||||
|
|
||||||
|
|
||||||
|
using namespace xoj::util;
|
||||||
|
|
||||||
|
LatexGenerator::LatexGenerator(const LatexSettings& settings): settings(settings) {}
|
||||||
|
|
||||||
|
auto LatexGenerator::templateSub(const std::string& input, const std::string& templ, const Color textColor)
|
||||||
|
-> std::string {
|
||||||
|
const static std::regex substRe("%%XPP_((TOOL_INPUT)|(TEXT_COLOR))%%");
|
||||||
|
std::string output;
|
||||||
|
output.reserve(templ.length());
|
||||||
|
size_t templatePos = 0;
|
||||||
|
for (std::sregex_iterator it(templ.begin(), templ.end(), substRe); it != std::sregex_iterator{}; it++) {
|
||||||
|
std::smatch match = *it;
|
||||||
|
std::string matchStr = match[1];
|
||||||
|
std::string repl;
|
||||||
|
// Performance can be optimized here by precomputing hashes
|
||||||
|
if (matchStr == "TOOL_INPUT") {
|
||||||
|
repl = input;
|
||||||
|
} else if (matchStr == "TEXT_COLOR") {
|
||||||
|
repl = Util::rgb_to_hex_string(textColor).substr(1);
|
||||||
|
}
|
||||||
|
output.append(templ, templatePos, as_unsigned(match.position()) - templatePos);
|
||||||
|
output.append(repl);
|
||||||
|
templatePos = as_unsigned(match.position() + match.length());
|
||||||
|
}
|
||||||
|
output.append(templ, templatePos);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LatexGenerator::asyncRun(const fs::path& texDir, const std::string& texFileContents) -> Result {
|
||||||
|
std::string cmd = this->settings.genCmd;
|
||||||
|
GErrorGuard err{};
|
||||||
|
std::string texFilePathOSEncoding = Util::GFilename(Util::getLongPath(texDir) / "tex.tex").c_str();
|
||||||
|
|
||||||
|
for (auto i = cmd.find("{}"); i != std::string::npos; i = cmd.find("{}", i + texFilePathOSEncoding.length())) {
|
||||||
|
cmd.replace(i, 2, texFilePathOSEncoding);
|
||||||
|
}
|
||||||
|
// Todo (rolandlo): is this a todo?
|
||||||
|
// Windows note: g_shell_parse_argv assumes POSIX paths, so Windows paths need to be escaped.
|
||||||
|
GStrvGuard argv{};
|
||||||
|
if (!g_shell_parse_argv(cmd.c_str(), nullptr, out_ptr(argv), out_ptr(err))) {
|
||||||
|
return GenError{FS(_F("Failed to parse LaTeX generator command: {1}") % err->message)};
|
||||||
|
}
|
||||||
|
gchar* prog = argv.get()[0];
|
||||||
|
if (!prog || !(prog = g_find_program_in_path(prog))) {
|
||||||
|
if (Util::isFlatpakInstallation()) {
|
||||||
|
return GenError{
|
||||||
|
FS(_F("Failed to find LaTeX generator program in PATH: {1}\n\nSince installation is detected "
|
||||||
|
"within Flatpak, you need to install the Flatpak freedesktop Tex Live extension. For "
|
||||||
|
"example, by running:\n\n$ flatpak install flathub org.freedesktop.Sdk.Extension.texlive") %
|
||||||
|
argv.get()[0])};
|
||||||
|
} else {
|
||||||
|
return GenError{FS(_F("Failed to find LaTeX generator program in PATH: {1}") % argv.get()[0])};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_free(argv.get()[0]);
|
||||||
|
argv.get()[0] = prog;
|
||||||
|
|
||||||
|
if (!g_file_set_contents(texFilePathOSEncoding.c_str(), texFileContents.c_str(), as_signed(texFileContents.size()),
|
||||||
|
out_ptr(err))) {
|
||||||
|
return GenError({FS(_F("Could not save .tex file: {1}") % err->message)});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto flags = static_cast<GSubprocessFlags>(G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_MERGE);
|
||||||
|
xoj::util::GObjectSPtr<GSubprocessLauncher> launcher(g_subprocess_launcher_new(flags), xoj::util::adopt);
|
||||||
|
g_subprocess_launcher_set_cwd(launcher.get(), Util::GFilename(texDir).c_str());
|
||||||
|
auto* proc = g_subprocess_launcher_spawnv(launcher.get(), argv.get(), out_ptr(err));
|
||||||
|
|
||||||
|
if (proc) {
|
||||||
|
return {proc};
|
||||||
|
}
|
||||||
|
std::ostringstream ss;
|
||||||
|
for (char** iter = argv.get(); iter != nullptr && *iter != nullptr; ++iter) {
|
||||||
|
ss << std::string_view(*iter) << ", ";
|
||||||
|
}
|
||||||
|
return GenError({FS(_F("Could not start {1}: {2} (exit code: {3})") % ss.str() % err->message % err->code)});
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Xournal++
|
||||||
|
*
|
||||||
|
* Latex file generator
|
||||||
|
*
|
||||||
|
* @author Xournal++ Team
|
||||||
|
* https://github.com/xournalpp/xournalpp
|
||||||
|
*
|
||||||
|
* @license GNU GPLv2 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
#include <variant> // for variant
|
||||||
|
|
||||||
|
#include <gio/gio.h> // for GSubprocess
|
||||||
|
|
||||||
|
#include "util/Color.h" // for Color
|
||||||
|
|
||||||
|
#include "filesystem.h" // for path
|
||||||
|
|
||||||
|
class LatexSettings;
|
||||||
|
|
||||||
|
class LatexGenerator {
|
||||||
|
public:
|
||||||
|
LatexGenerator(const LatexSettings& settings);
|
||||||
|
LatexGenerator(const LatexGenerator&) = delete;
|
||||||
|
LatexGenerator& operator=(const LatexGenerator&) = delete;
|
||||||
|
LatexGenerator(const LatexGenerator&&) = delete;
|
||||||
|
LatexGenerator&& operator=(const LatexGenerator&&) = delete;
|
||||||
|
virtual ~LatexGenerator() = default;
|
||||||
|
|
||||||
|
struct GenError {
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
using Result = std::variant<GSubprocess*, GenError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the LaTeX command asynchronously to generate a preview for the given
|
||||||
|
* LaTeX file. The contents of the LaTeX file will be written to "tex.tex"
|
||||||
|
* in the given directory.
|
||||||
|
* The resultant process will have its standard error and (original) standard output
|
||||||
|
* combined into a single standard out stream.
|
||||||
|
*/
|
||||||
|
Result asyncRun(const fs::path& texDir, const std::string& texFileContents);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate the LaTeX template.
|
||||||
|
*/
|
||||||
|
static std::string templateSub(const std::string& input, const std::string& templ, Color textColor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const LatexSettings& settings;
|
||||||
|
};
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
#include "LayerController.h"
|
||||||
|
|
||||||
|
#include <memory> // for __shared_ptr_access, make...
|
||||||
|
#include <mutex> // for unique_lock
|
||||||
|
#include <utility> // for move
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "control/Control.h" // for Control
|
||||||
|
#include "control/actions/ActionDatabase.h" // for ActionDatabase
|
||||||
|
#include "gui/MainWindow.h" // for MainWindow
|
||||||
|
#include "gui/XournalView.h" // for XournalView
|
||||||
|
#include "model/Document.h" // for Document
|
||||||
|
#include "model/XojPage.h" // for XojPage
|
||||||
|
#include "undo/InsertLayerUndoAction.h" // for InsertLayerUndoAction
|
||||||
|
#include "undo/MergeLayerDownUndoAction.h" // for MergeLayerDownUndoAction
|
||||||
|
#include "undo/MoveLayerUndoAction.h" // for MoveLayerUndoAction
|
||||||
|
#include "undo/RemoveLayerUndoAction.h" // for RemoveLayerUndoAction
|
||||||
|
#include "undo/UndoAction.h" // for UndoActionPtr, UndoAction
|
||||||
|
#include "undo/UndoRedoHandler.h" // for UndoRedoHandler
|
||||||
|
#include "util/Util.h" // for npos
|
||||||
|
#include "util/i18n.h" // for FS, _F
|
||||||
|
|
||||||
|
#include "LayerCtrlListener.h" // for LayerCtrlListener
|
||||||
|
|
||||||
|
LayerController::LayerController(Control* control): control(control), selectedPage(npos) {}
|
||||||
|
|
||||||
|
void LayerController::documentChanged(DocumentChangeType type) {
|
||||||
|
if (type == DOCUMENT_CHANGE_CLEARED || type == DOCUMENT_CHANGE_COMPLETE) {
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::pageSelected(size_t page) {
|
||||||
|
if (selectedPage == page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedPage = page;
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::insertLayer(PageRef page, Layer* layer, Layer::Index layerPos) {
|
||||||
|
xoj_assert(layer);
|
||||||
|
bool empty = layer->getElements().empty();
|
||||||
|
control->getDocument()->lock();
|
||||||
|
page->insertLayer(layer, layerPos);
|
||||||
|
auto id = empty ? npos : control->getDocument()->indexOf(page);
|
||||||
|
control->getDocument()->unlock();
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
if (!empty) {
|
||||||
|
// Rerender the page
|
||||||
|
control->getWindow()->getXournal()->layerChanged(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::removeLayer(PageRef page, Layer* layer) {
|
||||||
|
control->getDocument()->lock();
|
||||||
|
bool empty = layer->getElements().empty();
|
||||||
|
page->removeLayer(layer);
|
||||||
|
auto id = empty ? npos : control->getDocument()->indexOf(page);
|
||||||
|
control->getDocument()->unlock();
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
if (!empty) {
|
||||||
|
// Rerender the page
|
||||||
|
control->getWindow()->getXournal()->layerChanged(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::moveLayer(PageRef page, Layer* layer, Layer::Index newPos) {
|
||||||
|
control->getDocument()->lock();
|
||||||
|
page->removeLayer(layer);
|
||||||
|
page->insertLayer(layer, newPos);
|
||||||
|
auto id = control->getDocument()->indexOf(page);
|
||||||
|
control->getDocument()->unlock();
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
// Rerender the page
|
||||||
|
control->getWindow()->getXournal()->layerChanged(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::mergeLayers(PageRef page, Layer* bottomLayer, Layer* topLayer) {
|
||||||
|
control->getDocument()->lock();
|
||||||
|
page->removeLayer(topLayer);
|
||||||
|
|
||||||
|
for (auto&& elem: topLayer->clearNoFree()) {
|
||||||
|
bottomLayer->addElement(std::move(elem));
|
||||||
|
}
|
||||||
|
auto id = control->getDocument()->indexOf(page);
|
||||||
|
control->getDocument()->unlock();
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
control->getWindow()->getXournal()->layerChanged(id); // Rerender the page
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::moveElementsFromLayerToFreeLayer(PageRef page, Layer* layer,
|
||||||
|
const std::vector<const Element*>& elts, Layer* freeLayer,
|
||||||
|
Layer::Index newLayerId) {
|
||||||
|
control->getDocument()->lock();
|
||||||
|
|
||||||
|
for (const Element* elem: elts) {
|
||||||
|
freeLayer->addElement(layer->removeElement(elem).e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the upper layer back at its old pos
|
||||||
|
page->insertLayer(freeLayer, newLayerId);
|
||||||
|
auto id = control->getDocument()->indexOf(page);
|
||||||
|
control->getDocument()->unlock();
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
control->getWindow()->getXournal()->layerChanged(id); // Rerender the page
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void LayerController::addListener(LayerCtrlListener* listener) { this->listener.push_back(listener); }
|
||||||
|
|
||||||
|
void LayerController::removeListener(LayerCtrlListener* listener) { this->listener.remove(listener); }
|
||||||
|
|
||||||
|
void LayerController::updateActions() {
|
||||||
|
auto layer = getCurrentLayerId();
|
||||||
|
auto maxLayer = getLayerCount();
|
||||||
|
|
||||||
|
auto* actionDB = control->getActionDatabase();
|
||||||
|
|
||||||
|
actionDB->enableAction(Action::LAYER_DELETE, layer > 0);
|
||||||
|
actionDB->enableAction(Action::LAYER_MERGE_DOWN, layer > 1);
|
||||||
|
actionDB->enableAction(Action::LAYER_MOVE_UP, layer < maxLayer);
|
||||||
|
actionDB->enableAction(Action::LAYER_MOVE_DOWN, layer > 1);
|
||||||
|
actionDB->enableAction(Action::MOVE_SELECTION_LAYER_UP, layer < maxLayer);
|
||||||
|
actionDB->enableAction(Action::MOVE_SELECTION_LAYER_DOWN, layer > 1);
|
||||||
|
actionDB->enableAction(Action::LAYER_GOTO_NEXT, layer < maxLayer);
|
||||||
|
actionDB->enableAction(Action::LAYER_GOTO_PREVIOUS, layer > 0);
|
||||||
|
actionDB->enableAction(Action::LAYER_GOTO_TOP, layer < maxLayer);
|
||||||
|
|
||||||
|
actionDB->setActionState(Action::LAYER_ACTIVE, layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::fireRebuildLayerMenu() {
|
||||||
|
for (LayerCtrlListener* l: this->listener) { l->rebuildLayerMenu(); }
|
||||||
|
updateActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::fireLayerVisibilityChanged() {
|
||||||
|
for (LayerCtrlListener* l: this->listener) { l->layerVisibilityChanged(); }
|
||||||
|
|
||||||
|
// Rerenders the page - Todo: make this another listener
|
||||||
|
control->getWindow()->getXournal()->layerChanged(selectedPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::fireSelectedLayerChanged() {
|
||||||
|
for (LayerCtrlListener* l: this->listener) {
|
||||||
|
l->updateSelectedLayer();
|
||||||
|
}
|
||||||
|
updateActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show all layer on the current page
|
||||||
|
*/
|
||||||
|
void LayerController::showAllLayer() { showOrHideAllLayer(true); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide all layer on the current page
|
||||||
|
*/
|
||||||
|
void LayerController::hideAllLayer() { showOrHideAllLayer(false); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show / Hide all layer on the current page
|
||||||
|
*/
|
||||||
|
void LayerController::showOrHideAllLayer(bool show) {
|
||||||
|
auto lock = std::unique_lock(*control->getDocument());
|
||||||
|
PageRef page = getCurrentPage();
|
||||||
|
for (Layer::Index i = 1; i <= page->getLayerCount(); i++) { page->setLayerVisible(i, show); }
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
fireLayerVisibilityChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::addNewLayer(bool belowCurrentLayer) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto lock = std::unique_lock(*control->getDocument());
|
||||||
|
PageRef p = getCurrentPage();
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* l = new Layer();
|
||||||
|
xoj_assert(p->getSelectedLayerId() > 0);
|
||||||
|
auto layerPos = belowCurrentLayer ? p->getSelectedLayerId() - 1 : p->getSelectedLayerId();
|
||||||
|
p->insertLayer(l, layerPos);
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::make_unique<InsertLayerUndoAction>(this, p, l, layerPos));
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
// Repaint is not needed here - the new layer is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::deleteCurrentLayer() {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto lock = std::unique_lock(*control->getDocument());
|
||||||
|
PageRef p = getCurrentPage();
|
||||||
|
auto pId = selectedPage;
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lId = p->getSelectedLayerId();
|
||||||
|
if (lId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Layer* l = p->getSelectedLayer();
|
||||||
|
|
||||||
|
p->removeLayer(l);
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
MainWindow* win = control->getWindow();
|
||||||
|
if (win) {
|
||||||
|
win->getXournal()->layerChanged(pId);
|
||||||
|
}
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::make_unique<RemoveLayerUndoAction>(this, p, l, lId - 1));
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::moveCurrentLayer(bool up) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto lock = std::unique_lock(*control->getDocument());
|
||||||
|
PageRef p = getCurrentPage();
|
||||||
|
auto pId = selectedPage;
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lId = p->getSelectedLayerId();
|
||||||
|
Layer* currentLayer = p->getSelectedLayer();
|
||||||
|
if (lId == 0) {
|
||||||
|
// Background cannot be moved
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lId == 1 && !up) {
|
||||||
|
// bottom layer cannot be moved down
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lId == p->getLayerCount() && up) {
|
||||||
|
// top layer cannot be moved up
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p->removeLayer(currentLayer);
|
||||||
|
|
||||||
|
// Layer IDs are a bit strange, because background is 0
|
||||||
|
// so the first layer is 1, technical the first layer is still
|
||||||
|
// index 0 in the vector... confusing...
|
||||||
|
auto newIndex = up ? lId : lId - 2;
|
||||||
|
p->insertLayer(currentLayer, newIndex);
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
MainWindow* win = control->getWindow();
|
||||||
|
if (win) {
|
||||||
|
win->getXournal()->layerChanged(pId);
|
||||||
|
}
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(
|
||||||
|
std::make_unique<MoveLayerUndoAction>(this, p, currentLayer, lId - 1, newIndex));
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::mergeCurrentLayerDown() {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto lock = std::shared_lock(*control->getDocument());
|
||||||
|
PageRef page = getCurrentPage();
|
||||||
|
auto pageID = selectedPage;
|
||||||
|
if (page == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* layerID value:
|
||||||
|
* ...
|
||||||
|
* 2: layer 2
|
||||||
|
* 1: layer 1
|
||||||
|
* 0: background
|
||||||
|
*/
|
||||||
|
const auto layerID = page->getSelectedLayerId();
|
||||||
|
Layer* currentLayer = page->getSelectedLayer();
|
||||||
|
if (layerID < 2) {
|
||||||
|
/*
|
||||||
|
* lowest (non-background) layer cannot be merged into background
|
||||||
|
* and the background itself obviously also cannot be merged down
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We know this cannot be the background (or even an underflow) because
|
||||||
|
* we checked for !(layerID < 2) before.
|
||||||
|
*/
|
||||||
|
const auto layerBelowID = layerID - 1;
|
||||||
|
/*
|
||||||
|
* Layer indices in the vector are off by one from the layer IDs because
|
||||||
|
* the background is not in the vector, so layer 1 has index 0 and so on.
|
||||||
|
*/
|
||||||
|
const Layer::Index layerBelowIndex = layerBelowID - 1;
|
||||||
|
Layer* layerBelow = page->getLayers()[layerBelowIndex];
|
||||||
|
|
||||||
|
UndoActionPtr undo_redo_action =
|
||||||
|
std::make_unique<MergeLayerDownUndoAction>(this, page, currentLayer, layerID - 1, layerBelow, pageID);
|
||||||
|
lock.unlock();
|
||||||
|
// The mutex is locked in RW in redo()
|
||||||
|
undo_redo_action->redo(this->control);
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::move(undo_redo_action));
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::copyCurrentLayer() {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
|
||||||
|
auto lock = std::unique_lock(*control->getDocument());
|
||||||
|
PageRef p = getCurrentPage();
|
||||||
|
auto pId = selectedPage;
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lId = p->getSelectedLayerId();
|
||||||
|
if (lId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Layer* l = p->getSelectedLayer();
|
||||||
|
Layer* cloned = l->clone();
|
||||||
|
|
||||||
|
p->insertLayer(cloned, lId);
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
MainWindow* win = control->getWindow();
|
||||||
|
if (win) {
|
||||||
|
win->getXournal()->layerChanged(pId);
|
||||||
|
}
|
||||||
|
|
||||||
|
control->getUndoRedoHandler()->addUndoAction(std::make_unique<InsertLayerUndoAction>(this, p, cloned, lId));
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LayerController::getCurrentPage() const -> PageRef { return control->getDocument()->getPage(selectedPage); }
|
||||||
|
|
||||||
|
auto LayerController::getCurrentPageId() const -> size_t { return selectedPage; }
|
||||||
|
|
||||||
|
void LayerController::setLayerVisible(Layer::Index layerId, bool visible) {
|
||||||
|
getCurrentPage()->setLayerVisible(layerId, visible);
|
||||||
|
fireLayerVisibilityChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a layer
|
||||||
|
*
|
||||||
|
* @param hideShow Auto hide / show other layers,
|
||||||
|
* as it was before the advance layer menu
|
||||||
|
* @param clearSelection Clear / keep current selection before switching layers.
|
||||||
|
*/
|
||||||
|
void LayerController::switchToLay(Layer::Index layerId, bool hideShow, bool clearSelection) {
|
||||||
|
if (clearSelection) {
|
||||||
|
control->clearSelectionEndText();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lock = std::shared_lock(*control->getDocument());
|
||||||
|
PageRef p = getCurrentPage();
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p->setSelectedLayerId(layerId);
|
||||||
|
lock.unlock();
|
||||||
|
fireSelectedLayerChanged();
|
||||||
|
|
||||||
|
if (hideShow) {
|
||||||
|
for (Layer::Index i = 1; i <= p->getLayerCount(); i++) { p->setLayerVisible(i, i <= layerId); }
|
||||||
|
|
||||||
|
fireLayerVisibilityChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Layer count of the current page
|
||||||
|
*/
|
||||||
|
auto LayerController::getLayerCount() const -> Layer::Index {
|
||||||
|
PageRef page = getCurrentPage();
|
||||||
|
if (!page) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page->getLayerCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Current layer ID
|
||||||
|
*/
|
||||||
|
auto LayerController::getCurrentLayerId() const -> Layer::Index {
|
||||||
|
PageRef page = getCurrentPage();
|
||||||
|
if (!page) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return page->getSelectedLayerId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure the document's mutex is locked when calling this
|
||||||
|
static auto getLayerNameOnPage(const PageRef& page, Layer::Index id) -> std::string {
|
||||||
|
if (page == nullptr || id > page->getLayerCount()) {
|
||||||
|
return "Unknown layer name";
|
||||||
|
} else if (id == 0) { // If is background
|
||||||
|
return page->getBackgroundName();
|
||||||
|
} else if (auto layer = page->getLayersView()[id - 1]; layer->hasName()) {
|
||||||
|
return layer->getName();
|
||||||
|
} else {
|
||||||
|
return FS(_F("Layer {1}") % static_cast<long>(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LayerController::getCurrentLayerName() const -> std::string {
|
||||||
|
auto lock = std::shared_lock(*control->getDocument());
|
||||||
|
return getLayerNameOnPage(getCurrentPage(), getCurrentLayerId());
|
||||||
|
}
|
||||||
|
auto LayerController::getLayerNameById(Layer::Index id) const -> std::string {
|
||||||
|
auto lock = std::shared_lock(*control->getDocument());
|
||||||
|
return getLayerNameOnPage(getCurrentPage(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayerController::setCurrentLayerName(const std::string& newName) {
|
||||||
|
auto lock = std::unique_lock(*control->getDocument());
|
||||||
|
PageRef page = getCurrentPage();
|
||||||
|
|
||||||
|
if (page == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCurrentLayerId() == 0) { // Background
|
||||||
|
page->setBackgroundName(newName);
|
||||||
|
} else { // Any other layer
|
||||||
|
page->getSelectedLayer()->setName(newName);
|
||||||
|
}
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
fireRebuildLayerMenu();
|
||||||
|
}
|
||||||
Loaded 100 of 830 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user