🚚 移动源码

This commit is contained in:
shufeng2012 committed 2026-08-15 19:40:17 +08:00
1 parent bfeaad1899
commit 1ae3987fab
830 files changed
+95931

No files matched your search

+174
View File
@@ -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)
+69
View File
@@ -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
+50
View File
@@ -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)
+40
View File
@@ -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@";
+22
View File
@@ -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@"
+57
View File
@@ -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