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 @@
-
-
-
-
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());
}