diff --git a/prettysumatra/example.html b/prettysumatra/example.html deleted file mode 100644 index ffa4316d809a..000000000000 --- a/prettysumatra/example.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - Editor con Estilo Heroicons - - - - - -
- - - - - - - - - - - - - - - - -
- - - \ No newline at end of file diff --git a/prettysumatra/webui/home.html b/prettysumatra/webui/home.html index dfdf927d0778..7f4d61124958 100644 --- a/prettysumatra/webui/home.html +++ b/prettysumatra/webui/home.html @@ -72,30 +72,39 @@ - +
PrettySumatraPDF
@@ -238,11 +256,103 @@ renderRecentFiles(window._recentFiles); }; -window.applyTheme = function(isDark) { +function hexToRgb(hex) { + if (!hex) return null; + const clean = String(hex).trim().replace(/^#/, ''); + if (clean.length !== 6) return null; + const value = parseInt(clean, 16); + if (Number.isNaN(value)) return null; + return { + r: (value >> 16) & 255, + g: (value >> 8) & 255, + b: value & 255, + }; +} + +function rgbaFromHex(hex, alpha) { + const rgb = hexToRgb(hex); + if (!rgb) return ''; + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`; +} + +window.applyHomePageTheme = function(themePayload) { + if (!themePayload) return; + const root = document.documentElement; + const accent = themePayload.accent || themePayload.brandPrimary || '#eab308'; + const brandPrimary = themePayload.brandPrimary || accent; + const brandGlow = themePayload.brandGlow || brandPrimary; + const shadow = themePayload.shadow || '#000000'; + // Apply dynamic colors from theme payload (all except brand, which stays stable) + if (themePayload.canvas) root.style.setProperty('--canvas', themePayload.canvas); + if (themePayload.panel) root.style.setProperty('--panel', themePayload.panel); + if (themePayload.panel2) root.style.setProperty('--panel2', themePayload.panel2); + if (themePayload.stroke) root.style.setProperty('--stroke', themePayload.stroke); + if (themePayload.text) root.style.setProperty('--text', themePayload.text); + if (themePayload.muted) root.style.setProperty('--muted', themePayload.muted); + if (themePayload.btn) root.style.setProperty('--btn', themePayload.btn); + root.style.setProperty('--accent', accent); + root.style.setProperty('--brand-primary', brandPrimary); + root.style.setProperty('--brand-glow', rgbaFromHex(brandGlow, themePayload.appDark ? 0.28 : 0.18) || brandGlow); + root.style.setProperty('--brand-gradient', `linear-gradient(135deg, ${brandPrimary} 0%, ${accent} 100%)`); + root.style.setProperty('--shadow-sm', `0 2px 8px ${rgbaFromHex(shadow, themePayload.appDark ? 0.3 : 0.08) || 'rgba(0,0,0,0.12)'}`); + root.style.setProperty('--shadow-md', `0 8px 32px ${rgbaFromHex(shadow, themePayload.appDark ? 0.4 : 0.12) || 'rgba(0,0,0,0.18)'}`); + root.style.setProperty('--shadow-hover', `0 12px 40px ${rgbaFromHex(brandPrimary, themePayload.appDark ? 0.24 : 0.15) || 'rgba(0,0,0,0.18)'}`); + // Set data-theme attribute for media queries and dark mode detection + root.dataset.theme = themePayload.appDark ? 'dark' : 'light'; +}; + +// Legacy support: only set data-theme +window.applyTheme = function(isDark) { document.documentElement.dataset.theme = isDark ? 'dark' : 'light'; }; if (window.external && typeof window.external.invoke === 'function') window.external.invoke(JSON.stringify({ type: 'command', name: 'homePageReady', payload: {}, ts: Date.now() })); +// If theme payload was already injected via C++, apply it immediately +if (window.__homePageThemePayload) { window.applyHomePageTheme(window.__homePageThemePayload); } + +// Hide toolbar buttons +(function() { + try { + // Signal via localStorage that toolbar buttons should be hidden + try { + localStorage.setItem('prettysumatra_hide_toolbar', 'true'); + } catch (e) { + // Silently fail if localStorage not available + } + + // Send command to C++ to hide toolbar buttons on homepage + if (window.external && typeof window.external.invoke === 'function') { + window.external.invoke(JSON.stringify({ type: 'command', name: 'hideToolbarButtons', payload: {}, ts: Date.now() })); + } + + // Try to find and call hideToolbarButtons from accessible frames/windows + setTimeout(() => { + if (window.parent && window.parent !== window) { + if (typeof window.parent.hideToolbarButtons === 'function') { + window.parent.hideToolbarButtons(); + } + } + // Also try window.top + if (window.top && window.top !== window && typeof window.top.hideToolbarButtons === 'function') { + window.top.hideToolbarButtons(); + } + + // Try to access all iframes in the document + const iframes = document.querySelectorAll('iframe'); + iframes.forEach(iframe => { + try { + if (iframe.contentWindow && typeof iframe.contentWindow.hideToolbarButtons === 'function') { + iframe.contentWindow.hideToolbarButtons(); + } + } catch (e) { + // Silently ignore cross-origin iframes + } + }); + }, 100); + } catch (err) { + // Silently fail if cross-origin + } +})(); + + + \ No newline at end of file diff --git a/src/EngineCreate.cpp b/src/EngineCreate.cpp index 7d4be8643ec0..5faa10ac68ed 100644 --- a/src/EngineCreate.cpp +++ b/src/EngineCreate.cpp @@ -250,6 +250,26 @@ static EngineBase* CreateEngineForKind(Kind kind, Kind contentHintKind, const ch EngineBase* CreateEngineFromFile(const char* path, PasswordUI* pwdUI, bool enableChmEngine) { ReportIf(!path); + if (str::EndsWithI(path, ".p7m")) { + ByteSlice fileData = file::ReadFile(path); + ByteSlice extracted = ExtractP7m(fileData); + fileData.Free(); + if (!extracted.empty()) { + Kind kind = GuessFileTypeFromContent(extracted); + if (kind == kindFilePDF) { + EngineBase* engine = CreateEngineMupdfFromData(extracted, "file.pdf", pwdUI); + extracted.Free(); + if (engine) { + engine->SetFilePath(path); + engine->disableAntiAlias = gGlobalPrefs->disableAntiAlias; + return engine; + } + } else { + extracted.Free(); + } + } + } + // try to open with the engine guess from file name; if that fails, // guess the file type from content (one disk read inside // GuessFileTypeFromContent) and retry. diff --git a/src/HomePage.cpp b/src/HomePage.cpp index 5bebb7a9dc0e..f68e69c9f500 100644 --- a/src/HomePage.cpp +++ b/src/HomePage.cpp @@ -35,13 +35,14 @@ #include "AppSettings.h" #include "OverlayScrollbar.h" #include "DarkModeSubclass.h" - #include "wingui/WebView.h" #include #include #include #include #include "Toolbar.h" + +#include "prettysumatra/BridgeDispatcher.h" #pragma comment(lib, "shlwapi.lib") #define HOMEPAGE_HTML_PATH "..\\prettysumatra\\webui\\home.html" @@ -159,6 +160,9 @@ void HomePageShow(MainWindow* win) { SetWindowPos(hwndWV, nullptr, rc.x, rc.y, rc.dx, rc.dy, SWP_NOZORDER | SWP_SHOWWINDOW); win->homePageWebView->SetControllerVisible(true); win->homePageWebView->UpdateWebviewSize(); + + prettysumatra::bridge::SyncHybridToolbarButtonVisibility(win->hwndFrame, false); + prettysumatra::bridge::SyncHybridToolbarEditableAllowed(win->hwndFrame, false); } #ifndef ABOUT_USE_LESS_COLORS @@ -1172,6 +1176,8 @@ void HomePageHide(MainWindow* win) { return; } + prettysumatra::bridge::SyncHybridToolbarButtonVisibility(win->hwndFrame, true); + prettysumatra::bridge::SyncHybridToolbarEditableAllowed(win->hwndFrame, true); win->homePageWebView->SetControllerVisible(false); ShowWindow(win->homePageWebView->hwnd, SW_HIDE); } diff --git a/src/Installer.h b/src/Installer.h index 63832ada9d6b..ec0bb013e4bf 100644 --- a/src/Installer.h +++ b/src/Installer.h @@ -92,3 +92,5 @@ bool WriteExtendedFileExtensionInfo(HKEY hkey, const char* installedExePat); bool RemoveUninstallerRegistryInfo(HKEY hkey); void RemoveInstallRegistryKeys(HKEY hkey); int GetInstallerWinDx(); + +void ReRegisterFileAssociations(); diff --git a/src/RegistryInstaller.cpp b/src/RegistryInstaller.cpp index 39e028e44313..ef77d4217621 100644 --- a/src/RegistryInstaller.cpp +++ b/src/RegistryInstaller.cpp @@ -32,6 +32,22 @@ static void ShellNotifyAssociationsChanged() { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); } +static bool HasRegistryValue(HKEY hkey, const char* keyName, const char* valName) { + WCHAR* keyW = ToWStrTemp(keyName); + WCHAR* valW = ToWStrTemp(valName); + DWORD type = 0; + DWORD cb = 0; + LSTATUS res = SHGetValueW(hkey, keyW, valW, &type, nullptr, &cb); + // success or ERROR_MORE_DATA means the value exists + return (res == ERROR_SUCCESS || res == ERROR_MORE_DATA); +} + +static bool HasOurOpenWithEntry(HKEY hkey, const char* ext) { + TempStr key = str::JoinTemp("Software\\Classes\\", ext, "\\OpenWithProgids"); + TempStr progID = str::JoinTemp(kAppName, ext); + return HasRegistryValue(hkey, key, progID); +} + // caller needs to str::Free() the result static char* GetInstallDate() { SYSTEMTIME st; @@ -635,3 +651,44 @@ void RemoveInstallRegistryKeys(HKEY hkey) { ShellNotifyAssociationsChanged(); } + +// re-register our "Open With" file association handlers (under OpenWithProgids +// and the corresponding ProgID entries) if this is an installed (non-portable) +// copy of SumatraPDF. We do this at startup to counter other apps (e.g. Microsoft +// Edge) that might remove us from the "Open with" context menu for .pdf etc. files. +// We only touch HKCU (always writable by the current user) and optionally HKLM +// (for all-users installs; fails gracefully without admin rights). +void ReRegisterFileAssociations() { + if (!IsOurExeInstalled()) { + return; + } + TempStr exePath = GetSelfExePathTemp(); + if (str::IsEmpty(exePath)) { + return; + } + + bool didRegister = false; + if (!HasOurOpenWithEntry(HKEY_CURRENT_USER, ".pdf")) { + RegisterForOpenWith(HKEY_CURRENT_USER, exePath); + if (IsWindows10OrGreater()) { + RegisterForDefaultPrograms(HKEY_CURRENT_USER, exePath); + } + didRegister = true; + } + + // for all-users installs, also try to restore the HKLM entries (best effort) + TempStr regPathUninst = GetRegPathUninstTemp(kAppName); + if (HasRegistryValue(HKEY_LOCAL_MACHINE, regPathUninst, "InstallLocation")) { + if (!HasOurOpenWithEntry(HKEY_LOCAL_MACHINE, ".pdf")) { + RegisterForOpenWith(HKEY_LOCAL_MACHINE, exePath); + if (IsWindows10OrGreater()) { + RegisterForDefaultPrograms(HKEY_LOCAL_MACHINE, exePath); + } + didRegister = true; + } + } + + if (didRegister) { + ShellNotifyAssociationsChanged(); + } +} diff --git a/src/Settings.h b/src/Settings.h index 8af05881ce98..241c86ae0ab0 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -257,9 +257,24 @@ struct Theme { // control background color char* controlBackgroundColor; ParsedColor controlBackgroundColorParsed; + // toolbar background color + char* ToolbarBackgroundColor; + ParsedColor ToolbarBackgroundColorParsed; // link color char* linkColor; ParsedColor linkColorParsed; + // accent color used by the web UI + char* accentColor; + ParsedColor accentColorParsed; + // brand primary color used by the web UI + char* brandPrimaryColor; + ParsedColor brandPrimaryColorParsed; + // glow color used by the web UI + char* brandGlowColor; + ParsedColor brandGlowColorParsed; + // shadow base color used by the web UI + char* shadowColor; + ParsedColor shadowColorParsed; // should we colorize Windows controls and window areas bool colorizeControls; }; @@ -743,12 +758,18 @@ static const FieldInfo gThemeFields[] = { {offsetof(Theme, textColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, backgroundColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, controlBackgroundColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, ToolbarBackgroundColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, linkColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, accentColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, brandPrimaryColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, brandGlowColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, shadowColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, colorizeControls), SettingType::Bool, false}, }; static const StructInfo gThemeInfo = { - sizeof(Theme), 6, gThemeFields, - "Name\0TextColor\0BackgroundColor\0ControlBackgroundColor\0LinkColor\0ColorizeControls"}; + sizeof(Theme), 11, gThemeFields, + "Name\0TextColor\0BackgroundColor\0ControlBackgroundColor\0ToolbarBackgroundColor\0LinkColor\0AccentColor\0" + "BrandPrimaryColor\0BrandGlowColor\0ShadowColor\0ColorizeControls"}; static const FieldInfo gTabFileFields[] = { {offsetof(TabFile, path), SettingType::String, (intptr_t)""}, @@ -978,11 +999,16 @@ static const FieldInfo gTheme_1_Fields[] = { {offsetof(Theme, backgroundColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, controlBackgroundColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, linkColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, accentColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, brandPrimaryColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, brandGlowColor), SettingType::Color, (intptr_t)""}, + {offsetof(Theme, shadowColor), SettingType::Color, (intptr_t)""}, {offsetof(Theme, colorizeControls), SettingType::Bool, false}, }; static const StructInfo gTheme_1_Info = { - sizeof(Theme), 6, gTheme_1_Fields, - "Name\0TextColor\0BackgroundColor\0ControlBackgroundColor\0LinkColor\0ColorizeControls"}; + sizeof(Theme), 10, gTheme_1_Fields, + "Name\0TextColor\0BackgroundColor\0ControlBackgroundColor\0LinkColor\0AccentColor\0BrandPrimaryColor\0" + "BrandGlowColor\0ShadowColor\0ColorizeControls"}; static const FieldInfo gThemesFields[] = { {offsetof(Themes, themes), SettingType::Array, (intptr_t)&gTheme_1_Info}, diff --git a/src/SumatraPDF.cpp b/src/SumatraPDF.cpp index c63868dbe6d3..870390eae83c 100644 --- a/src/SumatraPDF.cpp +++ b/src/SumatraPDF.cpp @@ -120,7 +120,7 @@ struct ToolbarHeightBreakpoint { }; static const ToolbarHeightBreakpoint kToolbarHeightBreakpoints[] = { - {1700, 56}, {1300, 56}, {1000, 52}, {650, 44}, {0, 44}, // default fallback + {1700, 57}, {1300, 57}, {1000, 52}, {650, 44}, {0, 44}, // default fallback }; static int GetHybridToolbarHeight(HWND hwndFrame) { @@ -4084,7 +4084,7 @@ static TempWStr GetFileFilterTemp() { const char* filter; bool available; } fileFormats[] = { - {_TRA("PDF documents"), "*.pdf", true}, + {_TRA("PDF documents"), "*.pdf;*.p7m", true}, {_TRA("XPS documents"), "*.xps;*.oxps", true}, {_TRA("DjVu documents"), "*.djvu", true}, {_TRA("Postscript documents"), "*.ps;*.eps", IsEnginePsAvailable()}, diff --git a/src/SumatraStartup.cpp b/src/SumatraStartup.cpp index 50f640045cd7..5b6e1a248c33 100644 --- a/src/SumatraStartup.cpp +++ b/src/SumatraStartup.cpp @@ -1520,6 +1520,7 @@ int APIENTRY WinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE, _In_ LPST } DetectExternalViewers(); + ReRegisterFileAssociations(); gRenderCache = new RenderCache(); diff --git a/src/Theme.cpp b/src/Theme.cpp index f036e129c5a4..96370a54f961 100644 --- a/src/Theme.cpp +++ b/src/Theme.cpp @@ -41,99 +41,147 @@ constexpr COLORREF kRedColor = RgbToCOLORREF(0xff0000); static const char* themesTxt = R"(Themes [ [ - Name = Light - TextColor = #000000 - BackgroundColor = #f2f2f2 + Name = Sumatra Light + TextColor = #1f2937 + BackgroundColor = #f4f6f8 ControlBackgroundColor = #ffffff LinkColor = #eab308 - ColorizeControls = false + AccentColor = #eab308 + BrandPrimaryColor = #facc15 + BrandGlowColor = #fde68a + ShadowColor = #5b4210 + ColorizeControls = true ] [ - Name = Dark from 3.5 - TextColor = #bac9d0 - BackgroundColor = #263238 - ControlBackgroundColor = #263238 - LinkColor = #facc15 + Name = Sumatra Dark + TextColor = #f8fafc + BackgroundColor = #17130a + ControlBackgroundColor = #3a2901 + LinkColor = #eab308 + AccentColor = #eab308 + BrandPrimaryColor = #facc15 + BrandGlowColor = #fde68a + ShadowColor = #120b02 ColorizeControls = true ] [ - Name = Darker - TextColor = #c3c3c6 - BackgroundColor = #2d2d30 - ControlBackgroundColor = #2d2d30 - LinkColor = #9999a0 + Name = Modern Slate Light + TextColor = #243041 + BackgroundColor = #f4f6f8 + ControlBackgroundColor = #e0f0ee + LinkColor = #0f766e + AccentColor = #0f766e + BrandPrimaryColor = #14b8a6 + BrandGlowColor = #99f6e4 + ShadowColor = #1e293b ColorizeControls = true ] [ - Name = Dark - TextColor = #F9FAFB - BackgroundColor = #000000 - ControlBackgroundColor = #000000 - LinkColor = #6B7280 + Name = Modern Slate Dark + TextColor = #dbe4ee + BackgroundColor = #0f172a + ControlBackgroundColor = #05557a + LinkColor = #38bdf8 + AccentColor = #38bdf8 + BrandPrimaryColor = #0ea5e9 + BrandGlowColor = #7dd3fc + ShadowColor = #020617 ColorizeControls = true ] [ - Name = Dark background Bright text - TextColor = #ffffff - BackgroundColor = #2d2d30 - ControlBackgroundColor = #2d2d30 - LinkColor = #9999a0 + Name = Modern Blue Light + TextColor = #1e293b + BackgroundColor = #f8fafc + ControlBackgroundColor = #daebff + LinkColor = #0f62fe + AccentColor = #0f62fe + BrandPrimaryColor = #2563eb + BrandGlowColor = #93c5fd + ShadowColor = #1e293b ColorizeControls = true ] [ - Name = Solarized Light - TextColor = #212323 - BackgroundColor = #fdf6e3 - ControlBackgroundColor = #eee8d5 - LinkColor = #9999a0 + Name = Modern Blue Dark + TextColor = #e2e8f0 + BackgroundColor = #0b1120 + ControlBackgroundColor = #1c3d74 + LinkColor = #60a5fa + AccentColor = #60a5fa + BrandPrimaryColor = #3b82f6 + BrandGlowColor = #93c5fd + ShadowColor = #020617 ColorizeControls = true ] [ - Name = Solarized Dark - TextColor = #839496 - BackgroundColor = #002b36 - ControlBackgroundColor = #073642 - LinkColor = #268bd2 + Name = Modern Green Light + TextColor = #1f2937 + BackgroundColor = #f4f6f8 + ControlBackgroundColor = #d1fae5 + LinkColor = #16a34a + AccentColor = #16a34a + BrandPrimaryColor = #22c55e + BrandGlowColor = #86efac + ShadowColor = #1f2937 ColorizeControls = true ] [ - Name = Dracula - TextColor = #f8f8f2 - BackgroundColor = #282a36 - ControlBackgroundColor = #44475a - LinkColor = #8be9fd + Name = Modern Green Dark + TextColor = #e5f3ea + BackgroundColor = #10251b + ControlBackgroundColor = #0f5529 + LinkColor = #4ade80 + AccentColor = #4ade80 + BrandPrimaryColor = #22c55e + BrandGlowColor = #86efac + ShadowColor = #052e16 ColorizeControls = true ] [ - Name = Nebula - TextColor = #CBE3E7 - BackgroundColor = #100E23 - ControlBackgroundColor = #1E1C31 - LinkColor = #91DDFF + Name = Modern Purple Light + TextColor = #29223a + BackgroundColor = #f4f6f8 + ControlBackgroundColor = #f3e8ff + LinkColor = #7c3aed + AccentColor = #7c3aed + BrandPrimaryColor = #8b5cf6 + BrandGlowColor = #c4b5fd + ShadowColor = #29223a ColorizeControls = true ] [ - Name = Greeny - TextColor = #FDD085 - BackgroundColor = #4F6232 - ControlBackgroundColor = #1E3304 - LinkColor = #A2E53B + Name = Modern Purple Dark + TextColor = #ede9fe + BackgroundColor = #1c122b + ControlBackgroundColor = rgb(78, 69, 88) + LinkColor = #c084fc + AccentColor = #c084fc + BrandPrimaryColor = #a855f7 + BrandGlowColor = #ddd6fe + ShadowColor = #15051f ColorizeControls = true ] [ - Name = Choco - TextColor = #D7AD62 - BackgroundColor = #2A1104 - ControlBackgroundColor = #172736 - LinkColor = #E8CD12 + Name = Modern Amber Light + TextColor = #31251a + BackgroundColor = #f4f6f8 + ControlBackgroundColor = #fff7ed + LinkColor = #d97706 + AccentColor = #d97706 + BrandPrimaryColor = #f59e0b + BrandGlowColor = #fde68a + ShadowColor = #31251a ColorizeControls = true ] [ - Name = Purpy - TextColor = #E2C3C3 - BackgroundColor = #20222A - ControlBackgroundColor = #1E0126 - LinkColor = #EFF0B8 + Name = Modern Amber Dark + TextColor = #f9ede1 + BackgroundColor = #23160d + ControlBackgroundColor = #634106 + LinkColor = #fbbf24 + AccentColor = #fbbf24 + BrandPrimaryColor = #f59e0b + BrandGlowColor = #fde68a + ShadowColor = #120b02 ColorizeControls = true ] ] @@ -152,6 +200,59 @@ static Theme* gCurrentTheme = nullptr; static Theme* gThemeLight = nullptr; static Themes* gParsedThemes = nullptr; +static bool IsLightThemeVariantName(const char* name) { + if (str::IsEmpty(name)) { + return false; + } + return str::EqI(name, "Light") || str::EndsWithI(name, " Light"); +} + +static int GetThemeIndexByName(const char* name) { + for (int i = 0; i < gThemeCount; i++) { + Theme* theme = gThemes->At(i); + if (str::EqI(theme->name, name)) { + return i; + } + } + return -1; +} + +static int GetThemeVariantIndex(const char* currentName, bool targetDark) { + if (str::IsEmpty(currentName)) { + return -1; + } + + if (str::EqI(currentName, "Light")) { + return targetDark ? GetThemeIndexByName("Dark") : GetThemeIndexByName("Light"); + } + if (str::EqI(currentName, "Dark")) { + return targetDark ? GetThemeIndexByName("Dark") : GetThemeIndexByName("Light"); + } + + const char* lightSuffix = " Light"; + const char* darkSuffix = " Dark"; + if (!str::EndsWithI(currentName, lightSuffix) && !str::EndsWithI(currentName, darkSuffix)) { + return GetThemeIndexByName(targetDark ? "Dark" : "Light"); + } + + 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) { + 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); +} + bool IsCurrentThemeDefault() { return gCurrThemeIndex == 0; } @@ -161,19 +262,21 @@ bool PrettyStyleEnabled() { } COLORREF PrettySurfaceColor() { - return ThemeWindowControlBackgroundColor(); + return ThemeWindowBackgroundColor(); } COLORREF PrettySurfaceAltColor() { - return ThemeWindowBackgroundColor(); + return ThemeWindowControlBackgroundColor(); } COLORREF PrettyBorderColor() { - return ThemeWindowTextDisabledColor(); + // Make the border slightly darker than the window background so it stays subtle + // but still separates the toolbar from the canvas in both light and dark themes. + return AdjustLightness2(ThemeWindowControlBackgroundColor(), -10); } COLORREF PrettyAccentColor() { - return ThemeWindowLinkColor(); + return ThemeAccentColor(); } void FreeThemes() { @@ -221,6 +324,9 @@ void CreateThemeCommands() { void SetThemeByIndex(int themeIdx) { ReportIf((themeIdx < 0) || (themeIdx >= gThemeCount)); + if (themeIdx < 0) { + themeIdx = 0; + } if (themeIdx >= gThemeCount) { themeIdx = 0; } @@ -274,15 +380,21 @@ void SelectNextTheme() { SetThemeByIndex(newIdx); } +bool SetThemeVariant(bool targetDark) { + if (gThemeCount <= 0 || gCurrentTheme == nullptr) { + return false; + } + int idx = GetThemeVariantIndex(gCurrentTheme->name, targetDark); + if (idx < 0) { + return false; + } + SetThemeByIndex(idx); + return true; +} + // not case sensitive static int GetThemeByName(const char* name) { - for (int i = 0; i < gThemeCount; i++) { - Theme* theme = gThemes->At(i); - if (str::EqI(theme->name, name)) { - return i; - } - } - return -1; + return GetThemeIndexByName(name); } // this is the default aggressive yellow that we suppress @@ -340,7 +452,7 @@ COLORREF ThemeDocumentColors(COLORREF& bg) { COLORREF text = ThemeWindowTextColor(); bg = ThemeMainWindowBackgroundColor(); - if (gCurrThemeIndex < 3) { + if (IsLightThemeVariantName(gCurrentTheme->name)) { bg = AccentColor(bg, 8); } return text; @@ -375,7 +487,7 @@ COLORREF ThemePageRenderColors(COLORREF& bg) { } // default colors - if (gCurrentTheme == gThemeLight) { + if (IsLightThemeVariantName(gCurrentTheme->name)) { std::swap(text, bg); return text; } @@ -385,7 +497,7 @@ COLORREF ThemePageRenderColors(COLORREF& bg) { text = ThemeWindowTextColor(); bg = ThemeMainWindowBackgroundColor(); - if (gCurrThemeIndex < 3) { + if (IsLightThemeVariantName(gCurrentTheme->name)) { bg = AccentColor(bg, 8); } return text; @@ -440,6 +552,26 @@ COLORREF ThemeWindowLinkColor() { return col; } +COLORREF ThemeAccentColor() { + auto col = GetThemeCol(gCurrentTheme->accentColor, ThemeWindowLinkColor()); + return col; +} + +COLORREF ThemeBrandPrimaryColor() { + auto col = GetThemeCol(gCurrentTheme->brandPrimaryColor, ThemeAccentColor()); + return col; +} + +COLORREF ThemeBrandGlowColor() { + auto col = GetThemeCol(gCurrentTheme->brandGlowColor, ThemeBrandPrimaryColor()); + return col; +} + +COLORREF ThemeShadowColor() { + auto col = GetThemeCol(gCurrentTheme->shadowColor, kColBlack); + return col; +} + COLORREF ThemeNotificationsBackgroundColor() { auto col = ThemeWindowBackgroundColor(); return AdjustLightness2(col, 10); @@ -450,7 +582,20 @@ COLORREF ThemeNotificationsTextColor() { } COLORREF ThemeNotificationsProgressColor() { - return ThemeWindowLinkColor(); + return ThemeAccentColor(); +} + +// Highlight color used for notifications (background) +COLORREF ThemeNotificationsHighlightColor() { + // Use accent color brightened a bit for highlight background + COLORREF base = ThemeAccentColor(); + return AdjustLightness2(base, 24); +} + +// Text color to use on top of notification highlight background +COLORREF ThemeNotificationsHighlightTextColor() { + COLORREF bg = ThemeNotificationsHighlightColor(); + return IsLightColor(bg) ? kColBlack : kColWhite; } bool ThemeColorizeControls() { diff --git a/src/Theme.h b/src/Theme.h index 37bc96607504..41e3af5d72f1 100644 --- a/src/Theme.h +++ b/src/Theme.h @@ -3,6 +3,7 @@ License: GPLv3 */ void SetTheme(const char* name); void SetCurrentThemeFromSettings(); +bool SetThemeVariant(bool targetDark); void SelectNextTheme(); void CreateThemeCommands(); @@ -15,6 +16,10 @@ COLORREF ThemeWindowTextColor(); COLORREF ThemeWindowTextDisabledColor(); COLORREF ThemeWindowControlBackgroundColor(); COLORREF ThemeWindowLinkColor(); +COLORREF ThemeAccentColor(); +COLORREF ThemeBrandPrimaryColor(); +COLORREF ThemeBrandGlowColor(); +COLORREF ThemeShadowColor(); COLORREF ThemeNotificationsBackgroundColor(); COLORREF ThemeNotificationsTextColor(); COLORREF ThemeNotificationsHighlightColor(); diff --git a/src/prettysumatra/BridgeDispatcher.cpp b/src/prettysumatra/BridgeDispatcher.cpp index 5f7f06a7048c..7e80afc1a288 100644 --- a/src/prettysumatra/BridgeDispatcher.cpp +++ b/src/prettysumatra/BridgeDispatcher.cpp @@ -111,10 +111,6 @@ static MainWindow* FindWindowForFrame(HWND hwndFrame) { return FindMainWindowByHwnd(hwndFrame); } -static bool HomePageUsesDarkTheme() { - return DarkMode::isColorDark(ThemeMainWindowBackgroundColor()); -} - static TempStr HybridToolbarThemeJs(HWND hwndFrame) { MainWindow* win = FindWindowForFrame(hwndFrame); if (!win || !win->hybridToolbar) { @@ -122,90 +118,93 @@ static TempStr HybridToolbarThemeJs(HWND hwndFrame) { } COLORREF canvas = ThemeMainWindowBackgroundColor(); - COLORREF panel = ThemeMainWindowBackgroundColor(); + COLORREF panelOrig = ThemeWindowControlBackgroundColor(); + // Make toolbar background slightly lighter than the main window background + COLORREF panel = AdjustLightness2(panelOrig, 2); COLORREF panel2 = PrettyStyleEnabled() ? PrettySurfaceColor() : ThemeWindowControlBackgroundColor(); COLORREF stroke = PrettyBorderColor(); COLORREF text = ThemeWindowTextColor(); COLORREF muted = ThemeWindowTextDisabledColor(); - COLORREF btn = ThemeControlBackgroundColor(); + COLORREF btn = AdjustLightness2(panelOrig, 10); COLORREF accent = PrettyAccentColor(); - // Keep toolbar brand colors stable (gold) instead of following system accent (often blue). - COLORREF brand1 = RGB(248, 204, 24); - COLORREF brand2 = RGB(215, 167, 0); - bool appDark = DarkMode::isColorDark(panel); + COLORREF brandPrimary = ThemeBrandPrimaryColor(); + COLORREF brandGlow = ThemeBrandGlowColor(); + COLORREF shadow = ThemeShadowColor(); + // Determine appDark based on the original main window background + bool appDark = DarkMode::isColorDark(panelOrig); bool docInverted = gGlobalPrefs->fixedPageUI.invertColors; bool windowsDark = WindowsPrefersDarkModeForHybridToolbar(); bool followWindows = HybridThemeFollowsWindows(); return str::FormatTemp( "window.__hybridToolbarThemePayload={canvas:'%s',panel:'%s',panel2:'%s',stroke:'%s'," - "text:'%s',muted:'%s',btn:'%s',accent:'%s',brand1:'%s',brand2:'%s',appDark:%s,docInverted:%s,windowsDark:%s," - "followWindows:%s};" + "text:'%s',muted:'%s',btn:'%s',accent:'%s',brandPrimary:'%s',brandGlow:'%s',shadow:'%s',appDark:%s," + "docInverted:%s,windowsDark:%s,followWindows:%s};" "if(window.hybridToolbarApplyTheme){window.hybridToolbarApplyTheme(window.__hybridToolbarThemePayload);}", ColorToCssHex(canvas), ColorToCssHex(panel), ColorToCssHex(panel2), ColorToCssHex(stroke), ColorToCssHex(text), - ColorToCssHex(muted), ColorToCssHex(btn), ColorToCssHex(accent), ColorToCssHex(brand1), ColorToCssHex(brand2), - appDark ? "true" : "false", docInverted ? "true" : "false", windowsDark ? "true" : "false", - followWindows ? "true" : "false"); + ColorToCssHex(muted), ColorToCssHex(btn), ColorToCssHex(accent), ColorToCssHex(brandPrimary), + ColorToCssHex(brandGlow), ColorToCssHex(shadow), appDark ? "true" : "false", docInverted ? "true" : "false", + windowsDark ? "true" : "false", followWindows ? "true" : "false"); } -static const char* JsQuoted(const char* s) { - if (!s) { - static const char empty[] = {'\'', '\'', '\0'}; - return empty; - } +// Generate JavaScript to inject theme colors into home page +// Similar to HybridToolbarThemeJs but for home page +static TempStr HomePageThemeJs() { + COLORREF canvas = ThemeMainWindowBackgroundColor(); + COLORREF panel = ThemeMainWindowBackgroundColor(); + COLORREF panel2 = PrettyStyleEnabled() ? PrettySurfaceColor() : ThemeWindowControlBackgroundColor(); + COLORREF stroke = PrettyBorderColor(); + COLORREF text = ThemeWindowTextColor(); + COLORREF muted = ThemeWindowTextDisabledColor(); + COLORREF btn = ThemeControlBackgroundColor(); + COLORREF accent = PrettyAccentColor(); + COLORREF brandPrimary = ThemeBrandPrimaryColor(); + COLORREF brandGlow = ThemeBrandGlowColor(); + COLORREF shadow = ThemeShadowColor(); + bool appDark = DarkMode::isColorDark(panel); - // Calculate needed size - size_t len = 0; - for (const char* p = s; *p; ++p) { - switch (*p) { - case '\\': - case '\'': - case '\n': - case '\t': - len += 2; - break; - default: - len += 1; - break; - } - } - len += 2; // for surrounding quotes - len += 1; // for null terminator + // Build JavaScript payload with all theme colors + return str::FormatTemp( + "window.__homePageThemePayload={canvas:'%s',panel:'%s',panel2:'%s',stroke:'%s'," + "text:'%s',muted:'%s',btn:'%s',accent:'%s',brandPrimary:'%s',brandGlow:'%s',shadow:'%s',appDark:%s};" + "if(window.applyHomePageTheme){window.applyHomePageTheme(window.__homePageThemePayload);}", + ColorToCssHex(canvas), ColorToCssHex(panel), ColorToCssHex(panel2), ColorToCssHex(stroke), ColorToCssHex(text), + ColorToCssHex(muted), ColorToCssHex(btn), ColorToCssHex(accent), ColorToCssHex(brandPrimary), + ColorToCssHex(brandGlow), ColorToCssHex(shadow), appDark ? "true" : "false"); +} - char* out = (char*)malloc(len); - if (!out) return nullptr; +static TempStr JsQuoted(const char* s) { + if (!s) { + return str::FormatTemp("''"); + } - char* dst = out; - *dst++ = '\''; + StrBuilder sb; + sb.AppendChar('\''); for (const char* p = s; *p; ++p) { switch (*p) { case '\\': - *dst++ = '\\'; - *dst++ = '\\'; + sb.Append("\\\\"); break; case '\'': - *dst++ = '\\'; - *dst++ = '\''; + sb.Append("\\'"); break; case '\r': - break; // skip carriage returns + // skip + break; case '\n': - *dst++ = '\\'; - *dst++ = 'n'; + sb.Append("\\n"); break; case '\t': - *dst++ = '\\'; - *dst++ = 't'; + sb.Append("\\t"); break; default: - *dst++ = *p; + sb.AppendChar(*p); break; } } - *dst++ = '\''; - *dst = '\0'; + sb.AppendChar('\''); - return out; + return (TempStr)sb.StealData(); } static TempStr HybridToolbarTextJs(HWND hwndFrame) { @@ -288,6 +287,29 @@ void FocusHybridToolbarSearch(HWND hwndFrame) { win->hybridToolbar->Eval("window.hybridToolbarFocusSearch && window.hybridToolbarFocusSearch();"); } +void SyncHybridToolbarButtonVisibility(HWND hwndFrame, bool showButtons) { + MainWindow* win = FindWindowForFrame(hwndFrame); + if (!win || !win->hybridToolbar) { + return; + } + + const char* js = showButtons ? "window.showToolbarButtons && window.showToolbarButtons();" + : "window.hideToolbarButtons && window.hideToolbarButtons();"; + win->hybridToolbar->Eval(js); +} + +void SyncHybridToolbarEditableAllowed(HWND hwndFrame, bool allowed) { + MainWindow* win = FindWindowForFrame(hwndFrame); + if (!win || !win->hybridToolbar) { + return; + } + + const char* js = allowed + ? "window.hybridToolbarSetEditableAllowed && window.hybridToolbarSetEditableAllowed(true);" + : "window.hybridToolbarSetEditableAllowed && window.hybridToolbarSetEditableAllowed(false);"; + win->hybridToolbar->Eval(js); +} + void InitHybridToolbarTheme(HWND hwndFrame) { MainWindow* win = FindWindowForFrame(hwndFrame); if (!win || !win->hybridToolbar) { @@ -660,7 +682,9 @@ static bool WindowsPrefersDarkModeForHybrid() { } static void ApplyThemeState(bool targetDark) { - SetTheme(targetDark ? "Dark" : "Light"); + if (!SetThemeVariant(targetDark)) { + SetTheme(targetDark ? "Dark" : "Light"); + } SaveSettings(); } @@ -677,6 +701,7 @@ static bool DispatchToggleThemeFollowWindows() { MainWindow* win = GetTargetWindow(); if (win) { SyncHybridToolbarTheme(win->hwndFrame); + SyncHomePageTheme(win->hwndFrame); } return true; } @@ -692,6 +717,7 @@ static bool DispatchToggleThemeLightDark() { MainWindow* win = GetTargetWindow(); if (win) { SyncHybridToolbarTheme(win->hwndFrame); + SyncHomePageTheme(win->hwndFrame); } return true; } @@ -884,6 +910,8 @@ static bool DispatchToolbarReady() { if (!win) { return 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()); @@ -1051,10 +1079,11 @@ static bool DispatchHomePageReady() { char* js = str::FormatTemp("window.setRecentFiles && window.setRecentFiles(%s);", recentFilesJson); win->homePageWebView->Eval(js); - // Apply current theme - bool isDarkMode = HomePageUsesDarkTheme(); - char* themeJs = str::FormatTemp("window.applyTheme && window.applyTheme(%s);", isDarkMode ? "true" : "false"); - win->homePageWebView->Eval(themeJs); + // Apply current theme with full color payload + TempStr themeJs = HomePageThemeJs(); + if (themeJs) { + win->homePageWebView->Eval(themeJs); + } return true; } @@ -1165,10 +1194,10 @@ void SyncHomePageTheme(HWND hwndFrame) { if (!win || !win->homePageWebView) { return; } - // Apply the actual app theme so the home page stays in sync with manual theme changes. - bool isDarkMode = HomePageUsesDarkTheme(); - char* themeJs = str::FormatTemp("window.applyTheme && window.applyTheme(%s);", isDarkMode ? "true" : "false"); - win->homePageWebView->Eval(themeJs); + TempStr js = HomePageThemeJs(); + if (js) { + win->homePageWebView->Eval(js); + } } DispatchResult DispatchShellMessage(const char* msg) { diff --git a/src/prettysumatra/BridgeDispatcher.h b/src/prettysumatra/BridgeDispatcher.h index 6788eaf13bce..c075c97706ce 100644 --- a/src/prettysumatra/BridgeDispatcher.h +++ b/src/prettysumatra/BridgeDispatcher.h @@ -23,6 +23,8 @@ void InitHybridToolbarText(HWND hwndFrame); void SyncHybridToolbarText(HWND hwndFrame); void SyncHybridToolbarSearchText(HWND hwndFrame, const char* text); void FocusHybridToolbarSearch(HWND hwndFrame); +void SyncHybridToolbarButtonVisibility(HWND hwndFrame, bool showButtons); +void SyncHybridToolbarEditableAllowed(HWND hwndFrame, bool allowed); void SyncHybridToolbarPageState(HWND hwndFrame, int currentPage, int totalPages); void SyncHybridToolbarZoomState(HWND hwndFrame, float zoomPercent); void SyncHybridToolbarAnnotationAvailability(HWND hwndFrame); diff --git a/src/utils/CryptoUtil.cpp b/src/utils/CryptoUtil.cpp index df919ff73713..daafd27727f9 100644 --- a/src/utils/CryptoUtil.cpp +++ b/src/utils/CryptoUtil.cpp @@ -4,6 +4,8 @@ #include "utils/BaseUtil.h" #include "utils/CryptoUtil.h" +#include + #ifndef DWORD_MAX #define DWORD_MAX 0xffffffffUL #endif @@ -125,3 +127,38 @@ bool VerifySHA1Signature(const void* data, size_t dataLen, const char* hexSignat } return ok; } + +ByteSlice ExtractP7m(ByteSlice d) { + if (d.empty()) { + return {}; + } + const u8* data = d.data(); + DWORD dataLen = (DWORD)d.size(); + + HCRYPTMSG hMsg = CryptMsgOpenToDecode(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, 0, 0, nullptr, nullptr); + if (!hMsg) { + return {}; + } + + BOOL ok = CryptMsgUpdate(hMsg, data, dataLen, TRUE); + if (!ok) { + CryptMsgClose(hMsg); + return {}; + } + + DWORD cbContent = 0; + ok = CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, nullptr, &cbContent); + if (!ok || cbContent == 0) { + CryptMsgClose(hMsg); + return {}; + } + + u8* content = AllocArray(cbContent); + ok = CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, content, &cbContent); + CryptMsgClose(hMsg); + if (!ok) { + free(content); + return {}; + } + return {content, cbContent}; +} diff --git a/src/utils/CryptoUtil.h b/src/utils/CryptoUtil.h index cccbad40ca28..01f727dcaa4d 100644 --- a/src/utils/CryptoUtil.h +++ b/src/utils/CryptoUtil.h @@ -7,3 +7,6 @@ void CalcSHA2Digest(const void* data, int dataSize, u8 digest[32]); bool VerifySHA1Signature(const void* data, size_t dataLen, const char* hexSignature, const void* pubkey, size_t pubkeyLen); + +// extracts the content (e.g. PDF) from a PKCS#7 / .p7m wrapper using Win32 crypto APIs +ByteSlice ExtractP7m(ByteSlice d); diff --git a/src/utils/tests/CryptoUtil_ut.cpp b/src/utils/tests/CryptoUtil_ut.cpp index a3e0886974a5..cfc8c332b516 100644 --- a/src/utils/tests/CryptoUtil_ut.cpp +++ b/src/utils/tests/CryptoUtil_ut.cpp @@ -44,4 +44,9 @@ void CryptoUtilTest() { "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592")); utassert(TestDigestSHA2("The quick brown fox jumps over the lazy dog.", 44, "ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c")); + + // basic sanity for p7m extractor (no real p7m data, just ensure no crash and empty on bad input) + utassert(ExtractP7m(ByteSlice()).empty()); + utassert(ExtractP7m(ByteSlice((const u8*)"not a p7m", 9)).empty()); + utassert(ExtractP7m(ByteSlice((const u8*)"%PDF-1.4", 8)).empty()); }