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 - - - - - - -
-
-
- -
-
SumatraPDF
PrettySumatraPDF
Focused reading
-
-
- -
- - -1 - -
-
- -
-
- - -
-
- -
- -
-
-
-
- - -
-
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- -
- - - - - - - -
-
-
-
-
-
- - - - -
-
-
-
- - - -
- -
-
-
- -
- - - - - - - \ No newline at end of file + + + + + + +PrettySumatraPDF + + + + + + +
+
+
+ +
+
SumatraPDF
PrettySumatraPDF
Focused reading
+
+
+ +
+ + +1 + +
+
+ +
+
+ + +
+
+ +
+ +
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ + + + + + + +
+
+
+
+
+
+ + + + +
+
+
+
+ + + +
+ +
+
+
+ +
+ + + + + + + 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