From d0be4648bca214082eecf35592d2017117667d93 Mon Sep 17 00:00:00 2001 From: satoqz Date: Thu, 27 Jul 2023 21:53:36 -0700 Subject: [PATCH 01/26] Info.plist: Add CFBundleName --- resources/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/Info.plist b/resources/Info.plist index cb8abaded..b95b24a16 100644 --- a/resources/Info.plist +++ b/resources/Info.plist @@ -2,6 +2,8 @@ + CFBundleName + sioyek CFBundleExecutable @EXECUTABLE@ CFBundleIconFile From 4822b65c44f87784abdebebbc5144989d7c6a4f1 Mon Sep 17 00:00:00 2001 From: hrdl <31923882+hrdl-github@users.noreply.github.com> Date: Fri, 6 Oct 2023 19:06:12 +0200 Subject: [PATCH 02/26] Update maintainer for AUR sioyek-git --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dbdaff5a9..27e49207c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Distro | Link | Maintainer Flathub | [sioyek](https://flathub.org/apps/details/com.github.ahrm.sioyek) | [@nbenitez](https://flathub.org/apps/details/com.github.ahrm.sioyek) Alpine | [sioyek](https://pkgs.alpinelinux.org/packages?name=sioyek) | [@jirutka](https://github.com/jirutka) Arch | [AUR sioyek](https://aur.archlinux.org/packages/sioyek) | [@goggle](https://github.com/goggle) -Arch | [AUR Sioyek-git](https://aur.archlinux.org/packages/sioyek-git/) | [@randomn4me](https://github.com/randomn4me) +Arch | [AUR sioyek-git](https://aur.archlinux.org/packages/sioyek-git/) | [@hrdl-github](https://github.com/hrdl-github) Arch | [AUR sioyek-appimage](https://aur.archlinux.org/packages/sioyek-appimage/) | [@DhruvaSambrani](https://github.com/DhruvaSambrani) Debian | [sioyek](https://packages.debian.org/sioyek) | [@viccie30](https://github.com/viccie30) NixOS | [sioyek](https://search.nixos.org/packages?channel=unstable&show=sioyek&from=0&size=50&sort=relevance&type=packages&query=sioyek) | [@podocarp](https://github.com/podocarp) From 929345f7e42892fa4a4e3d288a35c4b5b57607ea Mon Sep 17 00:00:00 2001 From: hrdl <31923882+hrdl-github@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:50:38 +0200 Subject: [PATCH 03/26] Don't allow nan values in toc nodes. Fix https://github.com/ahrm/sioyek/issues/539#issuecomment-1759302654 --- pdf_viewer/document.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pdf_viewer/document.cpp b/pdf_viewer/document.cpp index 34e5a027c..fe532ab9a 100644 --- a/pdf_viewer/document.cpp +++ b/pdf_viewer/document.cpp @@ -803,20 +803,20 @@ void Document::convert_toc_tree(fz_outline* root, std::vector& output) break; } - TocNode* current_node = new TocNode; - current_node->title = utf8_decode(root->title); - current_node->x = root->x; - current_node->y = root->y; - if (root->page.page == -1) { - float xp, yp; - fz_location loc = fz_resolve_link(context, doc, root->uri, &xp, &yp); - int chapter_page = accum_chapter_pages[loc.chapter]; - current_node->page = chapter_page + loc.page; - } - else { - current_node->page = root->page.page; - } - convert_toc_tree(root->down, current_node->children); + TocNode* current_node = new TocNode; + current_node->title = utf8_decode(root->title); + current_node->x = std::isnan(root->x) ? 0.0 : root->x; + current_node->y = std::isnan(root->y) ? 0.0 : root->y; + if (root->page.page == -1) { + float xp, yp; + fz_location loc = fz_resolve_link(context, doc, root->uri, &xp, &yp); + int chapter_page = accum_chapter_pages[loc.chapter]; + current_node->page = chapter_page + loc.page; + } + else { + current_node->page = root->page.page; + } + convert_toc_tree(root->down, current_node->children); output.push_back(current_node); From 4322ff2906f97435d05c6ea1372273103458fc92 Mon Sep 17 00:00:00 2001 From: jinjiaodawang Date: Fri, 3 Nov 2023 08:22:12 +0800 Subject: [PATCH 04/26] Add path into build_mac.sh --- build_mac.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/build_mac.sh b/build_mac.sh index 38b7e7af2..da90d7d31 100644 --- a/build_mac.sh +++ b/build_mac.sh @@ -34,5 +34,15 @@ cp pdf_viewer/keys.config build/sioyek.app/Contents/MacOS/keys.config cp pdf_viewer/keys_user.config build/sioyek.app/Contents/MacOS/keys_user.config cp tutorial.pdf build/sioyek.app/Contents/MacOS/tutorial.pdf +# Capture the current PATH +CURRENT_PATH=$(echo $PATH) + +# Define the path to the Info.plist file inside the app bundle +INFO_PLIST="resources/Info.plist" + +# Add LSEnvironment key with PATH to Info.plist +/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" || echo "LSEnvironment already exists" +/usr/libexec/PlistBuddy -c "Add :LSEnvironment:PATH string $CURRENT_PATH" "$INFO_PLIST" || /usr/libexec/PlistBuddy -c "Set :LSEnvironment:PATH $CURRENT_PATH" "$INFO_PLIST" + macdeployqt build/sioyek.app -dmg zip -r sioyek-release-mac.zip build/sioyek.dmg From 084875476791dd11e1899181e1cf74c9781be4e9 Mon Sep 17 00:00:00 2001 From: smorodina <81278460+smooroodina@users.noreply.github.com> Date: Mon, 20 Nov 2023 04:08:15 +0900 Subject: [PATCH 05/26] Change Unix commands to Windows commands --- build_windows.bat | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build_windows.bat b/build_windows.bat index 6279f40c0..69ba98cf0 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -14,21 +14,21 @@ if %1 == portable ( ) msbuild -maxcpucount sioyek.vcxproj /property:Configuration=Release -rm -r sioyek-release-windows 2> NUL +rmdir /S sioyek-release-windows mkdir sioyek-release-windows -cp release\sioyek.exe sioyek-release-windows\sioyek.exe -cp pdf_viewer\keys.config sioyek-release-windows\keys.config -cp pdf_viewer\prefs.config sioyek-release-windows\prefs.config -cp -r pdf_viewer\shaders sioyek-release-windows\shaders -cp tutorial.pdf sioyek-release-windows\tutorial.pdf +copy release\sioyek.exe sioyek-release-windows\sioyek.exe +copy pdf_viewer\keys.config sioyek-release-windows\keys.config +copy pdf_viewer\prefs.config sioyek-release-windows\prefs.config +xcopy /E /I pdf_viewer\shaders sioyek-release-windows\shaders\ +copy tutorial.pdf sioyek-release-windows\tutorial.pdf windeployqt sioyek-release-windows\sioyek.exe -cp windows_runtime\vcruntime140_1.dll sioyek-release-windows\vcruntime140_1.dll -cp windows_runtime\libssl-1_1-x64.dll sioyek-release-windows\libssl-1_1-x64.dll -cp windows_runtime\libcrypto-1_1-x64.dll sioyek-release-windows\libcrypto-1_1-x64.dll +copy windows_runtime\vcruntime140_1.dll sioyek-release-windows\vcruntime140_1.dll +copy windows_runtime\libssl-1_1-x64.dll sioyek-release-windows\libssl-1_1-x64.dll +copy windows_runtime\libcrypto-1_1-x64.dll sioyek-release-windows\libcrypto-1_1-x64.dll if %1 == portable ( - cp pdf_viewer\keys_user.config sioyek-release-windows\keys_user.config - cp pdf_viewer\prefs_user.config sioyek-release-windows\prefs_user.config + copy pdf_viewer\keys_user.config sioyek-release-windows\keys_user.config + copy pdf_viewer\prefs_user.config sioyek-release-windows\prefs_user.config 7z a sioyek-release-windows-portable.zip sioyek-release-windows ) else ( From 51a641b215d0d44ba136d43f3e5d2661cd00fcd4 Mon Sep 17 00:00:00 2001 From: Feraidoon Mehri <36224762+NightMachinery@users.noreply.github.com> Date: Sat, 6 Jan 2024 06:59:46 +0330 Subject: [PATCH 06/26] README.md: Updated macOS build instructions The previous instructions did not quite work. --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 27e49207c..cc2a07e37 100644 --- a/README.md +++ b/README.md @@ -151,13 +151,27 @@ build_windows.bat ``` ### Mac -1. Install Xcode and Qt 5. -2. Clone the repository and build: -``` +1. Install Xcode. +2. Clone the repository and build: (The code below is in Zsh, which is the default shell on macOS.) +```zsh +( +setopt PIPE_FAIL PRINT_EXIT_VALUE ERR_RETURN SOURCE_TRACE XTRACE + git clone --recursive https://github.com/ahrm/sioyek cd sioyek chmod +x build_mac.sh -./build_mac.sh + +brew install 'qt@5' freeglut mesa harfbuzz + +export PATH="/opt/homebrew/opt/qt@5/bin:$PATH" +#: The above is needed to make =qmake= from =qt= be found. +#: Find the path using =brew info 'qt@5'=. + +MAKE_PARALLEL=8 ./build_mac.sh + +mv build/sioyek.app /Applications/ +sudo codesign --force --sign - --deep /Applications/sioyek.app +) ``` ## Donation From 5fd403e74794350e5af49e64fff6eafea5ec40ea Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Fri, 12 Jan 2024 15:49:46 +0330 Subject: [PATCH 07/26] treating backspace as delete --- pdf_viewer/ui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf_viewer/ui.cpp b/pdf_viewer/ui.cpp index 1f4668a12..029be9e2f 100644 --- a/pdf_viewer/ui.cpp +++ b/pdf_viewer/ui.cpp @@ -1676,7 +1676,7 @@ void BaseSelectorWidget::handle_edit() { #ifndef SIOYEK_QT6 void BaseSelectorWidget::keyReleaseEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Delete) { + if (event->key() == Qt::Key_Delete or event->key() == Qt::Key_Backspace) { handle_delete(); } QWidget::keyReleaseEvent(event); From 212b02a241f242a552fc1d3a7538da5504490621 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Fri, 12 Jan 2024 16:07:28 +0330 Subject: [PATCH 08/26] added shouldTriggerDelete --- pdf_viewer/ui.cpp | 2 +- pdf_viewer/utils.cpp | 22 ++++++++++++++++++++++ pdf_viewer/utils.h | 4 ++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pdf_viewer/ui.cpp b/pdf_viewer/ui.cpp index 029be9e2f..9abd939d3 100644 --- a/pdf_viewer/ui.cpp +++ b/pdf_viewer/ui.cpp @@ -1676,7 +1676,7 @@ void BaseSelectorWidget::handle_edit() { #ifndef SIOYEK_QT6 void BaseSelectorWidget::keyReleaseEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Delete or event->key() == Qt::Key_Backspace) { + if (shouldTriggerDelete(event)) { handle_delete(); } QWidget::keyReleaseEvent(event); diff --git a/pdf_viewer/utils.cpp b/pdf_viewer/utils.cpp index 918aa6dc5..4472afb8a 100644 --- a/pdf_viewer/utils.cpp +++ b/pdf_viewer/utils.cpp @@ -35,6 +35,8 @@ #include #include #endif +#include +#include #include @@ -3886,3 +3888,23 @@ bool is_in(char c, std::vector candidates){ return std::find(candidates.begin(), candidates.end(), c) != candidates.end(); } +bool shouldTriggerDelete(QKeyEvent *key_event) { + if (!key_event) { + return false; + } + + // Check for the Delete key + if (key_event->key() == Qt::Key_Delete) { + return true; + } + + // On macOS, treat the Backspace key as Delete as well +#ifdef Q_OS_MAC + if (key_event->key() == Qt::Key_Backspace) { + return true; + } +#endif + + // For other platforms, Backspace does not trigger delete + return false; +} diff --git a/pdf_viewer/utils.h b/pdf_viewer/utils.h index 307fdc247..788350534 100644 --- a/pdf_viewer/utils.h +++ b/pdf_viewer/utils.h @@ -14,6 +14,8 @@ #include #include +#include + #include #include #include @@ -439,3 +441,5 @@ std::vector quads_from_rects(const std::vector& rects) { bool is_bright(float color[3]); bool is_abbreviation(const std::wstring& txt); bool is_in(char c, std::vector candidates); + +bool shouldTriggerDelete(QKeyEvent *key_event); From 5f8f68462fc73eadfa331576057a85528db8f5c6 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Fri, 12 Jan 2024 19:09:30 +0330 Subject: [PATCH 09/26] renamed shouldTriggerDelete to should_trigger_delete --- pdf_viewer/ui.cpp | 2 +- pdf_viewer/utils.cpp | 2 +- pdf_viewer/utils.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pdf_viewer/ui.cpp b/pdf_viewer/ui.cpp index 9abd939d3..438711623 100644 --- a/pdf_viewer/ui.cpp +++ b/pdf_viewer/ui.cpp @@ -1676,7 +1676,7 @@ void BaseSelectorWidget::handle_edit() { #ifndef SIOYEK_QT6 void BaseSelectorWidget::keyReleaseEvent(QKeyEvent* event) { - if (shouldTriggerDelete(event)) { + if (should_trigger_delete(event)) { handle_delete(); } QWidget::keyReleaseEvent(event); diff --git a/pdf_viewer/utils.cpp b/pdf_viewer/utils.cpp index 4472afb8a..f1a0648cf 100644 --- a/pdf_viewer/utils.cpp +++ b/pdf_viewer/utils.cpp @@ -3888,7 +3888,7 @@ bool is_in(char c, std::vector candidates){ return std::find(candidates.begin(), candidates.end(), c) != candidates.end(); } -bool shouldTriggerDelete(QKeyEvent *key_event) { +bool should_trigger_delete(QKeyEvent *key_event) { if (!key_event) { return false; } diff --git a/pdf_viewer/utils.h b/pdf_viewer/utils.h index 788350534..1b8036bbe 100644 --- a/pdf_viewer/utils.h +++ b/pdf_viewer/utils.h @@ -442,4 +442,4 @@ bool is_bright(float color[3]); bool is_abbreviation(const std::wstring& txt); bool is_in(char c, std::vector candidates); -bool shouldTriggerDelete(QKeyEvent *key_event); +bool should_trigger_delete(QKeyEvent *key_event); From 79597436a6c4176af9de63d806bef2b2e22356b4 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Fri, 12 Jan 2024 18:32:31 +0330 Subject: [PATCH 10/26] updated build instructions for macOS, added missing import to pdf_viewer/coordinates.h, fuzzy matching uses smart case --- .gitignore | 2 + build_mac.sh | 31 ++++++++----- pdf_viewer/coordinates.h | 2 + pdf_viewer/mysortfilterproxymodel.cpp | 11 +++-- pdf_viewer/ui.cpp | 2 +- pdf_viewer/ui.h | 2 +- pdf_viewer/utils.cpp | 67 ++++++++++++++++++++++++--- pdf_viewer/utils.h | 16 ++++++- 8 files changed, 107 insertions(+), 26 deletions(-) mode change 100644 => 100755 build_mac.sh diff --git a/.gitignore b/.gitignore index 3cc46948b..d62829f01 100644 --- a/.gitignore +++ b/.gitignore @@ -372,3 +372,5 @@ tutorial/*.bbl tutorial/*.blg *.o.d compile_commands.json + +qrc_resources.cpp diff --git a/build_mac.sh b/build_mac.sh old mode 100644 new mode 100755 index da90d7d31..0e5972819 --- a/build_mac.sh +++ b/build_mac.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash -set -e -# prerequisite: brew install qt@5 freeglut mesa harfbuzz +set -exo pipefail +# prerequisite: brew install qt qt@5 freeglut mesa harfbuzz hiredis +export INCLUDE_PATH=/opt/homebrew/include +export LIBRARY_PATH=/opt/homebrew/lib + +incremental_p="${SIOYEK_BUILD_INCREMENTAL_P}" +# Using `SIOYEK_BUILD_INCREMENTAL_P=y` will cause the build script to become optimized for the local development builds and not publishing the app. #sys_glut_clfags=`pkg-config --cflags glut gl` #sys_glut_libs=`pkg-config --libs glut gl` @@ -12,7 +17,7 @@ echo "MAKE_PARALLEL set to $MAKE_PARALLEL" cd mupdf #make USE_SYSTEM_HARFBUZZ=yes USE_SYSTEM_GLUT=yes SYS_GLUT_CFLAGS="${sys_glut_clfags}" SYS_GLUT_LIBS="${sys_glut_libs}" SYS_HARFBUZZ_CFLAGS="${sys_harfbuzz_clfags}" SYS_HARFBUZZ_LIBS="${sys_harfbuzz_libs}" -j 4 -make +make XLIBS="-L${LIBRARY_PATH}" cd .. if [[ $1 == portable ]]; then @@ -34,15 +39,17 @@ cp pdf_viewer/keys.config build/sioyek.app/Contents/MacOS/keys.config cp pdf_viewer/keys_user.config build/sioyek.app/Contents/MacOS/keys_user.config cp tutorial.pdf build/sioyek.app/Contents/MacOS/tutorial.pdf -# Capture the current PATH -CURRENT_PATH=$(echo $PATH) +if test -z "${incremental_p}" ; then + # Capture the current PATH + CURRENT_PATH=$(echo $PATH) -# Define the path to the Info.plist file inside the app bundle -INFO_PLIST="resources/Info.plist" + # Define the path to the Info.plist file inside the app bundle + INFO_PLIST="resources/Info.plist" -# Add LSEnvironment key with PATH to Info.plist -/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" || echo "LSEnvironment already exists" -/usr/libexec/PlistBuddy -c "Add :LSEnvironment:PATH string $CURRENT_PATH" "$INFO_PLIST" || /usr/libexec/PlistBuddy -c "Set :LSEnvironment:PATH $CURRENT_PATH" "$INFO_PLIST" + # Add LSEnvironment key with PATH to Info.plist + /usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" || echo "LSEnvironment already exists" + /usr/libexec/PlistBuddy -c "Add :LSEnvironment:PATH string $CURRENT_PATH" "$INFO_PLIST" || /usr/libexec/PlistBuddy -c "Set :LSEnvironment:PATH $CURRENT_PATH" "$INFO_PLIST" -macdeployqt build/sioyek.app -dmg -zip -r sioyek-release-mac.zip build/sioyek.dmg + macdeployqt build/sioyek.app -dmg + zip -r sioyek-release-mac.zip build/sioyek.dmg +fi diff --git a/pdf_viewer/coordinates.h b/pdf_viewer/coordinates.h index d0a8546eb..a1d762ccb 100644 --- a/pdf_viewer/coordinates.h +++ b/pdf_viewer/coordinates.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include diff --git a/pdf_viewer/mysortfilterproxymodel.cpp b/pdf_viewer/mysortfilterproxymodel.cpp index 34373090d..0bc45c63c 100644 --- a/pdf_viewer/mysortfilterproxymodel.cpp +++ b/pdf_viewer/mysortfilterproxymodel.cpp @@ -1,3 +1,5 @@ +#include "utils.h" + #include "mysortfilterproxymodel.h" #include @@ -14,7 +16,7 @@ bool MySortFilterProxyModel::filter_accepts_row_column(int row, int col, const Q QString key = sourceModel()->data(source_index, filterRole()).toString(); std::wstring s1 = filterString.toStdWString(); std::wstring s2 = key.toStdWString(); - int score = static_cast(rapidfuzz::fuzz::partial_ratio(s1, s2)); + int score = calculate_partial_ratio(s1, s2); return score > 50; } @@ -84,8 +86,6 @@ bool MySortFilterProxyModel::lessThan(const QModelIndex& left, int right_index = right.row(); int left_score = scores[left_index]; int right_score = scores[right_index]; - //int left_score = static_cast(rapidfuzz::fuzz::partial_ratio(filterString.toStdWString(), leftData.toStdWString())); - //int right_score = static_cast(rapidfuzz::fuzz::partial_ratio(filterString.toStdWString(), rightData.toStdWString())); return left_score > right_score; } @@ -113,7 +113,7 @@ void MySortFilterProxyModel::update_scores() { if ((n_cols == 1) || (filter_column_index >= 0)) { for (int i = 0; i < n_rows; i++) { QString row_data = sourceModel()->data(sourceModel()->index(i, filter_column_index)).toString(); - int score = static_cast(rapidfuzz::fuzz::partial_ratio(filter_wstring, row_data.toStdWString())); + int score = calculate_partial_ratio(filter_wstring, row_data.toStdWString()); scores.push_back(score); } } @@ -123,7 +123,8 @@ void MySortFilterProxyModel::update_scores() { for (int col_index = 0; col_index < n_cols; col_index++) { QString rowcol_data = sourceModel()->data(sourceModel()->index(i, col_index)).toString(); - int col_score = static_cast(rapidfuzz::fuzz::partial_ratio(filter_wstring, rowcol_data.toStdWString())); + + int col_score = calculate_partial_ratio(filter_wstring, rowcol_data.toStdWString()); if (col_score > score) score = col_score; } diff --git a/pdf_viewer/ui.cpp b/pdf_viewer/ui.cpp index 438711623..5add024e2 100644 --- a/pdf_viewer/ui.cpp +++ b/pdf_viewer/ui.cpp @@ -1342,7 +1342,7 @@ bool CommandSelector::on_text_change(const QString& text) { std::string encoded = utf8_encode(string_elements.at(i).toStdWString()); int score = 0; if (is_fuzzy) { - score = static_cast(rapidfuzz::fuzz::partial_ratio(search_text_string, encoded)); + score = calculate_partial_ratio(search_text_string, encoded); } else { fts::fuzzy_match(search_text_string.c_str(), encoded.c_str(), score); diff --git a/pdf_viewer/ui.h b/pdf_viewer/ui.h index 3849aeb26..b4c55535f 100644 --- a/pdf_viewer/ui.h +++ b/pdf_viewer/ui.h @@ -663,7 +663,7 @@ class FileSelector : public BaseSelectorWidget { std::string encoded_file = utf8_encode(file.toStdWString()); int score = 0; if (is_fuzzy) { - score = static_cast(rapidfuzz::fuzz::partial_ratio(encoded_prefix, encoded_file)); + score = calculate_partial_ratio(encoded_prefix, encoded_file); } else { fts::fuzzy_match(encoded_prefix.c_str(), encoded_file.c_str(), score); diff --git a/pdf_viewer/utils.cpp b/pdf_viewer/utils.cpp index f1a0648cf..e5d81cda8 100644 --- a/pdf_viewer/utils.cpp +++ b/pdf_viewer/utils.cpp @@ -38,6 +38,13 @@ #include #include +#include "rapidfuzz_amalgamated.hpp" + +#include + +#include +#include + #include extern std::wstring LIBGEN_ADDRESS; @@ -72,14 +79,29 @@ extern bool VERBOSE; #endif -std::wstring to_lower(const std::wstring& inp) { - std::wstring res; - for (char c : inp) { - res.push_back(::tolower(c)); - } - return res; +template +std::basic_string to_lower(const std::basic_string& input) { + std::basic_string output = input; + std::locale loc; + std::transform(output.begin(), output.end(), output.begin(), + [&loc](CharT c) { return std::tolower(c, loc); }); + return output; } +// std::wstring to_lower(const std::wstring& input) { +// std::wstring output = input; +// std::transform(output.begin(), output.end(), output.begin(), +// [](wchar_t c) { return std::tolower(c, std::locale()); }); +// return output; +// } + +// std::string to_lower(const std::string& input) { +// std::string output = input; +// std::transform(output.begin(), output.end(), output.begin(), +// [](char c) { return std::tolower(c, std::locale()); }); +// return output; +// } + void get_flat_toc(const std::vector& roots, std::vector& output, std::vector& pages) { // Enumerate ToC nodes in DFS order @@ -3908,3 +3930,36 @@ bool should_trigger_delete(QKeyEvent *key_event) { // For other platforms, Backspace does not trigger delete return false; } + + +// Template function to check if a string is all lowercase +template +bool is_all_lower(const std::basic_string& input) { + std::locale loc; + return std::all_of(input.begin(), input.end(), [&loc](CharT c) { + return std::islower(c, loc) || !std::isalpha(c, loc); + }); +} + +// Template function for calculate_partial_ratio +template +int calculate_partial_ratio(const StringType& filterString, const StringType& key, bool smart_case_p) { + StringType s1 = filterString; + StringType s2 = key; + + // Convert strings to lowercase if smart_case_p is true and filterString is all lowercase + if (smart_case_p && is_all_lower(s1)) { + s1 = to_lower(s1); + s2 = to_lower(s2); + } + + // Calculate the partial ratio score + // Ensure that rapidfuzz::fuzz::partial_ratio can handle StringType + int score = static_cast(rapidfuzz::fuzz::partial_ratio(s1, s2)); + + return score; +} + +// Explicit template instantiation for std::wstring and std::string +template int calculate_partial_ratio(const std::string&, const std::string&, bool); +template int calculate_partial_ratio(const std::wstring&, const std::wstring&, bool); diff --git a/pdf_viewer/utils.h b/pdf_viewer/utils.h index 1b8036bbe..7e4d999ab 100644 --- a/pdf_viewer/utils.h +++ b/pdf_viewer/utils.h @@ -30,7 +30,20 @@ #define LOG(expr) if (VERBOSE) {(expr);}; -std::wstring to_lower(const std::wstring& inp); +// std::wstring to_lower(const std::wstring& inp); +template +std::basic_string to_lower(const std::basic_string& input); + +template +bool is_all_lower(const std::basic_string& input); + +template +int calculate_partial_ratio(const StringType& filterString, const StringType& key, bool smart_case_p = true); + +// Explicit template instantiation declarations for std::wstring and std::string +extern template int calculate_partial_ratio(const std::string&, const std::string&, bool); +extern template int calculate_partial_ratio(const std::wstring&, const std::wstring&, bool); + bool is_separator(fz_stext_char* last_char, fz_stext_char* current_char); void get_flat_toc(const std::vector& roots, std::vector& output, std::vector& pages); int mod(int a, int b); @@ -443,3 +456,4 @@ bool is_abbreviation(const std::wstring& txt); bool is_in(char c, std::vector candidates); bool should_trigger_delete(QKeyEvent *key_event); + From b1d1d47535bd779a650e3cdac96da4a0dffd3926 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sat, 13 Jan 2024 14:49:18 +0330 Subject: [PATCH 11/26] move_horizontal: added ignore_lock_p; shift+scrolling works even with horizontal lock --- pdf_viewer/main_widget.cpp | 8 ++++---- pdf_viewer/main_widget.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 3cdc38e93..9b3ae68a1 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -2928,11 +2928,11 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { float inverse_factor = INVERTED_HORIZONTAL_SCROLLING ? -1.0f : 1.0f; if (wevent->angleDelta().y() > 0) { - move_horizontal(-72.0f * horizontal_move_amount * num_repeats_f * inverse_factor); + move_horizontal(-72.0f * horizontal_move_amount * num_repeats_f * inverse_factor, false, true); return; } if (wevent->angleDelta().y() < 0) { - move_horizontal(72.0f * horizontal_move_amount * num_repeats_f * inverse_factor); + move_horizontal(72.0f * horizontal_move_amount * num_repeats_f * inverse_factor, false, true); return; } @@ -3639,8 +3639,8 @@ void MainWidget::zoom(WindowPos pos, float zoom_factor, bool zoom_in) { validate_render(); } -bool MainWidget::move_horizontal(float amount, bool force) { - if (!horizontal_scroll_locked) { +bool MainWidget::move_horizontal(float amount, bool force, bool ignore_lock_p) { + if (ignore_lock_p || !horizontal_scroll_locked) { bool ret = move_document(amount, 0, force); validate_render(); return ret; diff --git a/pdf_viewer/main_widget.h b/pdf_viewer/main_widget.h index 95afea39e..5d13c6feb 100644 --- a/pdf_viewer/main_widget.h +++ b/pdf_viewer/main_widget.h @@ -519,7 +519,7 @@ class MainWidget : public QQuickWidget { CommandManager* get_command_manager(); void move_vertical(float amount); - bool move_horizontal(float amount, bool force = false); + bool move_horizontal(float amount, bool force = false, bool ignore_lock_p = false); void get_window_params_for_one_window_mode(int* main_window_size, int* main_window_move); void get_window_params_for_two_window_mode(int* main_window_size, int* main_window_move, int* helper_window_size, int* helper_window_move); void apply_window_params_for_one_window_mode(bool force_resize = false); From 5cfd62a382a34dc96a8555c708f00851f806e71d Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sat, 13 Jan 2024 14:50:16 +0330 Subject: [PATCH 12/26] build_mac.sh: added SIOYEK_BUILD_INCREMENTAL_P (empty after rebase) --- build_mac.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_mac.sh b/build_mac.sh index 0e5972819..0864e9d3a 100755 --- a/build_mac.sh +++ b/build_mac.sh @@ -7,6 +7,9 @@ export LIBRARY_PATH=/opt/homebrew/lib incremental_p="${SIOYEK_BUILD_INCREMENTAL_P}" # Using `SIOYEK_BUILD_INCREMENTAL_P=y` will cause the build script to become optimized for the local development builds and not publishing the app. +incremental_p="${SIOYEK_BUILD_INCREMENTAL_P}" +# Using `SIOYEK_BUILD_INCREMENTAL_P=y` will cause the build script to become optimized for the local development builds and not publishing the app. + #sys_glut_clfags=`pkg-config --cflags glut gl` #sys_glut_libs=`pkg-config --libs glut gl` #sys_harfbuzz_clfags=`pkg-config --cflags harfbuzz` From 940611ef054ed5f8daa693db7f0636a917d18b11 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sat, 13 Jan 2024 15:25:39 +0330 Subject: [PATCH 13/26] MainWidget: added isKeyPressed --- pdf_viewer/main_widget.cpp | 7 +++++++ pdf_viewer/main_widget.h | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 9b3ae68a1..29ac1c9e1 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -1894,6 +1894,7 @@ void MainWidget::key_event(bool released, QKeyEvent* kevent) { if (released == false) { + keyStates[kevent->key()] = true; #ifdef SIOYEK_ANDROID if (kevent->key() == Qt::Key::Key_VolumeDown) { @@ -1958,10 +1959,16 @@ void MainWidget::key_event(bool released, QKeyEvent* kevent) { //for (auto& command : commands) { // handle_command_types(std::move(command), num_repeats); //} + } else { + keyStates[kevent->key()] = false; } } +bool MainWidget::isKeyPressed(int key) const { + return keyStates.value(key, false); +} + void MainWidget::handle_right_click(WindowPos click_pos, bool down, bool is_shift_pressed, bool is_control_pressed, bool is_alt_pressed) { if (is_scratchpad_mode()){ diff --git a/pdf_viewer/main_widget.h b/pdf_viewer/main_widget.h index 5d13c6feb..a2a0e52e5 100644 --- a/pdf_viewer/main_widget.h +++ b/pdf_viewer/main_widget.h @@ -18,6 +18,8 @@ #include "input.h" #include "path.h" +#include + extern float VERTICAL_MOVE_AMOUNT; extern float HORIZONTAL_MOVE_AMOUNT; @@ -116,6 +118,8 @@ enum class PaperDownloadFinishedAction { class MainWidget : public QQuickWidget { Q_OBJECT public: + QMap keyStates; + fz_context* mupdf_context = nullptr; DatabaseManager* db_manager = nullptr; DocumentManager* document_manager = nullptr; @@ -363,6 +367,8 @@ class MainWidget : public QQuickWidget { void add_text_annotation_to_selected_highlight(const std::wstring& annot_text); + bool isKeyPressed(int key) const; + // search the `paper_name` in one of the configurable when middle-click or shift+middle-clicking on paper's name void handle_search_paper_name(std::wstring paper_name, bool is_shift_pressed); From cb80499866140193fdad8a04a26151b8c38d5db8 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sat, 13 Jan 2024 15:27:43 +0330 Subject: [PATCH 14/26] MainWidget::wheelEvent: escape+scroll also scrolls horizontally --- pdf_viewer/main_widget.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 29ac1c9e1..3fe498751 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -2819,9 +2819,12 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { bool is_control_pressed = QApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier) || QApplication::queryKeyboardModifiers().testFlag(Qt::MetaModifier); + bool zoom_p = is_control_pressed; bool is_shift_pressed = QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier); bool is_visual_mark_mode = main_document_view->is_ruler_mode() && visual_scroll_mode; + bool is_esc_pressed = isKeyPressed(Qt::Key_Escape); + bool scroll_horizontally_p = is_shift_pressed || is_esc_pressed; #ifdef SIOYEK_QT6 @@ -2847,7 +2850,7 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { num_repeats = 1; } - if ((!is_control_pressed) && (!is_shift_pressed)) { + if ((!zoom_p) && (!scroll_horizontally_p)) { if (opengl_widget->is_window_point_in_overview({ normal_x, normal_y })) { if (wevent->angleDelta().y() > 0) { scroll_overview(-1); @@ -2926,12 +2929,12 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { } } - if (is_control_pressed) { + if (zoom_p) { float zoom_factor = 1.0f + num_repeats_f * (ZOOM_INC_FACTOR - 1.0f); zoom(mouse_window_pos, zoom_factor, wevent->angleDelta().y() > 0); return; } - if (is_shift_pressed) { + if (scroll_horizontally_p) { float inverse_factor = INVERTED_HORIZONTAL_SCROLLING ? -1.0f : 1.0f; if (wevent->angleDelta().y() > 0) { From 0bfc5a687424ab9f5b53865c23922a272a7aaaae Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sat, 13 Jan 2024 17:01:26 +0330 Subject: [PATCH 15/26] NIGHT_P: added hiredis, redisFlagGet --- build_mac.sh | 15 ++++--- pdf_viewer/main_widget.cpp | 51 ++++++++++++++++++++++++ pdf_viewer/main_widget.h | 14 +++++++ pdf_viewer/utils.h | 4 ++ pdf_viewer_build_config.pro | 8 ++++ sioyek.app/Contents/Info.plist | 41 +++++++++++++++++++ sioyek.app/Contents/PkgInfo | 1 + sioyek.app/Contents/Resources/icon2.ico | Bin 0 -> 42508 bytes 8 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 sioyek.app/Contents/Info.plist create mode 100644 sioyek.app/Contents/PkgInfo create mode 100644 sioyek.app/Contents/Resources/icon2.ico diff --git a/build_mac.sh b/build_mac.sh index 0864e9d3a..86d66d4c1 100755 --- a/build_mac.sh +++ b/build_mac.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -exo pipefail + # prerequisite: brew install qt qt@5 freeglut mesa harfbuzz hiredis export INCLUDE_PATH=/opt/homebrew/include export LIBRARY_PATH=/opt/homebrew/lib @@ -7,9 +8,6 @@ export LIBRARY_PATH=/opt/homebrew/lib incremental_p="${SIOYEK_BUILD_INCREMENTAL_P}" # Using `SIOYEK_BUILD_INCREMENTAL_P=y` will cause the build script to become optimized for the local development builds and not publishing the app. -incremental_p="${SIOYEK_BUILD_INCREMENTAL_P}" -# Using `SIOYEK_BUILD_INCREMENTAL_P=y` will cause the build script to become optimized for the local development builds and not publishing the app. - #sys_glut_clfags=`pkg-config --cflags glut gl` #sys_glut_libs=`pkg-config --libs glut gl` #sys_harfbuzz_clfags=`pkg-config --cflags harfbuzz` @@ -23,12 +21,17 @@ cd mupdf make XLIBS="-L${LIBRARY_PATH}" cd .. +qmake_opts=() +if test -n "${SIOYEK_NIGHT_P}" ; then + qmake_opts+=("DEFINES+=NIGHT_P") +fi + if [[ $1 == portable ]]; then - qmake pdf_viewer_build_config.pro -else - qmake "CONFIG+=non_portable" pdf_viewer_build_config.pro + qmake_opts+=("CONFIG+=non_portable") fi +qmake "${qmake_opts[@]}" pdf_viewer_build_config.pro + make -j$MAKE_PARALLEL rm -rf build 2> /dev/null diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 3fe498751..94e311354 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -756,6 +756,17 @@ MainWidget::MainWidget(fz_context* mupdf_context, window_id = next_window_id; next_window_id++; + #ifdef NIGHT_P + // Initialize the Redis connection + redisContext_ = redisConnect("127.0.0.1", 6379); + + if (redisContext_ != nullptr && redisContext_->err) { + // If the connection failed, clean up and set the pointer to nullptr + redisFree(redisContext_); + redisContext_ = nullptr; + } + #endif + setMouseTracking(true); setAcceptDrops(true); setAttribute(Qt::WA_DeleteOnClose); @@ -1130,6 +1141,15 @@ MainWidget::~MainWidget() { get_tts()->stop(); } validation_interval_timer->stop(); + + #ifdef NIGHT_P + // Clean up the Redis connection if it was established + if (redisContext_ != nullptr) { + redisFree(redisContext_); + redisContext_ = nullptr; + } + #endif + remove_self_from_windows(); if (windows.size() == 0) { @@ -1969,6 +1989,28 @@ bool MainWidget::isKeyPressed(int key) const { return keyStates.value(key, false); } +#ifdef NIGHT_P +bool MainWidget::redisFlagGet(const QString &name) { + if (redisContext_ == nullptr) { + return false; // Return false if the connection was not established + } + + redisReply *reply = static_cast(redisCommand(redisContext_, "GET %s", name.toUtf8().constData())); + if (reply == nullptr) { + return false; // Return false if the command failed + } + + bool result = false; // Default result is false + if (reply->type == REDIS_REPLY_STRING) { + // Convert the reply to a bool + result = (QString(reply->str).toLower() == "true"); + } + + freeReplyObject(reply); // Free the reply object + return result; // Return the result +} +#endif + void MainWidget::handle_right_click(WindowPos click_pos, bool down, bool is_shift_pressed, bool is_control_pressed, bool is_alt_pressed) { if (is_scratchpad_mode()){ @@ -2821,6 +2863,15 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { QApplication::queryKeyboardModifiers().testFlag(Qt::MetaModifier); bool zoom_p = is_control_pressed; + #ifdef NIGHT_P + // bool is_hyper = redisFlagGet("hyper_modality"); + // @surprise Using =redisFlagGet= here can have performance implications, so I have disabled it. + // However, I did not notice a performance degration when testing it briefly. + bool is_hyper = false; + + zoom_p = zoom_p || is_hyper; + #endif + bool is_shift_pressed = QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier); bool is_visual_mark_mode = main_document_view->is_ruler_mode() && visual_scroll_mode; bool is_esc_pressed = isKeyPressed(Qt::Key_Escape); diff --git a/pdf_viewer/main_widget.h b/pdf_viewer/main_widget.h index a2a0e52e5..847eace87 100644 --- a/pdf_viewer/main_widget.h +++ b/pdf_viewer/main_widget.h @@ -20,6 +20,10 @@ #include +#ifdef NIGHT_P +#include +#endif + extern float VERTICAL_MOVE_AMOUNT; extern float HORIZONTAL_MOVE_AMOUNT; @@ -372,6 +376,10 @@ class MainWidget : public QQuickWidget { // search the `paper_name` in one of the configurable when middle-click or shift+middle-clicking on paper's name void handle_search_paper_name(std::wstring paper_name, bool is_shift_pressed); + #ifdef NIGHT_P + bool redisFlagGet(const QString &name); + #endif + void persist(bool persist_drawings = false); bool is_pending_link_source_filled(); std::wstring get_status_string(); @@ -905,6 +913,12 @@ class MainWidget : public QQuickWidget { void handle_highlight_tags_pre_perform(const std::vector& visible_highlight_indices); void clear_keyboard_select_highlights(); void handle_goto_link_with_page_and_offset(int page, float y_offset); + +private: +#ifdef NIGHT_P + redisContext *redisContext_; // Member variable to hold the Redis connection +#endif + }; #endif diff --git a/pdf_viewer/utils.h b/pdf_viewer/utils.h index 7e4d999ab..0f8e7e8fa 100644 --- a/pdf_viewer/utils.h +++ b/pdf_viewer/utils.h @@ -26,6 +26,10 @@ #include "utf8.h" #include "coordinates.h" +#ifdef NIGHT_P +#include +#endif + #define LL_ITER(name, start) for(auto name=start;(name);name=name->next) #define LOG(expr) if (VERBOSE) {(expr);}; diff --git a/pdf_viewer_build_config.pro b/pdf_viewer_build_config.pro index 189bca88c..674c4f6fb 100644 --- a/pdf_viewer_build_config.pro +++ b/pdf_viewer_build_config.pro @@ -9,6 +9,7 @@ INCLUDEPATH += ./pdf_viewer \ INCLUDEPATH += zlib } +INCLUDEPATH += $$(INCLUDE_PATH) QT += core opengl gui widgets network 3dinput quickwidgets svg texttospeech @@ -192,7 +193,14 @@ unix:!mac:!android { mac { QMAKE_CXXFLAGS += -std=c++17 + LIBS += -ldl -Lmupdf/build/release -lmupdf -lmupdf-third -lmupdf-threads -lz + LIBS += -L$$(LIBRARY_PATH) + + contains(DEFINES, "NIGHT_P") { + LIBS += -lhiredis + } + CONFIG+=sdk_no_version_check QMAKE_MACOSX_DEPLOYMENT_TARGET = 11 ICON = pdf_viewer\icon2.ico diff --git a/sioyek.app/Contents/Info.plist b/sioyek.app/Contents/Info.plist new file mode 100644 index 000000000..89bc574cb --- /dev/null +++ b/sioyek.app/Contents/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + PDF Document + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + com.adobe.pdf + + + + CFBundleExecutable + sioyek + CFBundleIconFile + icon2.ico + CFBundleIdentifier + info.sioyek.sioyek + CFBundleName + sioyek + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0 + CFBundleSignature + ???? + LSMinimumSystemVersion + 10.15 + NSPrincipalClass + NSApplication + NSSupportsAutomaticGraphicsSwitching + + + diff --git a/sioyek.app/Contents/PkgInfo b/sioyek.app/Contents/PkgInfo new file mode 100644 index 000000000..6f749b0f3 --- /dev/null +++ b/sioyek.app/Contents/PkgInfo @@ -0,0 +1 @@ +APPL???? diff --git a/sioyek.app/Contents/Resources/icon2.ico b/sioyek.app/Contents/Resources/icon2.ico new file mode 100644 index 0000000000000000000000000000000000000000..a866c6cf322786fe67becc0ffe846b5f3ae0cad7 GIT binary patch literal 42508 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 literal 0 HcmV?d00001 From 8be7dd215dbfb0005429e0f6a51b76578ed4e230 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sat, 13 Jan 2024 17:16:54 +0330 Subject: [PATCH 16/26] NIGHT_P: config changes in scroll_horizontally_p and zoom_p --- pdf_viewer/main_widget.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 94e311354..6295159fa 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -2869,13 +2869,21 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { // However, I did not notice a performance degration when testing it briefly. bool is_hyper = false; - zoom_p = zoom_p || is_hyper; + bool is_z_pressed = isKeyPressed(Qt::Key_Z); + + zoom_p = zoom_p || is_hyper || is_z_pressed; #endif bool is_shift_pressed = QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier); bool is_visual_mark_mode = main_document_view->is_ruler_mode() && visual_scroll_mode; + bool scroll_horizontally_p = is_shift_pressed; + + #ifdef NIGHT_P bool is_esc_pressed = isKeyPressed(Qt::Key_Escape); - bool scroll_horizontally_p = is_shift_pressed || is_esc_pressed; + bool is_x_pressed = isKeyPressed(Qt::Key_X); + + scroll_horizontally_p = is_shift_pressed || is_esc_pressed || is_x_pressed; + #endif #ifdef SIOYEK_QT6 From 9b05564b48eb51921184df870d0485b4247ecbe0 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sun, 14 Jan 2024 08:28:00 +0330 Subject: [PATCH 17/26] added delete_last_highlight --- pdf_viewer/document.cpp | 9 +++++++++ pdf_viewer/document.h | 1 + pdf_viewer/document_view.cpp | 4 ++++ pdf_viewer/document_view.h | 1 + pdf_viewer/input.cpp | 15 +++++++++++++++ pdf_viewer/main_widget.cpp | 4 ++++ pdf_viewer/main_widget.h | 1 + 7 files changed, 35 insertions(+) diff --git a/pdf_viewer/document.cpp b/pdf_viewer/document.cpp index fe532ab9a..98bcc98c3 100644 --- a/pdf_viewer/document.cpp +++ b/pdf_viewer/document.cpp @@ -471,6 +471,15 @@ void Document::delete_highlight_with_index(int index) { is_annotations_dirty = true; } +void Document::delete_last_highlight() { + if (!highlights.empty()) { + + int last_index = highlights.size() - 1; + + delete_highlight_with_index(last_index); + } +} + void Document::delete_highlight(Highlight hl) { for (size_t i = (highlights.size() - 1); i >= 0; i--) { if (highlights[i] == hl) { diff --git a/pdf_viewer/document.h b/pdf_viewer/document.h index 37dbef5b6..a1cfc445a 100644 --- a/pdf_viewer/document.h +++ b/pdf_viewer/document.h @@ -180,6 +180,7 @@ class Document { std::string add_highlight(const std::wstring& desc, const std::vector& highlight_rects, AbsoluteDocumentPos selection_begin, AbsoluteDocumentPos selection_end, char type); std::string add_highlight(const std::wstring& annot, AbsoluteDocumentPos selection_begin, AbsoluteDocumentPos selection_end, char type); void delete_highlight_with_index(int index); + void delete_last_highlight(); void delete_highlight(Highlight hl); int get_bookmark_index_at_pos(AbsoluteDocumentPos abspos); int get_portal_index_at_pos(AbsoluteDocumentPos abspos); diff --git a/pdf_viewer/document_view.cpp b/pdf_viewer/document_view.cpp index 313c71b55..d98d04d09 100644 --- a/pdf_viewer/document_view.cpp +++ b/pdf_viewer/document_view.cpp @@ -216,6 +216,10 @@ void DocumentView::delete_highlight_with_index(int index) { current_document->delete_highlight_with_index(index); } +void DocumentView::delete_last_highlight() { + current_document->delete_last_highlight(); +} + void DocumentView::delete_highlight(Highlight hl) { current_document->delete_highlight(hl); } diff --git a/pdf_viewer/document_view.h b/pdf_viewer/document_view.h index fb5110321..bd44ef089 100644 --- a/pdf_viewer/document_view.h +++ b/pdf_viewer/document_view.h @@ -83,6 +83,7 @@ class DocumentView { void delete_closest_bookmark(); Highlight get_highlight_with_index(int index); void delete_highlight_with_index(int index); + void delete_last_highlight(); void delete_highlight(Highlight hl); void delete_closest_bookmark_to_offset(float offset); float get_offset_x(); diff --git a/pdf_viewer/input.cpp b/pdf_viewer/input.cpp index 85f39276f..41f8ce692 100644 --- a/pdf_viewer/input.cpp +++ b/pdf_viewer/input.cpp @@ -4686,6 +4686,19 @@ class DeleteHighlightUnderCursorCommand : public Command { }; +class DeleteLastHighlightCommand : public Command { +public: + DeleteLastHighlightCommand(MainWidget* w) : Command(w) {}; + + void perform() { + widget->handle_delete_last_highlight(); + } + + std::string get_name() { + return "delete_last_highlight"; + } +}; + class NoopCommand : public Command { public: @@ -6193,6 +6206,7 @@ CommandManager::CommandManager(ConfigManager* config_manager) { new_commands["overview_next_item"] = [](MainWidget* widget) {return std::make_unique< OverviewNextItemCommand>(widget); }; new_commands["overview_prev_item"] = [](MainWidget* widget) {return std::make_unique< OverviewPrevItemCommand>(widget); }; new_commands["delete_highlight_under_cursor"] = [](MainWidget* widget) {return std::make_unique< DeleteHighlightUnderCursorCommand>(widget); }; + new_commands["delete_last_highlight"] = [](MainWidget* widget) {return std::make_unique< DeleteLastHighlightCommand>(widget); }; new_commands["noop"] = [](MainWidget* widget) {return std::make_unique< NoopCommand>(widget); }; new_commands["import"] = [](MainWidget* widget) {return std::make_unique< ImportCommand>(widget); }; new_commands["export"] = [](MainWidget* widget) {return std::make_unique< ExportCommand>(widget); }; @@ -6404,6 +6418,7 @@ CommandManager::CommandManager(ConfigManager* config_manager) { command_human_readable_names["overview_next_item"] = "Open an overview to the next search result"; command_human_readable_names["overview_prev_item"] = "Open an overview to the previous search result"; command_human_readable_names["delete_highlight_under_cursor"] = "Delete highlight under mouse cursor"; + command_human_readable_names["delete_last_highlight"] = "Delete the last highlight"; command_human_readable_names["noop"] = "Do nothing"; command_human_readable_names["import"] = "Import annotation data from a json file"; command_human_readable_names["export"] = "Export annotation data to a json file"; diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 6295159fa..3faf2193b 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -5874,6 +5874,10 @@ void MainWidget::handle_toggle_typing_mode() { } } +void MainWidget::handle_delete_last_highlight() { + main_document_view->delete_last_highlight(); +} + void MainWidget::handle_delete_highlight_under_cursor() { QPoint mouse_pos = mapFromGlobal(cursor_pos()); WindowPos window_pos = WindowPos{ mouse_pos.x(), mouse_pos.y() }; diff --git a/pdf_viewer/main_widget.h b/pdf_viewer/main_widget.h index 847eace87..07f75cb64 100644 --- a/pdf_viewer/main_widget.h +++ b/pdf_viewer/main_widget.h @@ -607,6 +607,7 @@ class MainWidget : public QQuickWidget { void handle_toggle_typing_mode(); void handle_delete_highlight_under_cursor(); void handle_delete_selected_highlight(); + void handle_delete_last_highlight(); void handle_start_reading(); void handle_stop_reading(); void handle_play(); From 1da0d69fdcdb30c8f6187df0e2d0a862d6819238 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sun, 14 Jan 2024 08:33:47 +0330 Subject: [PATCH 18/26] keys.config: added delete_last_highlight --- pdf_viewer/keys.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pdf_viewer/keys.config b/pdf_viewer/keys.config index 700767d4e..8ecc68ae9 100644 --- a/pdf_viewer/keys.config +++ b/pdf_viewer/keys.config @@ -184,6 +184,8 @@ goto_highlight gh goto_highlight_g gH # Left click on a highlight and then press the `delete_highlight` shortcut to delete it. delete_highlight dh +# Delete the last highlight +# delete_last_highlight # Sets the highlight type to be used for other operations (the default highlight type is 'a') #set_select_highlight_type From 434f1726527351bb93edd0336e81c8da0ab000f5 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sun, 14 Jan 2024 11:37:47 +0330 Subject: [PATCH 19/26] added scroll_zoom_inc_factor --- pdf_viewer/config.cpp | 10 ++++++++++ pdf_viewer/document_view.h | 1 + pdf_viewer/main.cpp | 1 + pdf_viewer/main_widget.cpp | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pdf_viewer/config.cpp b/pdf_viewer/config.cpp index 482b1b4fe..0dc4f6b4a 100644 --- a/pdf_viewer/config.cpp +++ b/pdf_viewer/config.cpp @@ -6,6 +6,7 @@ //#include extern float ZOOM_INC_FACTOR; +extern float SCROLL_ZOOM_INC_FACTOR; extern float GAMMA; extern float VERTICAL_MOVE_AMOUNT; extern float HORIZONTAL_MOVE_AMOUNT; @@ -718,6 +719,15 @@ ConfigManager::ConfigManager(const Path& default_path, const Path& auto_path, co nullptr, FloatExtras{1.0f, 2.0f} }); + configs.push_back({ + L"scroll_zoom_inc_factor", + ConfigType::Float, + &SCROLL_ZOOM_INC_FACTOR, + float_serializer, + float_deserializer, + nullptr, + FloatExtras{1.0f, 6.0f} + }); configs.push_back({ L"vertical_move_amount", ConfigType::Float, diff --git a/pdf_viewer/document_view.h b/pdf_viewer/document_view.h index bd44ef089..99f8130d3 100644 --- a/pdf_viewer/document_view.h +++ b/pdf_viewer/document_view.h @@ -16,6 +16,7 @@ #include "book.h" extern float ZOOM_INC_FACTOR; +extern float SCROLL_ZOOM_INC_FACTOR; extern const int PAGE_PADDINGS; class CachedChecksummer; diff --git a/pdf_viewer/main.cpp b/pdf_viewer/main.cpp index 72455665b..5ebedf13d 100644 --- a/pdf_viewer/main.cpp +++ b/pdf_viewer/main.cpp @@ -183,6 +183,7 @@ float HIGHLIGHT_COLORS[26 * 3] = { \ float DARK_MODE_CONTRAST = 0.8f; float ZOOM_INC_FACTOR = 1.2f; +float SCROLL_ZOOM_INC_FACTOR = 1.2f; float VERTICAL_MOVE_AMOUNT = 1.0f; float HORIZONTAL_MOVE_AMOUNT = 1.0f; float MOVE_SCREEN_PERCENTAGE = 0.5f; diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 3faf2193b..7878579ae 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -2989,7 +2989,7 @@ void MainWidget::wheelEvent(QWheelEvent* wevent) { } if (zoom_p) { - float zoom_factor = 1.0f + num_repeats_f * (ZOOM_INC_FACTOR - 1.0f); + float zoom_factor = 1.0f + num_repeats_f * (SCROLL_ZOOM_INC_FACTOR - 1.0f); zoom(mouse_window_pos, zoom_factor, wevent->angleDelta().y() > 0); return; } From a6b75d7d8d86279eb6fe3574d9df0c705b9d072c Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Sun, 14 Jan 2024 21:56:26 +0330 Subject: [PATCH 20/26] added regex_searching --- pdf_viewer/config.cpp | 9 ++++++ pdf_viewer/main.cpp | 1 + pdf_viewer/mysortfilterproxymodel.cpp | 44 +++++++++++++++++++++------ pdf_viewer/prefs.config | 3 ++ pdf_viewer/ui.cpp | 6 +++- pdf_viewer/ui.h | 6 +++- pdf_viewer/utils.cpp | 21 +++++++++++++ pdf_viewer/utils.h | 8 +++++ 8 files changed, 86 insertions(+), 12 deletions(-) diff --git a/pdf_viewer/config.cpp b/pdf_viewer/config.cpp index 0dc4f6b4a..dfb9b958f 100644 --- a/pdf_viewer/config.cpp +++ b/pdf_viewer/config.cpp @@ -168,6 +168,7 @@ extern bool SHOULD_HIGHLIGHT_LINKS; extern bool SHOULD_HIGHLIGHT_UNSELECTED_SEARCH; extern int KEYBOARD_SELECT_FONT_SIZE; extern bool FUZZY_SEARCHING; +extern bool REGEX_SEARCHING; extern float CUSTOM_COLOR_CONTRAST; extern bool DEBUG; extern bool DEBUG_DISPLAY_FREEHAND_POINTS; @@ -1871,6 +1872,14 @@ ConfigManager::ConfigManager(const Path& default_path, const Path& auto_path, co bool_deserializer, bool_validator }); + configs.push_back({ + L"regex_searching", + ConfigType::Bool, + ®EX_SEARCHING, + bool_serializer, + bool_deserializer, + bool_validator + }); configs.push_back({ L"debug", ConfigType::Bool, diff --git a/pdf_viewer/main.cpp b/pdf_viewer/main.cpp index 5ebedf13d..661def616 100644 --- a/pdf_viewer/main.cpp +++ b/pdf_viewer/main.cpp @@ -292,6 +292,7 @@ bool SHOULD_HIGHLIGHT_UNSELECTED_SEARCH = false; int KEYBOARD_SELECT_FONT_SIZE = 20; int DOCUMENTATION_FONT_SIZE = 16; bool FUZZY_SEARCHING = false; +bool REGEX_SEARCHING = false; bool INVERTED_HORIZONTAL_SCROLLING = false; bool TOC_JUMP_ALIGN_TOP = false; float CUSTOM_COLOR_CONTRAST = 0.5f; diff --git a/pdf_viewer/mysortfilterproxymodel.cpp b/pdf_viewer/mysortfilterproxymodel.cpp index 0bc45c63c..e7873e0e9 100644 --- a/pdf_viewer/mysortfilterproxymodel.cpp +++ b/pdf_viewer/mysortfilterproxymodel.cpp @@ -5,6 +5,10 @@ #include "rapidfuzz_amalgamated.hpp" + +extern bool REGEX_SEARCHING; + + bool MySortFilterProxyModel::filter_accepts_row_column(int row, int col, const QModelIndex& source_parent) const { if (filterString.size() == 0 || filterString == "") return true; @@ -16,15 +20,18 @@ bool MySortFilterProxyModel::filter_accepts_row_column(int row, int col, const Q QString key = sourceModel()->data(source_index, filterRole()).toString(); std::wstring s1 = filterString.toStdWString(); std::wstring s2 = key.toStdWString(); - int score = calculate_partial_ratio(s1, s2); - return score > 50; - + if (REGEX_SEARCHING) { + return bool_regex_match(QString::fromStdWString(s1), QString::fromStdWString(s2)); + } else { + int score = calculate_partial_ratio(s1, s2); + return score > 50; + } } bool MySortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { - if (is_fuzzy) { + if (is_fuzzy || REGEX_SEARCHING) { int key_column = this->filterKeyColumn(); @@ -51,7 +58,7 @@ bool MySortFilterProxyModel::filterAcceptsRow(int source_row, void MySortFilterProxyModel::setFilterCustom(const QString& filterString) { - if (is_fuzzy) { + if (is_fuzzy || REGEX_SEARCHING) { this->filterString = filterString; this->setFilterFixedString(filterString); update_scores(); @@ -77,8 +84,11 @@ void MySortFilterProxyModel::setFilterCustom(const QString& filterString) { bool MySortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - if (is_fuzzy) { - + if (REGEX_SEARCHING) { + // When regex searching is enabled, maintain the original order + return left.row() < right.row(); + } + else if (is_fuzzy) { QString leftData = sourceModel()->data(left).toString(); QString rightData = sourceModel()->data(right).toString(); @@ -97,7 +107,7 @@ bool MySortFilterProxyModel::lessThan(const QModelIndex& left, MySortFilterProxyModel::MySortFilterProxyModel(bool fuzzy) { is_fuzzy = fuzzy; - if (fuzzy) { + if (fuzzy || REGEX_SEARCHING) { setDynamicSortFilter(true); } } @@ -113,7 +123,14 @@ void MySortFilterProxyModel::update_scores() { if ((n_cols == 1) || (filter_column_index >= 0)) { for (int i = 0; i < n_rows; i++) { QString row_data = sourceModel()->data(sourceModel()->index(i, filter_column_index)).toString(); - int score = calculate_partial_ratio(filter_wstring, row_data.toStdWString()); + + int score = 0; + if (REGEX_SEARCHING) { + score = bool_regex_match(QString::fromStdWString(filter_wstring), QString::fromStdWString(row_data.toStdWString())) ? 100 : 0; + } + else { + score = calculate_partial_ratio(filter_wstring, row_data.toStdWString()); + } scores.push_back(score); } } @@ -124,7 +141,14 @@ void MySortFilterProxyModel::update_scores() { for (int col_index = 0; col_index < n_cols; col_index++) { QString rowcol_data = sourceModel()->data(sourceModel()->index(i, col_index)).toString(); - int col_score = calculate_partial_ratio(filter_wstring, rowcol_data.toStdWString()); + int col_score = 0; + if (REGEX_SEARCHING) { + col_score = bool_regex_match(QString::fromStdWString(filter_wstring), QString::fromStdWString(rowcol_data.toStdWString())) ? 100 : 0; + } + else { + col_score = calculate_partial_ratio(filter_wstring, rowcol_data.toStdWString()); + } + if (col_score > score) score = col_score; } diff --git a/pdf_viewer/prefs.config b/pdf_viewer/prefs.config index 00e94db2c..3c9c554ca 100644 --- a/pdf_viewer/prefs.config +++ b/pdf_viewer/prefs.config @@ -275,3 +275,6 @@ highlight_color_y 1.00 1.00 0.00 #Zinnia highlight_color_z 1.00 0.31 0.02 +# Choose at most one of the below modes. +# fuzzy_searching 0 +# regex_searching 0 diff --git a/pdf_viewer/ui.cpp b/pdf_viewer/ui.cpp index 5add024e2..da66b4ba1 100644 --- a/pdf_viewer/ui.cpp +++ b/pdf_viewer/ui.cpp @@ -1341,7 +1341,11 @@ bool CommandSelector::on_text_change(const QString& text) { for (int i = 0; i < string_elements.size(); i++) { std::string encoded = utf8_encode(string_elements.at(i).toStdWString()); int score = 0; - if (is_fuzzy) { + + if (REGEX_SEARCHING) { + score = bool_regex_match(QString::fromStdString(search_text_string), QString::fromStdString(encoded)) ? 100 : 0; + } + else if (is_fuzzy) { score = calculate_partial_ratio(search_text_string, encoded); } else { diff --git a/pdf_viewer/ui.h b/pdf_viewer/ui.h index b4c55535f..afb8b06fe 100644 --- a/pdf_viewer/ui.h +++ b/pdf_viewer/ui.h @@ -76,6 +76,7 @@ extern bool SMALL_TOC; extern bool MULTILINE_MENUS; extern bool EMACS_MODE; extern bool TOUCH_MODE; +extern bool REGEX_SEARCHING; class HierarchialSortFilterProxyModel : public QSortFilterProxyModel { @@ -662,7 +663,10 @@ class FileSelector : public BaseSelectorWidget { for (auto file : all_directory_files) { std::string encoded_file = utf8_encode(file.toStdWString()); int score = 0; - if (is_fuzzy) { + if (REGEX_SEARCHING) { + score = bool_regex_match(QString::fromStdString(encoded_prefix), QString::fromStdString(encoded_file)) ? 100 : 0; + } + else if (is_fuzzy) { score = calculate_partial_ratio(encoded_prefix, encoded_file); } else { diff --git a/pdf_viewer/utils.cpp b/pdf_viewer/utils.cpp index e5d81cda8..98d8e9779 100644 --- a/pdf_viewer/utils.cpp +++ b/pdf_viewer/utils.cpp @@ -3963,3 +3963,24 @@ int calculate_partial_ratio(const StringType& filterString, const StringType& ke // Explicit template instantiation for std::wstring and std::string template int calculate_partial_ratio(const std::string&, const std::string&, bool); template int calculate_partial_ratio(const std::wstring&, const std::wstring&, bool); + +bool match_patterns(const QString& key, const QStringList& patterns) { + for (const QString &pattern : patterns) { + bool has_upper_case = std::any_of(pattern.begin(), pattern.end(), [](QChar c) { return c.isUpper(); }); + QRegularExpression::PatternOptions options = has_upper_case ? QRegularExpression::NoPatternOption + : QRegularExpression::CaseInsensitiveOption; + QRegularExpression regex(pattern, options); + + if (!regex.match(key).hasMatch()) { + return false; // If any pattern does not match, reject the string + } + } + return true; // All patterns matched +} + +bool bool_regex_match(const QString& search_text, const QString& key) { + if (search_text.isEmpty()) return true; + + QStringList patterns = search_text.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + return match_patterns(key, patterns); +} diff --git a/pdf_viewer/utils.h b/pdf_viewer/utils.h index 0f8e7e8fa..c769c05b8 100644 --- a/pdf_viewer/utils.h +++ b/pdf_viewer/utils.h @@ -16,6 +16,12 @@ #include +#include +#include +#include + +#include + #include #include #include @@ -461,3 +467,5 @@ bool is_in(char c, std::vector candidates); bool should_trigger_delete(QKeyEvent *key_event); +bool match_patterns(const QString& key, const QStringList& patterns); +bool bool_regex_match(const QString& search_text, const QString& key); From 2eeabd7fac7551c2005e2e4e4b12369766db86cf Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Mon, 15 Jan 2024 13:51:55 +0330 Subject: [PATCH 21/26] InputHandler::handle_key: fixed bug in handling Backspace on macOS --- pdf_viewer/input.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf_viewer/input.cpp b/pdf_viewer/input.cpp index 41f8ce692..ab7f695a7 100644 --- a/pdf_viewer/input.cpp +++ b/pdf_viewer/input.cpp @@ -6902,7 +6902,7 @@ bool is_digit(int key) { std::unique_ptr InputHandler::handle_key(MainWidget* w, QKeyEvent* key_event, bool shift_pressed, bool control_pressed, bool alt_pressed, int* num_repeats) { int key = 0; if (!USE_LEGACY_KEYBINDS) { - std::vector special_texts = { "\b", "\t", " ", "\r", "\n" }; + std::vector special_texts = { "\b", "\u007F", "\t", " ", "\r", "\n" }; if (((key_event->key() >= 'A') && (key_event->key() <= 'Z')) || ((key_event->text().size() > 0) && (std::find(special_texts.begin(), special_texts.end(), key_event->text()) == special_texts.end()))) { if (!control_pressed && !alt_pressed) { From 0ae615d2ad8a74d6773486ef5ea63a8d167daaf9 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Mon, 15 Jan 2024 16:21:55 +0330 Subject: [PATCH 22/26] added previous_page_smart, next_page_smart --- pdf_viewer/document_view.cpp | 95 +++++++++++++++++++++++++++++++++--- pdf_viewer/document_view.h | 13 +++-- pdf_viewer/input.cpp | 38 +++++++++++++++ 3 files changed, 133 insertions(+), 13 deletions(-) diff --git a/pdf_viewer/document_view.cpp b/pdf_viewer/document_view.cpp index d98d04d09..6661d5f8d 100644 --- a/pdf_viewer/document_view.cpp +++ b/pdf_viewer/document_view.cpp @@ -16,6 +16,9 @@ extern float RULER_X_PADDING; extern bool EXACT_HIGHLIGHT_SELECT; extern bool VERBOSE; +float GOTO_LEFTRIGHT_SMART_ACCEPTABLE_MARGIN = 40 ; // TODO make this configurable +float GOTO_TOPBOTTOM_SMART_ACCEPTABLE_MARGIN = 40 ; // TODO make this configurable + DocumentView::DocumentView(DatabaseManager* db_manager, DocumentManager* document_manager, @@ -481,14 +484,21 @@ void DocumentView::goto_end() { } } -void DocumentView::goto_left_smart() { +bool DocumentView::goto_left_smart() { float left_ratio, right_ratio; int normal_page_width; float page_width = current_document->get_page_size_smart(true, get_center_page_number(), &left_ratio, &right_ratio, &normal_page_width); float view_left_offset = (page_width / 2 - view_width / zoom_level / 2); - set_offset_x(view_left_offset); + float previous_offset_x = offset_x; + set_offset_x(view_left_offset); + + // qDebug() << "offset_x:" << offset_x << "previous_offset_x:" << previous_offset_x; + + auto acceptable_margin = GOTO_LEFTRIGHT_SMART_ACCEPTABLE_MARGIN; + return (((offset_x - previous_offset_x)) > acceptable_margin); + // returns whether we moved more to the left or not } void DocumentView::goto_left() { @@ -497,14 +507,21 @@ void DocumentView::goto_left() { set_offset_x(view_left_offset); } -void DocumentView::goto_right_smart() { +bool DocumentView::goto_right_smart() { float left_ratio, right_ratio; int normal_page_width; float page_width = current_document->get_page_size_smart(true, get_center_page_number(), &left_ratio, &right_ratio, &normal_page_width); float view_left_offset = -(page_width / 2 - view_width / zoom_level / 2); - set_offset_x(view_left_offset); + float previous_offset_x = offset_x; + set_offset_x(view_left_offset); + + // qDebug() << "offset_x:" << offset_x << "previous_offset_x:" << previous_offset_x; + + auto acceptable_margin = GOTO_LEFTRIGHT_SMART_ACCEPTABLE_MARGIN; + return ((-(offset_x - previous_offset_x)) > acceptable_margin); + // returns whether we moved more to the right or not } void DocumentView::goto_right() { @@ -513,6 +530,48 @@ void DocumentView::goto_right() { set_offset_x(view_left_offset); } +bool DocumentView::previous_page_smart() { + bool moved_top_p = goto_top_of_page(); + + if (! moved_top_p) { + bool moved_left_p = goto_left_smart(); + if (moved_left_p) { + // qDebug() << "PreviousPageSmartCommand: Moved left"; + + goto_bottom_of_page(); + } else { + // qDebug() << "PreviousPageSmartCommand: already at the left edge; going to the previous page's bottom right corner."; + + if (move_pages(-1)) { + goto_right_smart(); + goto_bottom_of_page(); + } + } + } +} + +bool DocumentView::next_page_smart() { + bool moved_bottom_p = goto_bottom_of_page(); + + if (! moved_bottom_p) { + bool moved_right_p = goto_right_smart(); + if (moved_right_p) { + // qDebug() << "NextPageSmartCommand: Moved right"; + + goto_top_of_page(); + } else { + // qDebug() << "NextPageSmartCommand: already at the right edge; going to the next page's top left corner."; + + if (move_pages(1)) { + goto_left_smart(); + goto_top_of_page(); + } else { + return false; + } + } + } +} + float DocumentView::set_zoom_level(float zl, bool should_exit_auto_resize_mode) { #ifdef SIOYEK_ANDROID const float max_zoom_level = 6.0f; @@ -616,13 +675,17 @@ void DocumentView::get_visible_pages(int window_height, std::vector& visibl current_document->get_visible_pages(window_y_range_begin, window_y_range_end, visible_pages); } -void DocumentView::move_pages(int num_pages) { - if (!current_document) return; +bool DocumentView::move_pages(int num_pages) { + if (!current_document) return false; int current_page = get_center_page_number(); if (current_page == -1) { current_page = 0; } move_absolute(0, num_pages * (current_document->get_page_height(current_page) + PAGE_PADDINGS)); + + int final_page = get_center_page_number(); + + return (final_page != current_page); } void DocumentView::move_screens(int num_screens) { @@ -1056,13 +1119,23 @@ void DocumentView::rotate() { set_offset_y(new_offset); } -void DocumentView::goto_top_of_page() { +bool DocumentView::goto_top_of_page() { + float previous_offset_y = offset_y; + int current_page = get_center_page_number(); float offset_y = get_document()->get_accum_page_height(current_page) + static_cast(view_height) / 2.0f / zoom_level; set_offset_y(offset_y); + + // qDebug() << "offset_y:" << offset_y << "previous_offset_y:" << previous_offset_y; + + auto acceptable_margin = GOTO_TOPBOTTOM_SMART_ACCEPTABLE_MARGIN; + return ((-(offset_y - previous_offset_y)) > acceptable_margin); + // returns whether we moved more to the top or not } -void DocumentView::goto_bottom_of_page() { +bool DocumentView::goto_bottom_of_page() { + float previous_offset_y = offset_y; + int current_page = get_center_page_number(); float offset_y = get_document()->get_accum_page_height(current_page + 1) - static_cast(view_height) / 2.0f / zoom_level; @@ -1070,6 +1143,12 @@ void DocumentView::goto_bottom_of_page() { offset_y = get_document()->get_accum_page_height(current_page) + get_document()->get_page_height(current_page) - static_cast(view_height) / 2.0f / zoom_level; } set_offset_y(offset_y); + + // qDebug() << "offset_y:" << offset_y << "previous_offset_y:" << previous_offset_y; + + auto acceptable_margin = GOTO_TOPBOTTOM_SMART_ACCEPTABLE_MARGIN; + return (((offset_y - previous_offset_y)) > acceptable_margin); + // returns whether we moved more to the bottom or not } int DocumentView::get_line_index() { diff --git a/pdf_viewer/document_view.h b/pdf_viewer/document_view.h index 99f8130d3..778145724 100644 --- a/pdf_viewer/document_view.h +++ b/pdf_viewer/document_view.h @@ -123,10 +123,13 @@ class DocumentView { void goto_end(); void goto_left(); - void goto_left_smart(); + bool goto_left_smart(); void goto_right(); - void goto_right_smart(); + bool goto_right_smart(); + + bool previous_page_smart(); + bool next_page_smart(); float get_max_valid_x(bool relenting); float get_min_valid_x(bool relenting); @@ -141,7 +144,7 @@ class DocumentView { void get_absolute_delta_from_doc_delta(float doc_dx, float doc_dy, float* abs_dx, float* abs_dy); int get_center_page_number(); void get_visible_pages(int window_height, std::vector& visible_pages); - void move_pages(int num_pages); + bool move_pages(int num_pages); void move_screens(int num_screens); void reset_doc_state(); void open_document(const std::wstring& doc_path, bool* invalid_flag, bool load_prev_state = true, std::optional prev_state = {}, bool foce_load_dimensions = false); @@ -175,8 +178,8 @@ class DocumentView { int get_page_offset(); void set_page_offset(int new_offset); void rotate(); - void goto_top_of_page(); - void goto_bottom_of_page(); + bool goto_top_of_page(); + bool goto_bottom_of_page(); int get_line_index_of_vertical_pos(); int get_line_index_of_pos(DocumentPos docpos); int get_line_index(); diff --git a/pdf_viewer/input.cpp b/pdf_viewer/input.cpp index ab7f695a7..6f37c183b 100644 --- a/pdf_viewer/input.cpp +++ b/pdf_viewer/input.cpp @@ -2267,6 +2267,40 @@ class PreviousPageCommand : public Command { }; +class NextPageSmartCommand : public Command { +public: + NextPageSmartCommand(MainWidget* w) : Command(w) {}; + + void perform() { + auto my_num_repeats = std::max(1, num_repeats); + for (int i = 0; i < my_num_repeats; i++) { + if (!(widget->main_document_view->next_page_smart())) { + break; + } + } + } + std::string get_name() { + return "next_page_smart"; + } +}; + +class PreviousPageSmartCommand : public Command { +public: + PreviousPageSmartCommand(MainWidget* w) : Command(w) {}; + + void perform() { + auto my_num_repeats = std::max(1, num_repeats); + for (int i = 0; i < my_num_repeats; i++) { + if (!(widget->main_document_view->previous_page_smart())) { + break; + } + } + } + std::string get_name() { + return "previous_page_smart"; + } +}; + class ZoomOutCommand : public Command { public: ZoomOutCommand(MainWidget* w) : Command(w) {}; @@ -6056,6 +6090,8 @@ CommandManager::CommandManager(ConfigManager* config_manager) { new_commands["fit_to_page_width_smart"] = [](MainWidget* widget) {return std::make_unique< FitToPageWidthSmartCommand>(widget); }; new_commands["next_page"] = [](MainWidget* widget) {return std::make_unique< NextPageCommand>(widget); }; new_commands["previous_page"] = [](MainWidget* widget) {return std::make_unique< PreviousPageCommand>(widget); }; + new_commands["next_page_smart"] = [](MainWidget* widget) {return std::make_unique< NextPageSmartCommand>(widget); }; + new_commands["previous_page_smart"] = [](MainWidget* widget) {return std::make_unique< PreviousPageSmartCommand>(widget); }; new_commands["open_document"] = [](MainWidget* widget) {return std::make_unique< OpenDocumentCommand>(widget); }; new_commands["screenshot"] = [](MainWidget* widget) {return std::make_unique< ScreenshotCommand>(widget); }; new_commands["framebuffer_screenshot"] = [](MainWidget* widget) {return std::make_unique< FramebufferScreenshotCommand>(widget); }; @@ -6295,6 +6331,8 @@ CommandManager::CommandManager(ConfigManager* config_manager) { command_human_readable_names["fit_to_page_width_smart"] = "Fit the page to screen width, ignoring white page margins"; command_human_readable_names["next_page"] = "Go to next page"; command_human_readable_names["previous_page"] = "Go to previous page"; + command_human_readable_names["next_page_smart"] = "Go to next page (tries to go to the next column on two-column pages)"; + command_human_readable_names["previous_page"] = "Go to previous page (tries to go to the previous column on two-column pages)"; command_human_readable_names["open_document"] = "Open documents using native file explorer"; command_human_readable_names["add_bookmark"] = "Add an invisible bookmark in the current location"; command_human_readable_names["add_marked_bookmark"] = "Add a bookmark in the selected location"; From 53e99fe391e79ccc9188c1299b307c89765c3f93 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Mon, 15 Jan 2024 19:39:36 +0330 Subject: [PATCH 23/26] added PushStateCommand --- pdf_viewer/input.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pdf_viewer/input.cpp b/pdf_viewer/input.cpp index 6f37c183b..d8941a0cb 100644 --- a/pdf_viewer/input.cpp +++ b/pdf_viewer/input.cpp @@ -1624,6 +1624,20 @@ class NextStateCommand : public Command { bool requires_document() { return false; } }; +class PushStateCommand : public Command { +public: + PushStateCommand(MainWidget* w) : Command(w) {}; + + void perform() { + widget->push_state(); + } + + std::string get_name() { + return "push_state"; + } + bool requires_document() { return false; } +}; + class PrevStateCommand : public Command { public: PrevStateCommand(MainWidget* w) : Command(w) {}; @@ -6116,6 +6130,7 @@ CommandManager::CommandManager(ConfigManager* config_manager) { new_commands["portal"] = [](MainWidget* widget) {return std::make_unique< PortalCommand>(widget); }; new_commands["create_visible_portal"] = [](MainWidget* widget) {return std::make_unique< CreateVisiblePortalCommand>(widget); }; new_commands["next_state"] = [](MainWidget* widget) {return std::make_unique< NextStateCommand>(widget); }; + new_commands["push_state"] = [](MainWidget* widget) {return std::make_unique< PushStateCommand>(widget); }; new_commands["prev_state"] = [](MainWidget* widget) {return std::make_unique< PrevStateCommand>(widget); }; new_commands["history_forward"] = [](MainWidget* widget) {return std::make_unique< NextStateCommand>(widget); }; new_commands["history_back"] = [](MainWidget* widget) {return std::make_unique< PrevStateCommand>(widget); }; @@ -6306,6 +6321,7 @@ CommandManager::CommandManager(ConfigManager* config_manager) { command_human_readable_names["move_text_mark_down"] = "Move text cursor down"; command_human_readable_names["move_text_mark_up"] = "Move text cursor up"; command_human_readable_names["set_mark"] = "Set mark in current location"; + command_human_readable_names["push_state"] = "Pushes the current state in the navigation history"; command_human_readable_names["toggle_drawing_mask"] = "Toggle drawing type visibility"; command_human_readable_names["turn_on_all_drawings"] = "Make all freehand drawings visible"; command_human_readable_names["turn_off_all_drawings"] = "Make all freehand drawings invisible"; From ef13e134f152d87aa70de112881b357fc796ccab Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Tue, 16 Jan 2024 10:01:03 +0330 Subject: [PATCH 24/26] added keyboard_select_copy_p --- pdf_viewer/config.cpp | 9 +++++++++ pdf_viewer/main.cpp | 1 + pdf_viewer/main_widget.cpp | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pdf_viewer/config.cpp b/pdf_viewer/config.cpp index dfb9b958f..e6df04311 100644 --- a/pdf_viewer/config.cpp +++ b/pdf_viewer/config.cpp @@ -16,6 +16,7 @@ extern float UNSELECTED_SEARCH_HIGHLIGHT_COLOR[3]; extern float DARK_MODE_BACKGROUND_COLOR[3]; extern float CUSTOM_COLOR_MODE_EMPTY_BACKGROUND_COLOR[3]; extern float DARK_MODE_CONTRAST; +extern bool KEYBOARD_SELECT_COPY_P; extern bool FLAT_TABLE_OF_CONTENTS; extern bool SMALL_TOC; extern bool SHOULD_USE_MULTIPLE_MONITORS; @@ -1880,6 +1881,14 @@ ConfigManager::ConfigManager(const Path& default_path, const Path& auto_path, co bool_deserializer, bool_validator }); + configs.push_back({ + L"keyboard_select_copy_p", + ConfigType::Bool, + &KEYBOARD_SELECT_COPY_P, + bool_serializer, + bool_deserializer, + bool_validator + }); configs.push_back({ L"debug", ConfigType::Bool, diff --git a/pdf_viewer/main.cpp b/pdf_viewer/main.cpp index 661def616..d9adbadec 100644 --- a/pdf_viewer/main.cpp +++ b/pdf_viewer/main.cpp @@ -191,6 +191,7 @@ const unsigned int CACHE_INVALID_MILIES = 1000; const int PERSIST_MILIES = 1000 * 60; const int PAGE_PADDINGS = 0; const int MAX_PENDING_REQUESTS = 31; +bool KEYBOARD_SELECT_COPY_P = false; bool FLAT_TABLE_OF_CONTENTS = false; bool SHOULD_USE_MULTIPLE_MONITORS = false; bool SHOULD_CHECK_FOR_LATEST_VERSION_ON_STARTUP = false; diff --git a/pdf_viewer/main_widget.cpp b/pdf_viewer/main_widget.cpp index 7878579ae..c44b17c3c 100644 --- a/pdf_viewer/main_widget.cpp +++ b/pdf_viewer/main_widget.cpp @@ -94,6 +94,7 @@ extern int next_window_id; +extern bool KEYBOARD_SELECT_COPY_P; extern bool SHOULD_USE_MULTIPLE_MONITORS; extern bool MULTILINE_MENUS; extern bool SORT_BOOKMARKS_BY_LOCATION; @@ -4728,8 +4729,12 @@ void MainWidget::handle_keyboard_select(const std::wstring& text) { opengl_widget->set_should_highlight_words(false); } + } + + if (KEYBOARD_SELECT_COPY_P) { + copy_to_clipboard(this->selected_text); } - } + } } From 28ded77cd146cb4a0a1dfaddca487bde9b9d5c06 Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Tue, 16 Jan 2024 20:11:26 +0330 Subject: [PATCH 25/26] fixed typo in the doc of previous_page_smart --- pdf_viewer/input.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf_viewer/input.cpp b/pdf_viewer/input.cpp index d8941a0cb..4010a4b75 100644 --- a/pdf_viewer/input.cpp +++ b/pdf_viewer/input.cpp @@ -6348,7 +6348,7 @@ CommandManager::CommandManager(ConfigManager* config_manager) { command_human_readable_names["next_page"] = "Go to next page"; command_human_readable_names["previous_page"] = "Go to previous page"; command_human_readable_names["next_page_smart"] = "Go to next page (tries to go to the next column on two-column pages)"; - command_human_readable_names["previous_page"] = "Go to previous page (tries to go to the previous column on two-column pages)"; + command_human_readable_names["previous_page_smart"] = "Go to previous page (tries to go to the previous column on two-column pages)"; command_human_readable_names["open_document"] = "Open documents using native file explorer"; command_human_readable_names["add_bookmark"] = "Add an invisible bookmark in the current location"; command_human_readable_names["add_marked_bookmark"] = "Add a bookmark in the selected location"; From 6a48415edd4973f18942b70a76591d9298b194db Mon Sep 17 00:00:00 2001 From: NightMachinery Date: Thu, 18 Jan 2024 16:04:23 +0330 Subject: [PATCH 26/26] BaseSelectorWidget: uses should_trigger_delete on QT 6 --- pdf_viewer/ui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf_viewer/ui.cpp b/pdf_viewer/ui.cpp index da66b4ba1..f7b4a533d 100644 --- a/pdf_viewer/ui.cpp +++ b/pdf_viewer/ui.cpp @@ -1505,7 +1505,7 @@ bool BaseSelectorWidget::eventFilter(QObject* obj, QEvent* event) { #ifdef SIOYEK_QT6 if (event->type() == QEvent::KeyRelease) { QKeyEvent* key_event = static_cast(event); - if (key_event->key() == Qt::Key_Delete) { + if (should_trigger_delete(key_event)) { handle_delete(); } else if (key_event->key() == Qt::Key_Insert) {