Add the system's native menu to the titlebar, configurable

This commit is contained in:
Amelia Frost 2026-03-28 17:22:40 -07:00
parent efbba2a65f
commit 09662fc37e
No known key found for this signature in database
5 changed files with 109 additions and 3 deletions

View file

@ -0,0 +1,57 @@
(function () {
if (window.__customFrameNativeMenu) return;
window.__customFrameNativeMenu = true;
function toggleCheckbox(newElem, toggle) {
const tmp = newElem.querySelector("mdui-checkbox");
if (tmp && tmp !== undefined) {
if (toggle) {
tmp.setAttribute('checked', '');
tmp.setAttribute('value', 'on');
} else {
tmp.removeAttribute('checked');
tmp.setAttribute('value', 'off');
}
}
}
function initCFNM() {
let elem = document.querySelector("#floating div:not(:empty) div.will-change_transform.flex_1_1_800px div:has(> a) > a:last-child")
if (!elem) { return; }
let title = elem.querySelector("div.flex-g_1 > div")
if (!title || title.textContent.trim() !== "Custom window frame") { return; }
let desc = elem.querySelector("div.flex-g_1 > span")
if (!desc || desc.textContent.trim() !== "Let Stoat use its own custom titlebar.") { return; }
var newElem = elem.cloneNode(true);
let nTitle = newElem.querySelector("div.flex-g_1 > div");
let nDesc = newElem.querySelector("div.flex-g_1 > span");
if (!nTitle || !nDesc) { newElem = null; return; }
nTitle.textContent = "Native menu on custom window frame";
nDesc.textContent = "Use the system's native menu on the custom window frame.";
let config = window.desktopConfig.get();
toggleCheckbox(newElem, config.customFrameNativeMenu);
newElem.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
let config = window.desktopConfig.get();
config.customFrameNativeMenu = !config.customFrameNativeMenu;
window.desktopConfig.set(config);
toggleCheckbox(newElem, config.customFrameNativeMenu);
});
elem.parentNode.appendChild(newElem);
}
initCFNM();
const observer = new MutationObserver(() => initCFNM());
observer.observe(document.body, { childList: true, subtree: true });
})();

1
src/config.d.ts vendored
View file

@ -1,6 +1,7 @@
declare type DesktopConfig = {
firstLaunch: boolean;
customFrame: boolean;
customFrameNativeMenu: boolean;
minimiseToTray: boolean;
spellchecker: boolean;
hardwareAcceleration: boolean;

View file

@ -58,7 +58,8 @@ const loadInject = () => {
"repofrontend.js",
"ButtonFix.js",
"headliner.js",
"aviadesktopversion.js"
"aviadesktopversion.js",
"customFrameNativeMenu.js"
];
for (const plugin of plugins) {

View file

@ -13,6 +13,9 @@ const schema = {
customFrame: {
type: "boolean",
} as JSONSchema.Boolean,
customFrameNativeMenu: {
type: "boolean",
} as JSONSchema.Boolean,
minimiseToTray: {
type: "boolean",
} as JSONSchema.Boolean,
@ -55,6 +58,7 @@ const store = new Store({
defaults: {
firstLaunch: true,
customFrame: true,
customFrameNativeMenu: false,
minimiseToTray: true,
startMinimisedToTray: false,
spellchecker: true,
@ -78,6 +82,7 @@ class Config {
mainWindow.webContents.send("config", {
firstLaunch: this.firstLaunch,
customFrame: this.customFrame,
customFrameNativeMenu: this.customFrameNativeMenu,
minimiseToTray: this.minimiseToTray,
startMinimisedToTray: this.startMinimisedToTray,
spellchecker: this.spellchecker,
@ -113,6 +118,19 @@ class Config {
this.sync();
}
get customFrameNativeMenu() {
return (store as never as { get(k: string): boolean }).get("customFrameNativeMenu");
}
set customFrameNativeMenu(value: boolean) {
(store as never as { set(k: string, value: boolean): void }).set(
"customFrameNativeMenu",
value,
);
this.sync();
}
get minimiseToTray() {
return (store as never as { get(k: string): boolean }).get(
"minimiseToTray",

View file

@ -48,6 +48,14 @@ export function createMainWindow() {
height: 720,
backgroundColor: "#191919",
frame: !config.customFrame,
...(config.customFrame && config.customFrameNativeMenu ? {
// remove the default titlebar
titleBarStyle: 'hidden',
// expose window controls in Windows/Linux
...(process.platform !== 'darwin' ? {
titleBarOverlay: true
} : {})
} : {}),
icon: windowIcon,
show: !startHidden,
webPreferences: {
@ -141,8 +149,29 @@ export function createMainWindow() {
}
});
// send the config
mainWindow.webContents.on("did-finish-load", () => config.sync());
const initialCustomFrame: boolean = config.customFrame;
const initialCFNM: boolean = config.customFrameNativeMenu;
mainWindow.webContents.on("did-finish-load", () => {
// send the config
config.sync();
// on macOS add margin to the title, and hide custom controls
// We only use initial values other the menu can disappear
if (process.platform === 'darwin' &&
initialCustomFrame && initialCFNM) {
mainWindow.webContents.insertCSS(`
#root > div[style="display: flex; flex-direction: column; height: 100%;"] > div > div.h_29px {
&> div.d_flex:first-child {
margin-left: 75px;
}
&> a.place-items_center {
display: none;
}
}
`);
}
});
// configure spellchecker context menu
mainWindow.webContents.on("context-menu", (_, params) => {