diff --git a/prettysumatra/webui/toolbar.html b/prettysumatra/webui/toolbar.html
index 33597c81d41b..0d3c09e0dc18 100644
--- a/prettysumatra/webui/toolbar.html
+++ b/prettysumatra/webui/toolbar.html
@@ -1,2556 +1,2508 @@
-
-
-
-
-
-
-PrettySumatraPDF
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+PrettySumatraPDF
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/installer/PrettySumatraPDF.iss b/scripts/installer/PrettySumatraPDF.iss
index 3695a0d17206..916f3c083036 100644
--- a/scripts/installer/PrettySumatraPDF.iss
+++ b/scripts/installer/PrettySumatraPDF.iss
@@ -2,7 +2,7 @@
; Requires Inno Setup (ISCC.exe) to compile
#define AppName "PrettySumatraPDF"
-#define AppVersion "1.0.0"
+#define AppVersion "1.0.1"
#define AppPublisher "PrettySumatraPDF"
#define AppURL "https://github.com/JaviLendi/PrettySumatraPDF"
#define AppExeName "SumatraPDF-dll.exe"
diff --git a/scripts/installer/README_INSTALLER.md b/scripts/installer/README_INSTALLER.md
index 13d4639e4e48..4148ef1df068 100644
--- a/scripts/installer/README_INSTALLER.md
+++ b/scripts/installer/README_INSTALLER.md
@@ -21,7 +21,7 @@ cmd /c "scripts\\build-vs2022.cmd Release x64 SumatraPDF-dll"
3. Compile the installer with Inno Setup:
```powershell
-& 'C:\Users\javil\AppData\Local\Programs\Inno Setup 6\ISCC.exe' scripts\\installer\\PrettySumatraPDF.iss
+& 'C:\Users\user\AppData\Local\Programs\Inno Setup 6\ISCC.exe' scripts\\installer\\PrettySumatraPDF.iss
```
4. The installer will be produced under `scripts\installer\dist\` with filename `PrettySumatraPDF-Setup-1.0.0.exe`.
diff --git a/scripts/installer/dist/PrettySumatraPDF-Setup-1.0.1.exe b/scripts/installer/dist/PrettySumatraPDF-Setup-1.0.1.exe
new file mode 100644
index 000000000000..214b49be5d84
Binary files /dev/null and b/scripts/installer/dist/PrettySumatraPDF-Setup-1.0.1.exe differ
diff --git a/src/DisplayModel.cpp b/src/DisplayModel.cpp
index 3f4d9f6dacb0..35fd41a6038c 100644
--- a/src/DisplayModel.cpp
+++ b/src/DisplayModel.cpp
@@ -72,6 +72,33 @@
// if true, we pre-render the pages right before and after the visible pages
bool gPredictiveRender = true;
+constexpr int kLargeDocPrecacheThresholdPages = 100;
+constexpr int kLargeDocPrecacheRadiusPages = 30;
+
+static bool IsDisplayModelValid(DisplayModel* dm) {
+ for (MainWindow* win : gWindows) {
+ for (WindowTab* tab : win->Tabs()) {
+ if (tab->AsFixed() == dm) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+static void RenderVisiblePartsOnUIThread(DisplayModel* dm) {
+ if (!IsDisplayModelValid(dm)) {
+ return;
+ }
+ dm->renderVisiblePartsScheduled = false;
+ dm->RenderVisibleParts();
+}
+
+static void ScheduleRenderVisiblePartsOnUIThread(DisplayModel* dm) {
+ auto fn = MkFunc0(RenderVisiblePartsOnUIThread, dm);
+ uitask::Post(fn, "RenderVisibleParts");
+}
+
static int ColumnsFromDisplayMode(DisplayMode displayMode) {
if (!IsSingle(displayMode)) {
return 2;
@@ -219,17 +246,6 @@ void DisplayModel::RepaintDisplay() {
cb->Repaint();
}
-static bool IsDisplayModelValid(DisplayModel* dm) {
- for (MainWindow* win : gWindows) {
- for (WindowTab* tab : win->Tabs()) {
- if (tab->AsFixed() == dm) {
- return true;
- }
- }
- }
- return false;
-}
-
static void RenderFinishedOnUIThread(PageRenderRequest* req) {
if (!IsDisplayModelValid(req->dm)) {
delete req;
@@ -1202,13 +1218,6 @@ void DisplayModel::RenderVisibleParts() {
return;
}
- // rendering happens LIFO except if the queue is currently
- // empty, so request the visible pages first and last to
- // make sure they're rendered before the predicted pages
- for (int pageNo = firstVisiblePage; pageNo <= lastVisiblePage; pageNo++) {
- cb->RequestRendering(pageNo);
- }
-
if (gPredictiveRender) {
// prerender two more pages in facing and book view modes
// if the rendering queue still has place for them
@@ -1228,15 +1237,43 @@ void DisplayModel::RenderVisibleParts() {
}
}
- // re-request the visible pages again so:
- // * they get picked first by rendering thread
- // * if queue fills up, the invisible pages from predictive rendering
- // wont be rendered
+ // rendering happens LIFO, so request the visible pages last in reverse
+ // order to make sure the most relevant page gets rendered first.
for (int pageNo = lastVisiblePage; pageNo >= firstVisiblePage; pageNo--) {
cb->RequestRendering(pageNo);
}
}
+void DisplayModel::PrecacheLargeDocumentPages() {
+ if (!cb || PageCount() <= kLargeDocPrecacheThresholdPages) {
+ return;
+ }
+
+ int currentPageNo = CurrentPageNo();
+ if (!ValidPageNo(currentPageNo)) {
+ return;
+ }
+
+ for (int delta = kLargeDocPrecacheRadiusPages; delta >= 1; --delta) {
+ int prevPageNo = currentPageNo - delta;
+ if (prevPageNo >= 1) {
+ cb->RequestRendering(prevPageNo);
+ }
+ int nextPageNo = currentPageNo + delta;
+ if (nextPageNo <= PageCount()) {
+ cb->RequestRendering(nextPageNo);
+ }
+ }
+}
+
+void DisplayModel::ScheduleRenderVisibleParts() {
+ if (pauseRendering || renderVisiblePartsScheduled) {
+ return;
+ }
+ renderVisiblePartsScheduled = true;
+ ScheduleRenderVisiblePartsOnUIThread(this);
+}
+
void DisplayModel::SetViewPortSize(Size newViewPortSize) {
ScrollState ss;
@@ -1257,7 +1294,7 @@ void DisplayModel::SetViewPortSize(Size newViewPortSize) {
}
} else {
RecalcVisibleParts();
- RenderVisibleParts();
+ ScheduleRenderVisibleParts();
cb->UpdateScrollbars(canvasSize);
}
}
@@ -1370,7 +1407,7 @@ void DisplayModel::GoToPage(int pageNo, int scrollY, bool addNavPt, int scrollX)
viewPort.y = limitValue(viewPort.y, 0, canvasSize.dy - viewPort.dy);
RecalcVisibleParts();
- RenderVisibleParts();
+ ScheduleRenderVisibleParts();
cb->UpdateScrollbars(canvasSize);
cb->PageNoChanged(this, pageNo);
RepaintDisplay();
@@ -1559,7 +1596,7 @@ void DisplayModel::ScrollYTo(int yOff) {
int currPageNo = CurrentPageNo();
viewPort.y = yOff;
RecalcVisibleParts();
- RenderVisibleParts();
+ ScheduleRenderVisibleParts();
int newPageNo = CurrentPageNo();
if (newPageNo != currPageNo) {
@@ -1617,7 +1654,7 @@ void DisplayModel::ScrollYBy(int dy, bool changePage) {
currPageNo = CurrentPageNo();
viewPort.y = newYOff;
RecalcVisibleParts();
- RenderVisibleParts();
+ ScheduleRenderVisibleParts();
cb->UpdateScrollbars(canvasSize);
newPageNo = CurrentPageNo();
if (newPageNo != currPageNo) {
diff --git a/src/DisplayModel.h b/src/DisplayModel.h
index 5e008ac216be..cde573fb82f3 100644
--- a/src/DisplayModel.h
+++ b/src/DisplayModel.h
@@ -207,6 +207,8 @@ struct DisplayModel : DocController {
Point GetContentStart(int pageNo) const;
void RecalcVisibleParts() const;
void RenderVisibleParts();
+ void PrecacheLargeDocumentPages();
+ void ScheduleRenderVisibleParts();
void AddNavPoint();
RectF GetContentBox(int pageNo) const;
void CalcZoomReal(float zoomVirtual);
@@ -263,6 +265,7 @@ struct DisplayModel : DocController {
/* allow resizing a window without triggering a new rendering (needed for window destruction) */
bool pauseRendering = false;
+ bool renderVisiblePartsScheduled = false;
void RenderFinished(PageRenderRequest* req);
void RenderFinishedAsync(PageRenderRequest* req);
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 8e5d168cffc2..9dc6660fff2d 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -262,6 +262,12 @@ struct MainWindow {
LayoutState lastLayoutState;
int currPageNo = 0; // cached value, needed to determine when to auto-update the ToC selection
+ DocController* hybridToolbarSyncCtrl = nullptr;
+ int hybridToolbarSyncPageNo = 0;
+ int hybridToolbarSyncPageCount = 0;
+ float hybridToolbarSyncZoom = 0.0f;
+ bool hybridToolbarSyncCanAnnotate = false;
+ bool hybridToolbarSyncHasState = false;
// overlay scrollbars (used when scrollbars mode is "smart" or "overlay")
struct OverlayScrollbar* overlayScrollV = nullptr;
diff --git a/src/RenderCache.cpp b/src/RenderCache.cpp
index 53216ce8417f..32d795445c35 100644
--- a/src/RenderCache.cpp
+++ b/src/RenderCache.cpp
@@ -38,6 +38,17 @@ static DWORD WINAPI RenderCacheThread(LPVOID data);
bool gShowTileLayout = false;
int gMaxRenderThreads = 8;
+static int GetRequestPriority(DisplayModel* dm, int pageNo) {
+ if (!dm || !dm->ValidPageNo(pageNo)) {
+ return 0;
+ }
+ int currentPageNo = dm->CurrentPageNo();
+ if (!dm->ValidPageNo(currentPageNo)) {
+ return 0;
+ }
+ return pageNo >= currentPageNo ? pageNo - currentPageNo : currentPageNo - pageNo;
+}
+
struct RenderThreadData {
RenderCache* cache;
int threadIdx;
@@ -114,16 +125,39 @@ RenderCache::~RenderCache() {
and in the cache - call DropCacheEntry when you
no longer need a found entry. */
BitmapCacheEntry* RenderCache::Find(DisplayModel* dm, int pageNo, int rotation, float zoom, TilePosition* tile) {
- ScopedCritSec scope(&cacheAccess);
rotation = NormalizeRotation(rotation);
+ float targetZoom = zoom == kInvalidZoom ? dm->GetZoomReal(pageNo) : zoom;
+
+ ScopedCritSec scope(&cacheAccess);
+ BitmapCacheEntry* bestMatch = nullptr;
+ float bestZoomDiff = 0;
for (int i = 0; i < cacheCount; i++) {
BitmapCacheEntry* e = cache[i];
- if ((dm == e->dm) && (pageNo == e->pageNo) && (rotation == e->rotation) &&
- (kInvalidZoom == zoom || zoom == e->zoom) && (!tile || e->tile == *tile)) {
+ if ((dm != e->dm) || (pageNo != e->pageNo) || (rotation != e->rotation) || (tile && e->tile != *tile)) {
+ continue;
+ }
+ if (kInvalidZoom != zoom && zoom == e->zoom) {
e->refs++;
ReportIf(i != e->cacheIdx);
return e;
}
+ if (kInvalidZoom == zoom) {
+ if (e->zoom == kInvalidZoom) {
+ continue;
+ }
+ float zoomDiff = e->zoom - targetZoom;
+ if (zoomDiff < 0) {
+ zoomDiff = -zoomDiff;
+ }
+ if (!bestMatch || zoomDiff < bestZoomDiff) {
+ bestMatch = e;
+ bestZoomDiff = zoomDiff;
+ }
+ }
+ }
+ if (bestMatch) {
+ bestMatch->refs++;
+ return bestMatch;
}
return nullptr;
}
@@ -497,6 +531,7 @@ void RenderCache::RequestRendering(DisplayModel* dm, int pageNo, TilePosition ti
int rotation = NormalizeRotation(dm->GetRotation());
float zoom = dm->GetZoomReal(pageNo);
+ int priority = GetRequestPriority(dm, pageNo);
for (int i = 0; i < nRenderThreads; i++) {
auto* cr = curReqs[i];
@@ -533,6 +568,7 @@ void RenderCache::RequestRendering(DisplayModel* dm, int pageNo, TilePosition ti
req->zoom = zoom;
req->rotation = rotation;
}
+ req->priority = std::min(req->priority, priority);
return;
}
}
@@ -577,18 +613,29 @@ bool RenderCache::Render(DisplayModel* dm, int pageNo, int rotation, float zoom,
ScopedCritSec scope(&requestAccess);
PageRenderRequest* newRequest;
+ int priority = GetRequestPriority(dm, pageNo);
/* add request to the queue */
if (requestCount == MAX_PAGE_REQUESTS) {
- /* queue is full -> remove the oldest items on the queue */
- if (requests[0].renderFinishedCb.IsValid()) {
- requests[0].abort = true;
- requests[0].bmp = nullptr;
- requests[0].errorCode = 0;
- requests[0].renderFinishedCb.Call(&requests[0]);
+ /* queue is full -> replace the lowest-priority queued item if the new
+ request is more important. Otherwise drop the new request. */
+ int worstIdx = 0;
+ for (int i = 1; i < MAX_PAGE_REQUESTS; i++) {
+ if (requests[i].priority > requests[worstIdx].priority ||
+ (requests[i].priority == requests[worstIdx].priority && requests[i].timestamp < requests[worstIdx].timestamp)) {
+ worstIdx = i;
+ }
+ }
+ if (priority >= requests[worstIdx].priority) {
+ return true;
+ }
+ if (requests[worstIdx].renderFinishedCb.IsValid()) {
+ requests[worstIdx].abort = true;
+ requests[worstIdx].bmp = nullptr;
+ requests[worstIdx].errorCode = 0;
+ requests[worstIdx].renderFinishedCb.Call(&requests[worstIdx]);
}
- memmove(&(requests[0]), &(requests[1]), sizeof(PageRenderRequest) * (MAX_PAGE_REQUESTS - 1));
- newRequest = &(requests[MAX_PAGE_REQUESTS - 1]);
+ newRequest = &(requests[worstIdx]);
} else {
newRequest = &(requests[requestCount]);
requestCount++;
@@ -599,6 +646,7 @@ bool RenderCache::Render(DisplayModel* dm, int pageNo, int rotation, float zoom,
newRequest->pageNo = pageNo;
newRequest->rotation = rotation;
newRequest->zoom = zoom;
+ newRequest->priority = priority;
if (tile) {
newRequest->pageRect = GetTileRectUser(dm->GetEngine(), pageNo, rotation, zoom, *tile);
newRequest->tile = *tile;
@@ -661,8 +709,18 @@ bool RenderCache::GetNextRequest(PageRenderRequest* req, int threadIdx) {
ReportIf(requestCount < 0);
ReportIf(requestCount > MAX_PAGE_REQUESTS);
+ int bestIdx = 0;
+ for (int i = 1; i < requestCount; i++) {
+ if (requests[i].priority < requests[bestIdx].priority ||
+ (requests[i].priority == requests[bestIdx].priority && i > bestIdx)) {
+ bestIdx = i;
+ }
+ }
+ *req = requests[bestIdx];
requestCount--;
- *req = requests[requestCount];
+ if (bestIdx != requestCount) {
+ requests[bestIdx] = requests[requestCount];
+ }
curReqs[threadIdx] = req;
ReportIf(requestCount < 0);
ReportIf(req->abort);
@@ -677,6 +735,8 @@ bool RenderCache::ClearCurrentRequest(int threadIdx) {
}
curReqs[threadIdx] = nullptr;
+
+
bool isQueueEmpty = requestCount == 0;
return isQueueEmpty;
}
@@ -686,7 +746,6 @@ bool RenderCache::ClearCurrentRequest(int threadIdx) {
user know he has to wait until we finish */
void RenderCache::CancelRendering(DisplayModel* dm) {
ClearQueueForDisplayModel(dm);
-
for (;;) {
EnterCriticalSection(&requestAccess);
bool found = false;
@@ -704,8 +763,9 @@ void RenderCache::CancelRendering(DisplayModel* dm) {
}
LeaveCriticalSection(&requestAccess);
- /* TODO: busy loop is not good, but I don't have a better idea */
- Sleep(50);
+ // Small sleep to avoid busy-looping while waiting for render threads
+ // to process the abort signals
+ Sleep(10);
}
}
diff --git a/src/RenderCache.h b/src/RenderCache.h
index 3ac00908ba24..c179beab5fea 100644
--- a/src/RenderCache.h
+++ b/src/RenderCache.h
@@ -71,6 +71,7 @@ struct PageRenderRequest {
int pageNo = 0;
int rotation = 0;
float zoom = 0.f;
+ int priority = 0;
TilePosition tile;
RectF pageRect; // calculated from TilePosition
diff --git a/src/SumatraPDF.cpp b/src/SumatraPDF.cpp
index 870390eae83c..02b3175426b1 100644
--- a/src/SumatraPDF.cpp
+++ b/src/SumatraPDF.cpp
@@ -5732,15 +5732,11 @@ void SetSidebarVisibility(MainWindow* win, bool tocVisible, bool showFavorites,
// Notify hybrid toolbar WebView (if present) about panel state changes
if (win->hybridToolbar) {
- TempStr jsFav =
- str::FormatTemp("try{if(window.notifyPanelState)window.notifyPanelState('favorites',%s);}catch(e){};",
- showFavorites ? "true" : "false");
- win->hybridToolbar->Eval(jsFav);
-
- TempStr jsToc =
- str::FormatTemp("try{if(window.notifyPanelState)window.notifyPanelState('bookmarks',%s);}catch(e){};",
- tocVisible ? "true" : "false");
- win->hybridToolbar->Eval(jsToc);
+ TempStr js = str::FormatTemp(
+ "try{if(window.notifyPanelState){window.notifyPanelState('favorites',%s);"
+ "window.notifyPanelState('bookmarks',%s);}}catch(e){};",
+ showFavorites ? "true" : "false", tocVisible ? "true" : "false");
+ win->hybridToolbar->Eval(js);
}
}
diff --git a/src/SumatraStartup.cpp b/src/SumatraStartup.cpp
index 5b6e1a248c33..30b17f76b826 100644
--- a/src/SumatraStartup.cpp
+++ b/src/SumatraStartup.cpp
@@ -1613,7 +1613,9 @@ int APIENTRY WinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE, _In_ LPST
// TODO: pass print request through to previous instance?
} else if (flags.reuseDdeInstance || flags.dde) {
existingHwnd = FindWindowW(FRAME_CLASS_NAME, nullptr);
- } else if (gGlobalPrefs->reuseInstance) {
+ } else if (gGlobalPrefs->reuseInstance || SettingsUseTabs()) {
+ // With tabs enabled, route new file opens to the running instance so
+ // they land in a new tab instead of a separate process.
existingHwnd = existingInstanceHwnd;
}
diff --git a/src/Theme.cpp b/src/Theme.cpp
index 96370a54f961..e7f1eea3f451 100644
--- a/src/Theme.cpp
+++ b/src/Theme.cpp
@@ -55,13 +55,13 @@ static const char* themesTxt = R"(Themes [
[
Name = Sumatra Dark
TextColor = #f8fafc
- BackgroundColor = #17130a
- ControlBackgroundColor = #3a2901
+ BackgroundColor = #000000
+ ControlBackgroundColor = #222222
LinkColor = #eab308
AccentColor = #eab308
BrandPrimaryColor = #facc15
BrandGlowColor = #fde68a
- ShadowColor = #120b02
+ ShadowColor = #5a5a5a
ColorizeControls = true
]
[
@@ -238,19 +238,15 @@ static int GetThemeVariantIndex(const char* currentName, bool targetDark) {
const int suffixLen = (int)str::Len(str::EndsWithI(currentName, lightSuffix) ? lightSuffix : darkSuffix);
const int nameLen = (int)str::Len(currentName);
const int baseLen = nameLen - suffixLen;
- if (baseLen <= 0 || baseLen >= 256) {
+ if (baseLen <= 0) {
return GetThemeIndexByName(targetDark ? "Dark" : "Light");
}
- char base[256];
- memcpy(base, currentName, baseLen);
- base[baseLen] = '\0';
-
- char targetName[256];
- if (!str::BufFmt(targetName, dimof(targetName), "%s %s", base, targetDark ? "Dark" : "Light")) {
- return GetThemeIndexByName(targetDark ? "Dark" : "Light");
- }
- return GetThemeIndexByName(targetName);
+ StrBuilder targetName(baseLen + 6);
+ targetName.Append(currentName, (size_t)baseLen);
+ targetName.AppendChar(' ');
+ targetName.Append(targetDark ? "Dark" : "Light");
+ return GetThemeIndexByName(targetName.CStr());
}
bool IsCurrentThemeDefault() {
diff --git a/src/prettysumatra/BridgeDispatcher.cpp b/src/prettysumatra/BridgeDispatcher.cpp
index 7e80afc1a288..aff0f58ec4df 100644
--- a/src/prettysumatra/BridgeDispatcher.cpp
+++ b/src/prettysumatra/BridgeDispatcher.cpp
@@ -85,7 +85,11 @@ bool UseHybridShell() {
}
bool LogBridgeMessages() {
+#ifdef NDEBUG
+ return ParseBoolEnvWithDefault("PRETTYSUMATRA_LOG_BRIDGE", false);
+#else
return ParseBoolEnvWithDefault("PRETTYSUMATRA_LOG_BRIDGE", true);
+#endif
}
static bool gHybridFollowWindowsTheme = true;
@@ -111,6 +115,63 @@ static MainWindow* FindWindowForFrame(HWND hwndFrame) {
return FindMainWindowByHwnd(hwndFrame);
}
+static bool HybridToolbarCanAnnotate(MainWindow* win) {
+ if (!win) {
+ return false;
+ }
+ bool canAnnotate = false;
+ WindowTab* tab = win->CurrentTab();
+ if (tab && tab->IsDocLoaded()) {
+ EngineBase* eng = tab->GetEngine();
+ if (eng) {
+ canAnnotate = EngineSupportsAnnotations(eng) && !(win->isFullScreen || win->presentation);
+ }
+ }
+ return canAnnotate;
+}
+
+static void SyncHybridToolbarState(HWND hwndFrame, int currentPage, int totalPages, float zoomPercent,
+ bool canAnnotate) {
+ MainWindow* win = FindWindowForFrame(hwndFrame);
+ if (!win || !win->hybridToolbar) {
+ return;
+ }
+
+ if (currentPage <= 0) {
+ currentPage = 1;
+ }
+ if (totalPages <= 0) {
+ totalPages = 1;
+ }
+ if (zoomPercent < 1.0f) {
+ zoomPercent = 1.0f;
+ }
+ if (zoomPercent > 6400.0f) {
+ zoomPercent = 6400.0f;
+ }
+
+ if (win->hybridToolbarSyncCtrl != win->ctrl) {
+ win->hybridToolbarSyncCtrl = win->ctrl;
+ win->hybridToolbarSyncHasState = false;
+ }
+ if (win->hybridToolbarSyncHasState && win->hybridToolbarSyncPageNo == currentPage &&
+ win->hybridToolbarSyncPageCount == totalPages && win->hybridToolbarSyncZoom == zoomPercent &&
+ win->hybridToolbarSyncCanAnnotate == canAnnotate) {
+ return;
+ }
+
+ win->hybridToolbarSyncPageNo = currentPage;
+ win->hybridToolbarSyncPageCount = totalPages;
+ win->hybridToolbarSyncZoom = zoomPercent;
+ win->hybridToolbarSyncCanAnnotate = canAnnotate;
+ win->hybridToolbarSyncHasState = true;
+
+ TempStr js = str::FormatTemp(
+ "window.hybridToolbarBatchUpdate && window.hybridToolbarBatchUpdate({page:%d,total:%d,zoom:%.4f,canAnnotate:%s});",
+ currentPage, totalPages, zoomPercent, canAnnotate ? "true" : "false");
+ win->hybridToolbar->Eval(js);
+}
+
static TempStr HybridToolbarThemeJs(HWND hwndFrame) {
MainWindow* win = FindWindowForFrame(hwndFrame);
if (!win || !win->hybridToolbar) {
@@ -173,36 +234,11 @@ static TempStr HomePageThemeJs() {
ColorToCssHex(brandGlow), ColorToCssHex(shadow), appDark ? "true" : "false");
}
-static TempStr JsQuoted(const char* s) {
- if (!s) {
- return str::FormatTemp("''");
- }
+static void AppendJsonString(StrBuilder& json, const char* src);
+static TempStr JsQuoted(const char* s) {
StrBuilder sb;
- sb.AppendChar('\'');
- for (const char* p = s; *p; ++p) {
- switch (*p) {
- case '\\':
- sb.Append("\\\\");
- break;
- case '\'':
- sb.Append("\\'");
- break;
- case '\r':
- // skip
- break;
- case '\n':
- sb.Append("\\n");
- break;
- case '\t':
- sb.Append("\\t");
- break;
- default:
- sb.AppendChar(*p);
- break;
- }
- }
- sb.AppendChar('\'');
+ AppendJsonString(sb, s ? s : "");
return (TempStr)sb.StealData();
}
@@ -266,12 +302,8 @@ void SyncHybridToolbarSearchText(HWND hwndFrame, const char* text) {
if (!win || !win->hybridToolbar) {
return;
}
- TempStr escaped = str::ReplaceTemp(text ? text : "", "\\", "\\\\");
- escaped = str::ReplaceTemp(escaped, "'", "\\'");
- escaped = str::ReplaceTemp(escaped, "\r", "");
- escaped = str::ReplaceTemp(escaped, "\n", "\\n");
TempStr js =
- str::FormatTemp("window.hybridToolbarSetSearchText && window.hybridToolbarSetSearchText('%s');", escaped);
+ str::FormatTemp("window.hybridToolbarSetSearchText && window.hybridToolbarSetSearchText(%s);", JsQuoted(text));
win->hybridToolbar->Eval(js);
}
@@ -336,62 +368,32 @@ void InitHybridToolbarText(HWND hwndFrame) {
void SyncHybridToolbarPageState(HWND hwndFrame, int currentPage, int totalPages) {
MainWindow* win = FindWindowForFrame(hwndFrame);
- if (!win || !win->hybridToolbar) {
+ if (!win) {
return;
}
- if (currentPage <= 0) {
- currentPage = 1;
- }
- if (totalPages <= 0) {
- totalPages = 1;
- }
- TempStr js = str::FormatTemp("window.hybridToolbarSetPageState && window.hybridToolbarSetPageState(%d,%d);",
- currentPage, totalPages);
- win->hybridToolbar->Eval(js);
- // Also update annotation availability whenever page/document state is synced
- SyncHybridToolbarAnnotationAvailability(hwndFrame);
+ float zoomPercent = win->ctrl ? win->ctrl->GetZoomVirtual(true) : 100.0f;
+ SyncHybridToolbarState(hwndFrame, currentPage, totalPages, zoomPercent, HybridToolbarCanAnnotate(win));
}
void SyncHybridToolbarAnnotationAvailability(HWND hwndFrame) {
MainWindow* win = FindWindowForFrame(hwndFrame);
- if (!win || !win->hybridToolbar) return;
-
- bool canAnnotate = false;
- WindowTab* tab = win->CurrentTab();
- if (tab && tab->IsDocLoaded()) {
- EngineBase* eng = tab->GetEngine();
- if (eng) {
- canAnnotate = EngineSupportsAnnotations(eng) && !(win->isFullScreen || win->presentation);
- }
- }
-
- const char* h = canAnnotate ? "true" : "false";
- const char* a = canAnnotate ? "true" : "false";
- const char* d = canAnnotate ? "true" : "false";
- TempStr js = str::FormatTemp(
- "window.hybridToolbarSetAnnotationAvailability && "
- "window.hybridToolbarSetAnnotationAvailability({highlight:%s,annotate:%s,draw:%s});",
- h, a, d);
- if (LogBridgeMessages()) {
- logf("[PrettySumatraBridge] sending annotation availability: highlight=%s annotate=%s draw=%s\n", h, a, d);
+ if (!win) {
+ return;
}
- win->hybridToolbar->Eval(js);
+ int currentPage = win->ctrl ? win->ctrl->CurrentPageNo() : 1;
+ int totalPages = win->ctrl ? win->ctrl->PageCount() : 1;
+ float zoomPercent = win->ctrl ? win->ctrl->GetZoomVirtual(true) : 100.0f;
+ SyncHybridToolbarState(hwndFrame, currentPage, totalPages, zoomPercent, HybridToolbarCanAnnotate(win));
}
void SyncHybridToolbarZoomState(HWND hwndFrame, float zoomPercent) {
MainWindow* win = FindWindowForFrame(hwndFrame);
- if (!win || !win->hybridToolbar) {
+ if (!win) {
return;
}
- if (zoomPercent < 1.0f) {
- zoomPercent = 1.0f;
- }
- if (zoomPercent > 6400.0f) {
- zoomPercent = 6400.0f;
- }
- TempStr js =
- str::FormatTemp("window.hybridToolbarSetZoomState && window.hybridToolbarSetZoomState(%.4f);", zoomPercent);
- win->hybridToolbar->Eval(js);
+ int currentPage = win->ctrl ? win->ctrl->CurrentPageNo() : 1;
+ int totalPages = win->ctrl ? win->ctrl->PageCount() : 1;
+ SyncHybridToolbarState(hwndFrame, currentPage, totalPages, zoomPercent, HybridToolbarCanAnnotate(win));
}
struct BridgeMessage {
@@ -772,43 +774,38 @@ static int ResolveBridgeCommandId(const char* commandName) {
if (str::IsEmptyOrWhiteSpace(commandName)) {
return 0;
}
- if (str::EqI(commandName, "commandPalette")) return CmdCommandPalette;
- if (str::EqI(commandName, "openFile")) return CmdOpenFile;
- if (str::EqI(commandName, "properties")) return CmdProperties;
- if (str::EqI(commandName, "find")) return CmdFindFirst;
- if (str::EqI(commandName, "newWindow")) return CmdNewWindow;
- if (str::EqI(commandName, "saveAs")) return CmdSaveAs;
- if (str::EqI(commandName, "reload")) return CmdReloadDocument;
- if (str::EqI(commandName, "reopenLastClosed")) return CmdReopenLastClosedFile;
-
- if (str::EqI(commandName, "navigateBack")) return CmdNavigateBack;
- if (str::EqI(commandName, "navigateForward")) return CmdNavigateForward;
- if (str::EqI(commandName, "prevPage")) return CmdGoToPrevPage;
- if (str::EqI(commandName, "nextPage")) return CmdGoToNextPage;
- if (str::EqI(commandName, "firstPage")) return CmdGoToFirstPage;
- if (str::EqI(commandName, "lastPage")) return CmdGoToLastPage;
-
- if (str::EqI(commandName, "zoomIn")) return CmdZoomIn;
- if (str::EqI(commandName, "zoomOut")) return CmdZoomOut;
- if (str::EqI(commandName, "fitWidth")) return CmdZoomFitWidth;
- if (str::EqI(commandName, "fitPage")) return CmdZoomFitPage;
- if (str::EqI(commandName, "actualSize")) return CmdZoomActualSize;
- if (str::EqI(commandName, "singlePage")) return CmdSinglePageView;
- if (str::EqI(commandName, "facing")) return CmdFacingView;
- if (str::EqI(commandName, "bookView")) return CmdBookView;
- if (str::EqI(commandName, "showPagesContinuously")) return CmdToggleContinuousView;
-
- if (str::EqI(commandName, "toggleBookmarks")) return CmdToggleBookmarks;
- if (str::EqI(commandName, "toggleFavorites")) return CmdFavoriteToggle;
- if (str::EqI(commandName, "toggleFullscreen")) return CmdToggleFullscreen;
- if (str::EqI(commandName, "rotateLeft")) return CmdRotateLeft;
- if (str::EqI(commandName, "rotateRight")) return CmdRotateRight;
- if (str::EqI(commandName, "print")) return CmdPrint;
- if (str::EqI(commandName, "highlightSelection")) return CmdCreateAnnotHighlight;
- if (str::EqI(commandName, "addAnnotation")) return CmdCreateAnnotText;
- if (str::EqI(commandName, "freeDraw")) return CmdCreateAnnotInk;
- if (str::EqI(commandName, "createAnnotUnderline")) return CmdCreateAnnotUnderline;
- if (str::EqI(commandName, "createAnnotStrikeOut")) return CmdCreateAnnotStrikeOut;
+ struct CommandMapEntry {
+ const char* name;
+ int cmdId;
+ };
+
+ static constexpr CommandMapEntry kCommandMap[] = {
+ {"commandPalette", CmdCommandPalette}, {"openFile", CmdOpenFile},
+ {"properties", CmdProperties}, {"find", CmdFindFirst},
+ {"newWindow", CmdNewWindow}, {"saveAs", CmdSaveAs},
+ {"reload", CmdReloadDocument}, {"reopenLastClosed", CmdReopenLastClosedFile},
+ {"navigateBack", CmdNavigateBack}, {"navigateForward", CmdNavigateForward},
+ {"prevPage", CmdGoToPrevPage}, {"nextPage", CmdGoToNextPage},
+ {"firstPage", CmdGoToFirstPage}, {"lastPage", CmdGoToLastPage},
+ {"zoomIn", CmdZoomIn}, {"zoomOut", CmdZoomOut},
+ {"fitWidth", CmdZoomFitWidth}, {"fitPage", CmdZoomFitPage},
+ {"actualSize", CmdZoomActualSize}, {"singlePage", CmdSinglePageView},
+ {"facing", CmdFacingView}, {"bookView", CmdBookView},
+ {"showPagesContinuously", CmdToggleContinuousView},
+ {"toggleBookmarks", CmdToggleBookmarks}, {"toggleFavorites", CmdFavoriteToggle},
+ {"toggleFullscreen", CmdToggleFullscreen}, {"rotateLeft", CmdRotateLeft},
+ {"rotateRight", CmdRotateRight}, {"print", CmdPrint},
+ {"highlightSelection", CmdCreateAnnotHighlight},
+ {"addAnnotation", CmdCreateAnnotText}, {"freeDraw", CmdCreateAnnotInk},
+ {"createAnnotUnderline", CmdCreateAnnotUnderline},
+ {"createAnnotStrikeOut", CmdCreateAnnotStrikeOut},
+ };
+
+ for (size_t i = 0; i < dimof(kCommandMap); ++i) {
+ if (str::EqI(commandName, kCommandMap[i].name)) {
+ return kCommandMap[i].cmdId;
+ }
+ }
return 0;
}
@@ -910,58 +907,90 @@ static bool DispatchToolbarReady() {
if (!win) {
return false;
}
+ win->hybridToolbarSyncCtrl = nullptr;
+ win->hybridToolbarSyncHasState = false;
SyncHybridToolbarButtonVisibility(win->hwndFrame, !win->IsCurrentTabAbout());
SyncHybridToolbarEditableAllowed(win->hwndFrame, !win->IsCurrentTabAbout());
SyncHybridToolbarTheme(win->hwndFrame);
if (win->ctrl) {
- SyncHybridToolbarPageState(win->hwndFrame, win->ctrl->CurrentPageNo(), win->ctrl->PageCount());
- SyncHybridToolbarZoomState(win->hwndFrame, win->ctrl->GetZoomVirtual(true));
+ SyncHybridToolbarState(win->hwndFrame, win->ctrl->CurrentPageNo(), win->ctrl->PageCount(),
+ win->ctrl->GetZoomVirtual(true), HybridToolbarCanAnnotate(win));
} else {
- SyncHybridToolbarPageState(win->hwndFrame, 1, 1);
- SyncHybridToolbarZoomState(win->hwndFrame, 100.0f);
+ SyncHybridToolbarState(win->hwndFrame, 1, 1, 100.0f, HybridToolbarCanAnnotate(win));
}
- // ensure annotation availability is sent when the toolbar becomes ready
- SyncHybridToolbarAnnotationAvailability(win->hwndFrame);
return true;
}
-// Helper to escape string for JSON
-static void AppendJsonString(char*& dst, size_t& remaining, const char* src) {
- *dst++ = '"';
- remaining--;
- while (*src && remaining > 1) {
- if (*src == '"' || *src == '\\') {
- if (remaining < 2) break;
- *dst++ = '\\';
- *dst++ = *src++;
- remaining -= 2;
- } else if (*src == '\n') {
- if (remaining < 2) break;
- *dst++ = '\\';
- *dst++ = 'n';
- src++;
- remaining -= 2;
- } else if (*src == '\r') {
- if (remaining < 2) break;
- *dst++ = '\\';
- *dst++ = 'r';
- src++;
- remaining -= 2;
- } else if (*src == '\t') {
- if (remaining < 2) break;
- *dst++ = '\\';
- *dst++ = 't';
- src++;
- remaining -= 2;
- } else {
- *dst++ = *src++;
- remaining--;
- }
+static void AppendJsonEscapedChar(StrBuilder& json, unsigned char c) {
+ switch (c) {
+ case '"':
+ json.Append("\\\"");
+ return;
+ case '\\':
+ json.Append("\\\\");
+ return;
+ case '\b':
+ json.Append("\\b");
+ return;
+ case '\f':
+ json.Append("\\f");
+ return;
+ case '\n':
+ json.Append("\\n");
+ return;
+ case '\r':
+ json.Append("\\r");
+ return;
+ case '\t':
+ json.Append("\\t");
+ return;
+ default:
+ break;
+ }
+ if (c < 0x20) {
+ static const char* kHex = "0123456789abcdef";
+ json.Append("\\u00");
+ json.AppendChar(kHex[(c >> 4) & 0x0f]);
+ json.AppendChar(kHex[c & 0x0f]);
+ return;
}
- if (remaining > 0) {
- *dst++ = '"';
- remaining--;
+ json.AppendChar((char)c);
+}
+
+static void AppendJsonString(StrBuilder& json, const char* src) {
+ json.AppendChar('"');
+ if (src) {
+ const unsigned char* p = (const unsigned char*)src;
+ while (*p) {
+ if (*p < 0x80) {
+ AppendJsonEscapedChar(json, *p++);
+ continue;
+ }
+
+ int runeLen = utf8RuneLen(p);
+ if (runeLen <= 0 || !isLegalUTF8Sequence(p, p + runeLen)) {
+ json.Append("\\uFFFD");
+ p++;
+ continue;
+ }
+
+ if (runeLen == 3 && p[0] == 0xE2 && p[1] == 0x80 && p[2] == 0xA8) {
+ json.Append("\\u2028");
+ p += 3;
+ continue;
+ }
+ if (runeLen == 3 && p[0] == 0xE2 && p[1] == 0x80 && p[2] == 0xA9) {
+ json.Append("\\u2029");
+ p += 3;
+ continue;
+ }
+ for (int i = 0; i < runeLen; ++i) {
+ AppendJsonEscapedChar(json, p[i]);
+ }
+ p += runeLen;
+ }
}
+ json.AppendChar('"');
}
static constexpr int kHomePageMaxRecentItems = 40;
@@ -983,17 +1012,8 @@ static u32 CalcRecentFilesSignature() {
// Helper function to serialize recent files to JSON for HomePage
static ::TempStr SerializeRecentFilesToJson() {
- // Allocate a large enough buffer for the JSON
- const size_t bufSize = 8192;
- char* buf = (char*)malloc(bufSize);
- if (!buf) return nullptr;
-
- char* dst = buf;
- size_t remaining = bufSize - 1; // Leave room for null terminator
-
- *dst++ = '[';
- remaining--;
-
+ StrBuilder json(4096);
+ json.AppendChar('[');
bool first = true;
for (int i = 0; i < kHomePageMaxRecentItems; i++) {
FileState* fs = gFileHistory.Get(i);
@@ -1004,9 +1024,8 @@ static ::TempStr SerializeRecentFilesToJson() {
continue;
}
- if (!first && remaining > 0) {
- *dst++ = ',';
- remaining--;
+ if (!first) {
+ json.AppendChar(',');
}
first = false;
@@ -1019,43 +1038,15 @@ static ::TempStr SerializeRecentFilesToJson() {
}
}
- // Build the JSON entry
- const char* entryStart = "{\"path\":";
- if (remaining > str::Len(entryStart)) {
- memcpy(dst, entryStart, str::Len(entryStart));
- dst += str::Len(entryStart);
- remaining -= str::Len(entryStart);
- }
-
- AppendJsonString(dst, remaining, filePath);
-
- const char* separator = ",\"name\":";
- if (remaining > str::Len(separator)) {
- memcpy(dst, separator, str::Len(separator));
- dst += str::Len(separator);
- remaining -= str::Len(separator);
- }
-
- AppendJsonString(dst, remaining, fileName);
-
- if (remaining > 0) {
- *dst++ = '}';
- remaining--;
- }
- }
-
- if (remaining > 0) {
- *dst++ = ']';
- remaining--;
- }
-
- if (remaining > 0) {
- *dst++ = '\0';
- } else {
- buf[bufSize - 1] = '\0';
+ json.Append("{\"path\":");
+ AppendJsonString(json, filePath);
+ json.Append(",\"name\":");
+ AppendJsonString(json, fileName);
+ json.AppendChar('}');
}
- return buf;
+ json.AppendChar(']');
+ return json.StealData();
}
// HomePage UI message handlers
diff --git a/src/utils/StrconvUtil.cpp b/src/utils/StrconvUtil.cpp
index f1ad8ffce26a..2ac5f920ee2f 100644
--- a/src/utils/StrconvUtil.cpp
+++ b/src/utils/StrconvUtil.cpp
@@ -154,17 +154,20 @@ TempStr UnknownToUtf8Temp(const char* s, size_t cb) {
}
if (str::StartsWith(s, UTF16BE_BOM)) {
- // convert from utf16 big endian to utf16
+ // convert from utf16 big endian to utf16 (swap bytes)
s += 2;
- WCHAR* ws = str::CastToWCHAR(s);
+ const WCHAR* ws = str::CastToWCHAR(s);
int n = str::Leni(ws);
WCHAR* tmpW = str::DupTemp(ws, n + 1);
- char* tmp = (char*)tmpW;
+ if (!tmpW) {
+ return nullptr;
+ }
+ unsigned char* bytes = reinterpret_cast(tmpW);
for (int i = 0; i < n; i++) {
int idx = i * 2;
- std::swap(tmp[idx], tmp[idx + 1]);
+ std::swap(bytes[idx], bytes[idx + 1]);
}
- return ToUtf8Temp((WCHAR*)tmp);
+ return ToUtf8Temp(tmpW);
}
// if s is valid utf8, leave it alone