Merge pull request #702 from TriliumNext/feature/native_window_buttons

Native title bar buttons
This commit is contained in:
Elian Doran 2024-12-07 00:43:06 +02:00 committed by GitHub
commit e22e974786
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 175 additions and 59 deletions

View File

@ -8,6 +8,7 @@ import macInit from './services/mac_init.js';
import electronContextMenu from "./menus/electron_context_menu.js"; import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import { t } from "./services/i18n.js"; import { t } from "./services/i18n.js";
import options from "./services/options.js";
await appContext.earlyInit(); await appContext.earlyInit();
@ -30,8 +31,7 @@ bundleService.getWidgetBundlesByParent().then(async widgetBundles => {
glob.setupGlobs(); glob.setupGlobs();
if (utils.isElectron()) { if (utils.isElectron()) {
utils.dynamicRequire('electron').ipcRenderer.on('globalShortcut', initOnElectron();
async (event, actionName) => appContext.triggerCommand(actionName));
} }
macInit.init(); macInit.init();
@ -43,3 +43,40 @@ noteAutocompleteService.init();
if (utils.isElectron()) { if (utils.isElectron()) {
electronContextMenu.setupContextMenu(); electronContextMenu.setupContextMenu();
} }
function initOnElectron() {
const electron = utils.dynamicRequire('electron');
electron.ipcRenderer.on('globalShortcut', async (event, actionName) => appContext.triggerCommand(actionName));
if (options.get("nativeTitleBarVisible") !== "true") {
initTitleBarButtons();
}
}
function initTitleBarButtons() {
const electronRemote = utils.dynamicRequire("@electron/remote");
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
if (window.glob.platform === "win32") {
const applyWindowsOverlay = () => {
const color = style.getPropertyValue("--native-titlebar-background");
const symbolColor = style.getPropertyValue("--native-titlebar-foreground");
if (color && symbolColor) {
currentWindow.setTitleBarOverlay({ color, symbolColor });
}
};
applyWindowsOverlay();
// Register for changes to the native title bar colors.
window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", applyWindowsOverlay);
}
if (window.glob.platform === "darwin") {
const xOffset = parseInt(style.getPropertyValue("--native-titlebar-darwin-x-offset"), 10);
const yOffset = parseInt(style.getPropertyValue("--native-titlebar-darwin-y-offset"), 10);
currentWindow.setWindowButtonPosition({ x: xOffset, y: yOffset });
}
}

View File

@ -84,6 +84,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref
import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js"; import options from "../services/options.js";
import utils from "../services/utils.js";
export default class DesktopLayout { export default class DesktopLayout {
constructor(customWidgets) { constructor(customWidgets) {
@ -95,15 +96,27 @@ export default class DesktopLayout {
const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal"); const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal); const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
const isElectron = (utils.isElectron());
const isMac = (window.glob.platform === "darwin");
const isWindows = (window.glob.platform === "win32");
const hasNativeTitleBar = (window.glob.hasNativeTitleBar);
return new RootContainer(launcherPaneIsHorizontal) /**
* If true, the tab bar is displayed above the launcher pane with full width; if false (default), the tab bar is displayed in the rest pane.
* On macOS we need to force the full-width tab bar on Electron in order to allow the semaphore (window controls) enough space.
*/
const fullWidthTabBar = (launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac));
const customTitleBarButtons = (hasNativeTitleBar && !isMac && !isWindows);
return new RootContainer(true)
.setParent(appContext) .setParent(appContext)
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout") .class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
.optChild(launcherPaneIsHorizontal, new FlexContainer('row') .optChild(fullWidthTabBar, new FlexContainer('row')
.class("tab-row-container") .class("tab-row-container")
.child(new LeftPaneToggleWidget(true)) .child(new FlexContainer( "row").id("tab-row-left-spacer"))
.optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
.child(new TabRowWidget().class("full-width")) .child(new TabRowWidget().class("full-width"))
.child(new TitleBarButtonsWidget()) .optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css('height', '40px') .css('height', '40px')
.css('background-color', 'var(--launcher-pane-background-color)') .css('background-color', 'var(--launcher-pane-background-color)')
.setParent(appContext) .setParent(appContext)
@ -120,9 +133,9 @@ export default class DesktopLayout {
.child(new FlexContainer('column') .child(new FlexContainer('column')
.id('rest-pane') .id('rest-pane')
.css("flex-grow", "1") .css("flex-grow", "1")
.optChild(!launcherPaneIsHorizontal, new FlexContainer('row') .optChild(!fullWidthTabBar, new FlexContainer('row')
.child(new TabRowWidget()) .child(new TabRowWidget())
.child(new TitleBarButtonsWidget()) .optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css('height', '40px') .css('height', '40px')
) )
.child(new FlexContainer('row') .child(new FlexContainer('row')

View File

@ -143,6 +143,11 @@ const TPL = `
</div> </div>
</span> </span>
<li class="dropdown-item toggle-pin">
<span class="bx bx-pin"></span>
${t('title_bar_buttons.window-on-top')}
</li>
<div class="dropdown-divider zoom-container-separator"></div> <div class="dropdown-divider zoom-container-separator"></div>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion"> <li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
@ -294,6 +299,23 @@ export default class GlobalMenuWidget extends BasicWidget {
const isElectron = utils.isElectron(); const isElectron = utils.isElectron();
this.$widget.find(".toggle-pin").toggle(isElectron);
if (isElectron) {
this.$widget.on("click", ".toggle-pin", (e) => {
const $el = $(e.target);
const remote = utils.dynamicRequire('@electron/remote');
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
const isAlwaysOnTop = focusedWindow.isAlwaysOnTop()
if (isAlwaysOnTop) {
focusedWindow.setAlwaysOnTop(false)
$el.removeClass('active');
} else {
focusedWindow.setAlwaysOnTop(true);
$el.addClass('active');
}
});
}
this.$widget.find(".logout-button").toggle(!isElectron); this.$widget.find(".logout-button").toggle(!isElectron);
this.$widget.find(".logout-button-separator").toggle(!isElectron); this.$widget.find(".logout-button-separator").toggle(!isElectron);

View File

@ -30,19 +30,14 @@ const TPL = `
display: inline-block; display: inline-block;
height: 40px; height: 40px;
width: 40px; width: 40px;
} }
.title-bar-buttons .top-btn.active{
background-color:var(--accented-background-color);
}
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus { .title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
box-shadow: none; box-shadow: none;
} }
</style> </style>
<!-- divs act as a hitbox for the buttons, making them clickable on corners --> <!-- divs act as a hitbox for the buttons, making them clickable on corners -->
<div class="top-btn" title="${t("title_bar_buttons.window-on-top")}"><button class="btn bx bx-pin"></button></div>
<div class="minimize-btn"><button class="btn bx bx-minus"></button></div> <div class="minimize-btn"><button class="btn bx bx-minus"></button></div>
<div class="maximize-btn"><button class="btn bx bx-checkbox"></button></div> <div class="maximize-btn"><button class="btn bx bx-checkbox"></button></div>
<div class="close-btn"><button class="btn bx bx-x"></button></div> <div class="close-btn"><button class="btn bx bx-x"></button></div>
@ -56,35 +51,11 @@ export default class TitleBarButtonsWidget extends BasicWidget {
this.$widget = $(TPL); this.$widget = $(TPL);
this.contentSized(); this.contentSized();
const $topBtn = this.$widget.find(".top-btn");
const $minimizeBtn = this.$widget.find(".minimize-btn"); const $minimizeBtn = this.$widget.find(".minimize-btn");
const $maximizeBtn = this.$widget.find(".maximize-btn"); const $maximizeBtn = this.$widget.find(".maximize-btn");
const $closeBtn = this.$widget.find(".close-btn"); const $closeBtn = this.$widget.find(".close-btn");
// When the window is restarted, the window will not be reset when it is set to the top,
// so get the window status and set the icon background
setTimeout(() => {
const remote = utils.dynamicRequire('@electron/remote');
if (remote.BrowserWindow.getFocusedWindow()?.isAlwaysOnTop()) {
$topBtn.addClass('active');
}
}, 1000);
$topBtn.on('click', () => {
$topBtn.trigger('blur');
const remote = utils.dynamicRequire('@electron/remote');
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
const isAlwaysOnTop = focusedWindow.isAlwaysOnTop()
if (isAlwaysOnTop) {
focusedWindow.setAlwaysOnTop(false)
$topBtn.removeClass('active');
} else {
focusedWindow.setAlwaysOnTop(true);
$topBtn.addClass('active');
}
});
$minimizeBtn.on('click', () => { $minimizeBtn.on('click', () => {
$minimizeBtn.trigger('blur'); $minimizeBtn.trigger('blur');
const remote = utils.dynamicRequire('@electron/remote'); const remote = utils.dynamicRequire('@electron/remote');

View File

@ -20,7 +20,7 @@
} }
:root { :root {
--submenu-opening-delay: 300ms; --submenu-opening-delay: 300ms;
} }
html { html {
@ -38,6 +38,15 @@ body {
color: var(--main-text-color); color: var(--main-text-color);
font-family: var(--main-font-family); font-family: var(--main-font-family);
font-size: var(--main-font-size); font-size: var(--main-font-size);
--native-titlebar-background: var(--main-background-color);
--native-titlebar-foreground: var(--main-text-color);
--native-titlebar-darwin-x-offset: 10;
--native-titlebar-darwin-y-offset: 12;
}
body.layout-horizontal {
--native-titlebar-background: var(--left-pane-background-color);
} }
body.mobile .desktop-only { body.mobile .desktop-only {
@ -1273,6 +1282,19 @@ textarea {
color: var(--muted-text-color); color: var(--muted-text-color);
} }
body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
padding-left: 1em;
}
#tab-row-left-spacer {
width: env(titlebar-area-x);
-webkit-app-region: drag;
}
.tab-row-container {
margin-right: calc(100vw - env(titlebar-area-width, 100vw));
}
.tab-row-container .toggle-button { .tab-row-container .toggle-button {
background: transparent; background: transparent;
appearance: none; appearance: none;

View File

@ -20,7 +20,7 @@
:root { :root {
/* --main-font-family: "Noto Sans", sans-serif; */ /* --main-font-family: "Noto Sans", sans-serif; */
--main-font-family: "Ubuntu Sans", sans-serif; --main-font-family: "Segoe UI", sans-serif;
/* --main-font-family: "Ubuntu", sans-serif; */ /* --main-font-family: "Ubuntu", sans-serif; */
/* --main-font-family: "Nunito", sans-serif; */ /* --main-font-family: "Nunito", sans-serif; */
/* --main-font-family: "Inter", sans-serif; */ /* --main-font-family: "Inter", sans-serif; */
@ -392,12 +392,24 @@
background-color: var(--root-background); background-color: var(--root-background);
} }
#root-widget.horizontal-layout { body {
--native-titlebar-darwin-x-offset: 10;
--native-titlebar-darwin-y-offset: 17 !important;
}
body.layout-vertical {
--native-titlebar-background: var(--root-background);
}
body.layout-horizontal {
--launcher-pane-background-color: var(--launcher-pane-horizontal-background-color); --launcher-pane-background-color: var(--launcher-pane-horizontal-background-color);
--launcher-pane-size: var(--launcher-pane-horizontal-size); --launcher-pane-size: var(--launcher-pane-horizontal-size);
--active-tab-background-color: var(--launcher-pane-background-color); --active-tab-background-color: var(--launcher-pane-background-color);
--active-tab-hover-background-color: var(--active-tab-background-color); --active-tab-hover-background-color: var(--active-tab-background-color);
--new-tab-button-background: transparent; --new-tab-button-background: transparent;
--tab-bar-height: 44px;
--native-titlebar-darwin-x-offset: 12;
--native-titlebar-darwin-y-offset: 14 !important;
} }
/* Matches when the left pane is collapsed */ /* Matches when the left pane is collapsed */
@ -426,7 +438,7 @@
* Launcher pane * Launcher pane
*/ */
#launcher-container, #launcher-container,
#root-widget.horizontal-layout > .horizontal { body.layout-horizontal > .horizontal {
align-items: center; align-items: center;
} }
@ -796,7 +808,7 @@ div.quick-search .search-button.show {
position: relative; position: relative;
} }
#root-widget.horizontal-layout .tab-row-container:after { body.layout-horizontal .tab-row-container:after {
content: ""; content: "";
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -806,16 +818,20 @@ div.quick-search .search-button.show {
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color); border-bottom: 1px solid var(--launcher-pane-horizontal-border-color);
} }
body.layout-vertical.electron.platform-darwin .tab-row-container {
border-bottom: 1px solid var(--subtle-border-color);
}
.tab-row-widget-container { .tab-row-widget-container {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
height: var(--tab-height) !important; height: var(--tab-height) !important;
} }
#root-widget.horizontal-layout .tab-row-container { body.layout-horizontal .tab-row-container {
padding-top: calc((var(--tab-bar-height) - var(--tab-height))); padding-top: calc((var(--tab-bar-height) - var(--tab-height)));
} }
#root-widget.horizontal-layout .tab-row-widget-container { body.layout-horizontal .tab-row-widget-container {
margin-top: 0; margin-top: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -842,7 +858,11 @@ div.quick-search .search-button.show {
transition: none; transition: none;
} }
#root-widget.horizontal-layout .tab-row-widget .note-tab .note-tab-wrapper { .tab-row-container .title-bar-buttons {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) * -1);
}
body.layout-horizontal .tab-row-widget .note-tab .note-tab-wrapper {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }

View File

@ -1407,7 +1407,7 @@
"saved-search-note-refreshed": "Gespeicherte Such-Notiz wurde aktualisiert." "saved-search-note-refreshed": "Gespeicherte Such-Notiz wurde aktualisiert."
}, },
"title_bar_buttons": { "title_bar_buttons": {
"window-on-top": "Dieses Fenster immer oben halten." "window-on-top": "Dieses Fenster immer oben halten"
}, },
"note_detail": { "note_detail": {
"could_not_find_typewidget": "Konnte typeWidget für Typ {{type}} nicht finden" "could_not_find_typewidget": "Konnte typeWidget für Typ {{type}} nicht finden"

View File

@ -1434,7 +1434,7 @@
"saved-search-note-refreshed": "Saved search note refreshed." "saved-search-note-refreshed": "Saved search note refreshed."
}, },
"title_bar_buttons": { "title_bar_buttons": {
"window-on-top": "Keep this window on top." "window-on-top": "Keep Window on Top"
}, },
"note_detail": { "note_detail": {
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'" "could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'"

View File

@ -1432,7 +1432,7 @@
"saved-search-note-refreshed": "La nota de búsqueda guardada fue recargada." "saved-search-note-refreshed": "La nota de búsqueda guardada fue recargada."
}, },
"title_bar_buttons": { "title_bar_buttons": {
"window-on-top": "Mantener esta ventana en la parte superior." "window-on-top": "Mantener esta ventana en la parte superior"
}, },
"note_detail": { "note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'" "could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'"

View File

@ -1408,7 +1408,7 @@
"saved-search-note-refreshed": "Note de recherche enregistrée actualisée." "saved-search-note-refreshed": "Note de recherche enregistrée actualisée."
}, },
"title_bar_buttons": { "title_bar_buttons": {
"window-on-top": "Épingler cette fenêtre au premier plan." "window-on-top": "Épingler cette fenêtre au premier plan"
}, },
"note_detail": { "note_detail": {
"could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'" "could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'"

View File

@ -28,10 +28,15 @@ function index(req: Request, res: Response) {
// The page is restored from cache, but the API call fail. // The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
const isElectron = utils.isElectron();
res.render(view, { res.render(view, {
csrfToken: csrfToken, csrfToken: csrfToken,
themeCssUrl: getThemeCssUrl(options.theme), themeCssUrl: getThemeCssUrl(options.theme),
headingStyle: options.headingStyle, headingStyle: options.headingStyle,
layoutOrientation: options.layoutOrientation,
platform: process.platform,
isElectron,
hasNativeTitleBar: (isElectron && options.nativeTitleBarVisible === "true"),
mainFontSize: parseInt(options.mainFontSize), mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize), treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize), detailFontSize: parseInt(options.detailFontSize),

View File

@ -8,7 +8,7 @@ import sqlInit from "./sql_init.js";
import cls from "./cls.js"; import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js"; import keyboardActionsService from "./keyboard_actions.js";
import remoteMain from "@electron/remote/main/index.js"; import remoteMain from "@electron/remote/main/index.js";
import { App, BrowserWindow, WebContents, ipcMain } from 'electron'; import { App, BrowserWindow, BrowserWindowConstructorOptions, WebContents, ipcMain } from 'electron';
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname } from "path"; import { dirname } from "path";
@ -31,7 +31,7 @@ async function createExtraWindow(extraWindowHash: string) {
contextIsolation: false, contextIsolation: false,
spellcheck: spellcheckEnabled spellcheck: spellcheckEnabled
}, },
frame: optionService.getOptionBool('nativeTitleBarVisible'), ...getWindowExtraOpts(),
icon: getIcon() icon: getIcon()
}); });
@ -71,6 +71,8 @@ async function createMainWindow(app: App) {
const { BrowserWindow } = (await import('electron')); // should not be statically imported const { BrowserWindow } = (await import('electron')); // should not be statically imported
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
x: mainWindowState.x, x: mainWindowState.x,
y: mainWindowState.y, y: mainWindowState.y,
@ -82,9 +84,9 @@ async function createMainWindow(app: App) {
contextIsolation: false, contextIsolation: false,
spellcheck: spellcheckEnabled, spellcheck: spellcheckEnabled,
webviewTag: true webviewTag: true
}, },
frame: optionService.getOptionBool('nativeTitleBarVisible'), icon: getIcon(),
icon: getIcon() ...getWindowExtraOpts()
}); });
mainWindowState.manage(mainWindow); mainWindowState.manage(mainWindow);
@ -110,6 +112,28 @@ async function createMainWindow(app: App) {
}); });
} }
function getWindowExtraOpts() {
const extraOpts: Partial<BrowserWindowConstructorOptions> = {};
const isMac = (process.platform === "darwin");
const isWindows = (process.platform === "win32");
if (!optionService.getOptionBool('nativeTitleBarVisible')) {
if (isMac) {
extraOpts.titleBarStyle = "hiddenInset";
extraOpts.titleBarOverlay = true;
} else if (isWindows) {
extraOpts.titleBarStyle = "hidden";
extraOpts.titleBarOverlay = true;
} else {
// Linux or other platforms.
extraOpts.frame = false;
}
}
return extraOpts;
}
function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) { function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
remoteMain.enable(webContents); remoteMain.enable(webContents);

View File

@ -6,7 +6,7 @@
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest"> <link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>TriliumNext Notes</title> <title>TriliumNext Notes</title>
</head> </head>
<body class="desktop heading-style-<%= headingStyle %>"> <body class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %>">
<noscript><%= t("javascript-required") %></noscript> <noscript><%= t("javascript-required") %></noscript>
<script> <script>
@ -36,6 +36,8 @@
triliumVersion: "<%= triliumVersion %>", triliumVersion: "<%= triliumVersion %>",
assetPath: "<%= assetPath %>", assetPath: "<%= assetPath %>",
appPath: "<%= appPath %>", appPath: "<%= appPath %>",
platform: "<%= platform %>",
hasNativeTitleBar: <%= hasNativeTitleBar %>,
TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %> TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>
}; };
</script> </script>
@ -84,5 +86,5 @@
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css"> <link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">
</body> </>
</html> </html>