mirror of
https://gitcode.com/shufeng2012/ohos-xournalpp.git
synced 2026-11-07 06:41: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