diff --git a/src/public/app/desktop.js b/src/public/app/desktop.js
index 835624d35..bad74c698 100644
--- a/src/public/app/desktop.js
+++ b/src/public/app/desktop.js
@@ -8,6 +8,7 @@ import macInit from './services/mac_init.js';
import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
+import options from "./services/options.js";
await appContext.earlyInit();
@@ -30,8 +31,7 @@ bundleService.getWidgetBundlesByParent().then(async widgetBundles => {
glob.setupGlobs();
if (utils.isElectron()) {
- utils.dynamicRequire('electron').ipcRenderer.on('globalShortcut',
- async (event, actionName) => appContext.triggerCommand(actionName));
+ initOnElectron();
}
macInit.init();
@@ -43,3 +43,40 @@ noteAutocompleteService.init();
if (utils.isElectron()) {
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 });
+ }
+}
diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js
index 995654f4c..8d0a667a1 100644
--- a/src/public/app/layouts/desktop_layout.js
+++ b/src/public/app/layouts/desktop_layout.js
@@ -84,6 +84,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
+import utils from "../services/utils.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -95,15 +96,27 @@ export default class DesktopLayout {
const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
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)
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
- .optChild(launcherPaneIsHorizontal, new FlexContainer('row')
+ .optChild(fullWidthTabBar, new FlexContainer('row')
.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 TitleBarButtonsWidget())
+ .optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css('height', '40px')
.css('background-color', 'var(--launcher-pane-background-color)')
.setParent(appContext)
@@ -120,9 +133,9 @@ export default class DesktopLayout {
.child(new FlexContainer('column')
.id('rest-pane')
.css("flex-grow", "1")
- .optChild(!launcherPaneIsHorizontal, new FlexContainer('row')
+ .optChild(!fullWidthTabBar, new FlexContainer('row')
.child(new TabRowWidget())
- .child(new TitleBarButtonsWidget())
+ .optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css('height', '40px')
)
.child(new FlexContainer('row')
diff --git a/src/public/app/widgets/buttons/global_menu.js b/src/public/app/widgets/buttons/global_menu.js
index 9ee235a45..6607c0182 100644
--- a/src/public/app/widgets/buttons/global_menu.js
+++ b/src/public/app/widgets/buttons/global_menu.js
@@ -143,6 +143,11 @@ const TPL = `
+
+
+ ${t('title_bar_buttons.window-on-top')}
+
+
@@ -294,6 +299,23 @@ export default class GlobalMenuWidget extends BasicWidget {
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-separator").toggle(!isElectron);
diff --git a/src/public/app/widgets/title_bar_buttons.js b/src/public/app/widgets/title_bar_buttons.js
index edb270f77..6d14dca1a 100644
--- a/src/public/app/widgets/title_bar_buttons.js
+++ b/src/public/app/widgets/title_bar_buttons.js
@@ -30,19 +30,14 @@ const TPL = `
display: inline-block;
height: 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 {
box-shadow: none;
}
-
-
+
@@ -56,35 +51,11 @@ export default class TitleBarButtonsWidget extends BasicWidget {
this.$widget = $(TPL);
this.contentSized();
-
- const $topBtn = this.$widget.find(".top-btn");
+
const $minimizeBtn = this.$widget.find(".minimize-btn");
const $maximizeBtn = this.$widget.find(".maximize-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.trigger('blur');
const remote = utils.dynamicRequire('@electron/remote');
diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css
index 2b2e3839f..ec995f548 100644
--- a/src/public/stylesheets/style.css
+++ b/src/public/stylesheets/style.css
@@ -20,7 +20,7 @@
}
:root {
- --submenu-opening-delay: 300ms;
+ --submenu-opening-delay: 300ms;
}
html {
@@ -38,6 +38,15 @@ body {
color: var(--main-text-color);
font-family: var(--main-font-family);
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 {
@@ -1273,6 +1282,19 @@ textarea {
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 {
+ padding-right: calc(100vw - env(titlebar-area-width, 100vw));
+}
+
.tab-row-container .toggle-button {
background: transparent;
appearance: none;
diff --git a/src/public/stylesheets/theme-next.css b/src/public/stylesheets/theme-next.css
index f8d8e5097..ef3f72727 100644
--- a/src/public/stylesheets/theme-next.css
+++ b/src/public/stylesheets/theme-next.css
@@ -20,7 +20,7 @@
:root {
/* --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: "Nunito", sans-serif; */
/* --main-font-family: "Inter", sans-serif; */
@@ -392,12 +392,24 @@
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-size: var(--launcher-pane-horizontal-size);
--active-tab-background-color: var(--launcher-pane-background-color);
--active-tab-hover-background-color: var(--active-tab-background-color);
--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 */
@@ -426,7 +438,7 @@
* Launcher pane
*/
#launcher-container,
-#root-widget.horizontal-layout > .horizontal {
+body.layout-horizontal > .horizontal {
align-items: center;
}
@@ -837,7 +849,7 @@ div.quick-search .search-button.show {
position: relative;
}
-#root-widget.horizontal-layout .tab-row-container:after {
+body.layout-horizontal .tab-row-container:after {
content: "";
position: absolute;
bottom: 0;
@@ -847,16 +859,20 @@ div.quick-search .search-button.show {
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 {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
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)));
}
-#root-widget.horizontal-layout .tab-row-widget-container {
+body.layout-horizontal .tab-row-widget-container {
margin-top: 0;
position: relative;
overflow: hidden;
@@ -883,7 +899,11 @@ div.quick-search .search-button.show {
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-right-radius: 0;
}
diff --git a/src/public/translations/de/translation.json b/src/public/translations/de/translation.json
index 6563dde95..bb4306481 100644
--- a/src/public/translations/de/translation.json
+++ b/src/public/translations/de/translation.json
@@ -1407,7 +1407,7 @@
"saved-search-note-refreshed": "Gespeicherte Such-Notiz wurde aktualisiert."
},
"title_bar_buttons": {
- "window-on-top": "Dieses Fenster immer oben halten."
+ "window-on-top": "Dieses Fenster immer oben halten"
},
"note_detail": {
"could_not_find_typewidget": "Konnte typeWidget für Typ ‚{{type}}‘ nicht finden"
diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json
index 43c975c2e..46688974f 100644
--- a/src/public/translations/en/translation.json
+++ b/src/public/translations/en/translation.json
@@ -1434,7 +1434,7 @@
"saved-search-note-refreshed": "Saved search note refreshed."
},
"title_bar_buttons": {
- "window-on-top": "Keep this window on top."
+ "window-on-top": "Keep Window on Top"
},
"note_detail": {
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'"
diff --git a/src/public/translations/es/translation.json b/src/public/translations/es/translation.json
index cd6dec361..db75b1067 100644
--- a/src/public/translations/es/translation.json
+++ b/src/public/translations/es/translation.json
@@ -1432,7 +1432,7 @@
"saved-search-note-refreshed": "La nota de búsqueda guardada fue recargada."
},
"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": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'"
diff --git a/src/public/translations/fr/translation.json b/src/public/translations/fr/translation.json
index 2a85f610e..f7ebadbc6 100644
--- a/src/public/translations/fr/translation.json
+++ b/src/public/translations/fr/translation.json
@@ -1408,7 +1408,7 @@
"saved-search-note-refreshed": "Note de recherche enregistrée actualisée."
},
"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": {
"could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'"
diff --git a/src/routes/index.ts b/src/routes/index.ts
index f5951d3c6..a37965b4c 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -28,10 +28,15 @@ function index(req: Request, res: Response) {
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ const isElectron = utils.isElectron();
res.render(view, {
csrfToken: csrfToken,
themeCssUrl: getThemeCssUrl(options.theme),
headingStyle: options.headingStyle,
+ layoutOrientation: options.layoutOrientation,
+ platform: process.platform,
+ isElectron,
+ hasNativeTitleBar: (isElectron && options.nativeTitleBarVisible === "true"),
mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),
diff --git a/src/services/window.ts b/src/services/window.ts
index d0c625a15..72a337d9a 100644
--- a/src/services/window.ts
+++ b/src/services/window.ts
@@ -8,7 +8,7 @@ import sqlInit from "./sql_init.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.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 { dirname } from "path";
@@ -31,7 +31,7 @@ async function createExtraWindow(extraWindowHash: string) {
contextIsolation: false,
spellcheck: spellcheckEnabled
},
- frame: optionService.getOptionBool('nativeTitleBarVisible'),
+ ...getWindowExtraOpts(),
icon: getIcon()
});
@@ -71,6 +71,8 @@ async function createMainWindow(app: App) {
const { BrowserWindow } = (await import('electron')); // should not be statically imported
+
+
mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
@@ -82,9 +84,9 @@ async function createMainWindow(app: App) {
contextIsolation: false,
spellcheck: spellcheckEnabled,
webviewTag: true
- },
- frame: optionService.getOptionBool('nativeTitleBarVisible'),
- icon: getIcon()
+ },
+ icon: getIcon(),
+ ...getWindowExtraOpts()
});
mainWindowState.manage(mainWindow);
@@ -110,6 +112,28 @@ async function createMainWindow(app: App) {
});
}
+function getWindowExtraOpts() {
+ const extraOpts: Partial = {};
+
+ 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) {
remoteMain.enable(webContents);
diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs
index 59a4b2b4f..29afdcfd3 100644
--- a/src/views/desktop.ejs
+++ b/src/views/desktop.ejs
@@ -6,7 +6,7 @@
TriliumNext Notes
-
+
@@ -84,5 +86,5 @@
-
+>