mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 02:02:29 +08:00
Merge pull request #702 from TriliumNext/feature/native_window_buttons
Native title bar buttons
This commit is contained in:
commit
e22e974786
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -143,6 +143,11 @@ const TPL = `
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 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>
|
||||
<!-- divs act as a hitbox for the buttons, making them clickable on corners -->
|
||||
<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="close-btn"><button class="btn bx bx-x"></button></div>
|
||||
@ -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');
|
||||
|
@ -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 {
|
||||
margin-right: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
}
|
||||
|
||||
.tab-row-container .toggle-button {
|
||||
background: transparent;
|
||||
appearance: none;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -796,7 +808,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;
|
||||
@ -806,16 +818,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;
|
||||
@ -842,7 +858,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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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}}'"
|
||||
|
@ -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}}'"
|
||||
|
@ -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}}'"
|
||||
|
@ -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),
|
||||
|
@ -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<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) {
|
||||
remoteMain.enable(webContents);
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>TriliumNext Notes</title>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
@ -36,6 +36,8 @@
|
||||
triliumVersion: "<%= triliumVersion %>",
|
||||
assetPath: "<%= assetPath %>",
|
||||
appPath: "<%= appPath %>",
|
||||
platform: "<%= platform %>",
|
||||
hasNativeTitleBar: <%= hasNativeTitleBar %>,
|
||||
TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>
|
||||
};
|
||||
</script>
|
||||
@ -84,5 +86,5 @@
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">
|
||||
|
||||
</body>
|
||||
</>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user