From 5634601554d40d5089e9069f232cccaaeb14f676 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 8 Jun 2022 15:00:32 -0700 Subject: [PATCH] Use patched frameless-titlebar on Windows --- .storybook/preview-head.html | 18 ++ ACKNOWLEDGMENTS.md | 24 ++ about.html | 5 + app/main.ts | 298 +++++++++++++++--- app/menu.ts | 41 +-- app/spell_check.ts | 2 +- background.html | 20 +- debug_log.html | 5 + package.json | 1 + preload.js | 14 + settings.html | 5 + sticker-creator/components/ConfirmModal.scss | 2 +- sticker-creator/index.html | 5 + sticker-creator/preload.js | 2 +- stylesheets/_global.scss | 11 + stylesheets/_mixins.scss | 2 +- stylesheets/_modules.scss | 18 +- stylesheets/_variables.scss | 1 + stylesheets/components/About.scss | 2 +- stylesheets/components/App.scss | 4 + stylesheets/components/Lightbox.scss | 2 +- stylesheets/components/MediaEditor.scss | 4 +- stylesheets/components/Preferences.scss | 2 +- stylesheets/components/Stories.scss | 4 +- stylesheets/components/StoryViewer.scss | 12 +- stylesheets/components/TitleBarContainer.scss | 31 ++ stylesheets/manifest.scss | 1 + ts/background.ts | 56 +++- ts/components/About.tsx | 65 ++-- ts/components/App.tsx | 59 +++- ts/components/DebugLogWindow.stories.tsx | 2 + ts/components/DebugLogWindow.tsx | 149 +++++---- ts/components/ModalHost.tsx | 15 +- ts/components/Preferences.stories.tsx | 3 + ts/components/Preferences.tsx | 157 ++++----- ts/components/TitleBarContainer.tsx | 185 +++++++++++ ts/context/createNativeThemeListener.ts | 10 + ts/hooks/useTheme.ts | 58 ++++ ts/main/settingsChannel.ts | 9 +- ts/state/ducks/user.ts | 22 ++ ts/state/getInitialState.ts | 10 + ts/state/selectors/user.ts | 22 ++ ts/state/smart/App.tsx | 27 +- ts/test-node/app/menu_test.ts | 17 +- ts/types/Settings.ts | 9 - ts/types/menu.ts | 33 ++ ts/util/lint/exceptions.json | 154 +++++++++ ts/windows/about/preload.ts | 6 +- ts/windows/context.ts | 28 ++ ts/windows/debuglog/preload.ts | 4 +- ts/windows/permissions/preload.ts | 4 +- ts/windows/screenShare/preload.ts | 5 +- ts/windows/settings/preload.ts | 5 +- yarn.lock | 16 +- 54 files changed, 1343 insertions(+), 323 deletions(-) create mode 100644 stylesheets/components/TitleBarContainer.scss create mode 100644 ts/components/TitleBarContainer.tsx create mode 100644 ts/hooks/useTheme.ts create mode 100644 ts/types/menu.ts diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 9768dffa7..a5ed1aa6e 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -3,6 +3,11 @@ + diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index df1b48ce5..bd118790b 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -5,6 +5,30 @@ Signal Desktop makes use of the following open source projects. +## @indutny/frameless-titlebar + + MIT License + + Copyright (c) 2019 Cristian Ponce + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ## @popperjs/core License: MIT diff --git a/about.html b/about.html index 0b5a5f3a5..a4156e371 100644 --- a/about.html +++ b/about.html @@ -20,6 +20,11 @@ rel="stylesheet" type="text/css" /> + diff --git a/app/main.ts b/app/main.ts index c3313e855..0b0167163 100644 --- a/app/main.ts +++ b/app/main.ts @@ -27,6 +27,10 @@ import { shell, systemPreferences, } from 'electron'; +import type { + MenuItemConstructorOptions, + TitleBarOverlayOptions, +} from 'electron'; import { z } from 'zod'; import packageJson from '../package.json'; @@ -75,7 +79,8 @@ import * as logging from '../ts/logging/main_process_logging'; import { MainSQL } from '../ts/sql/main'; import * as sqlChannels from './sql_channel'; import * as windowState from './window_state'; -import type { MenuOptionsType } from './menu'; +import type { CreateTemplateOptionsType } from './menu'; +import type { MenuActionType } from '../ts/types/menu'; import { createTemplate } from './menu'; import { installFileHandler, installWebHandler } from './protocol_filter'; import * as OS from '../ts/OS'; @@ -91,10 +96,6 @@ import { } from '../ts/util/sgnlHref'; import { clearTimeoutIfNecessary } from '../ts/util/clearTimeoutIfNecessary'; import { toggleMaximizedBrowserWindow } from '../ts/util/toggleMaximizedBrowserWindow'; -import { - getTitleBarVisibility, - TitleBarVisibility, -} from '../ts/types/Settings'; import { ChallengeMainHandler } from '../ts/main/challengeMain'; import { NativeThemeNotifier } from '../ts/main/NativeThemeNotifier'; import { PowerChannel } from '../ts/main/powerChannel'; @@ -324,6 +325,8 @@ if (windowFromUserConfig) { ephemeralConfig.set('window', windowConfig); } +let menuOptions: CreateTemplateOptionsType | undefined; + // These will be set after app fires the 'ready' event let logger: LoggerType | undefined; let locale: LocaleType | undefined; @@ -429,7 +432,10 @@ async function handleUrl(event: Electron.Event, rawTarget: string) { } } -function handleCommonWindowEvents(window: BrowserWindow) { +function handleCommonWindowEvents( + window: BrowserWindow, + titleBarOverlay: TitleBarOverlayOptions | false = false +) { window.webContents.on('will-navigate', handleUrl); window.webContents.on('new-window', handleUrl); window.webContents.on( @@ -467,6 +473,23 @@ function handleCommonWindowEvents(window: BrowserWindow) { window.webContents.on('preferred-size-changed', onZoomChanged); nativeThemeNotifier.addWindow(window); + + if (titleBarOverlay) { + const onThemeChange = async () => { + try { + const newOverlay = await getTitleBarOverlay(); + if (!newOverlay) { + return; + } + window.setTitleBarOverlay(newOverlay); + } catch (error) { + console.error('onThemeChange error', error); + } + }; + + nativeTheme.on('updated', onThemeChange); + settingsChannel?.on('change:themeSetting', onThemeChange); + } } const DEFAULT_WIDTH = 800; @@ -521,10 +544,50 @@ if (OS.isWindows()) { windowIcon = join(__dirname, '../build/icons/png/512x512.png'); } +const mainTitleBarStyle = + OS.isLinux() || isTestEnvironment(getEnvironment()) + ? ('default' as const) + : ('hidden' as const); + +const nonMainTitleBarStyle = OS.isWindows() + ? ('hidden' as const) + : ('default' as const); + +async function getTitleBarOverlay(): Promise { + if (!OS.isWindows()) { + return false; + } + + const theme = await getResolvedThemeSetting(); + + let color: string; + let symbolColor: string; + if (theme === 'light') { + color = '#e8e8e8'; + symbolColor = '#1b1b1b'; + } else if (theme === 'dark') { + color = '#24292e'; + symbolColor = '#fff'; + } else { + throw missingCaseError(theme); + } + + return { + color, + symbolColor, + + // Should match stylesheets/components/TitleBarContainer.scss minus the + // border + height: 28 - 1, + }; +} + async function createWindow() { const usePreloadBundle = !isTestEnvironment(getEnvironment()) || forcePreloadBundle; + const titleBarOverlay = await getTitleBarOverlay(); + const windowOptions: Electron.BrowserWindowConstructorOptions = { show: false, width: DEFAULT_WIDTH, @@ -532,11 +595,8 @@ async function createWindow() { minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT, autoHideMenuBar: false, - titleBarStyle: - getTitleBarVisibility() === TitleBarVisibility.Hidden && - !isTestEnvironment(getEnvironment()) - ? 'hidden' - : 'default', + titleBarStyle: mainTitleBarStyle, + titleBarOverlay, backgroundColor: isTestEnvironment(getEnvironment()) ? '#ffffff' // Tests should always be rendered on a white background : await getBackgroundColor(), @@ -616,7 +676,20 @@ async function createWindow() { systemTrayService.setMainWindow(mainWindow); } - function captureAndSaveWindowStats() { + function saveWindowStats() { + if (!windowConfig) { + return; + } + + getLogger().info( + 'Updating BrowserWindow config: %s', + JSON.stringify(windowConfig) + ); + ephemeralConfig.set('window', windowConfig); + } + const debouncedSaveStats = debounce(saveWindowStats, 500); + + function captureWindowStats() { if (!mainWindow) { return; } @@ -624,8 +697,7 @@ async function createWindow() { const size = mainWindow.getSize(); const position = mainWindow.getPosition(); - // so if we need to recreate the window, we have the most recent settings - windowConfig = { + const newWindowConfig = { maximized: mainWindow.isMaximized(), autoHideMenuBar: mainWindow.autoHideMenuBar, fullscreen: mainWindow.isFullScreen(), @@ -635,16 +707,24 @@ async function createWindow() { y: position[1], }; - getLogger().info( - 'Updating BrowserWindow config: %s', - JSON.stringify(windowConfig) - ); - ephemeralConfig.set('window', windowConfig); + if ( + newWindowConfig.fullscreen !== windowConfig?.fullscreen || + newWindowConfig.maximized !== windowConfig?.maximized + ) { + mainWindow.webContents.send('window:set-window-stats', { + isMaximized: newWindowConfig.maximized, + isFullScreen: newWindowConfig.fullscreen, + }); + } + + // so if we need to recreate the window, we have the most recent settings + windowConfig = newWindowConfig; + + debouncedSaveStats(); } - const debouncedCaptureStats = debounce(captureAndSaveWindowStats, 500); - mainWindow.on('resize', debouncedCaptureStats); - mainWindow.on('move', debouncedCaptureStats); + mainWindow.on('resize', captureWindowStats); + mainWindow.on('move', captureWindowStats); const setWindowFocus = () => { if (!mainWindow) { @@ -681,7 +761,7 @@ async function createWindow() { mainWindow.webContents.openDevTools(); } - handleCommonWindowEvents(mainWindow); + handleCommonWindowEvents(mainWindow, titleBarOverlay); // App dock icon bounce bounce.init(mainWindow); @@ -981,6 +1061,7 @@ function showScreenShareWindow(sourceName: string) { resizable: false, show: false, title: getLocale().i18n('screenShareWindow'), + titleBarStyle: nonMainTitleBarStyle, width, webPreferences: { ...defaultWebPrefs, @@ -1021,11 +1102,15 @@ async function showAbout() { return; } + const titleBarOverlay = await getTitleBarOverlay(); + const options = { width: 500, height: 500, resizable: false, title: getLocale().i18n('aboutSignalDesktop'), + titleBarStyle: nonMainTitleBarStyle, + titleBarOverlay, autoHideMenuBar: true, backgroundColor: await getBackgroundColor(), show: false, @@ -1041,7 +1126,7 @@ async function showAbout() { aboutWindow = new BrowserWindow(options); - handleCommonWindowEvents(aboutWindow); + handleCommonWindowEvents(aboutWindow, titleBarOverlay); aboutWindow.loadURL(prepareFileUrl([__dirname, '../about.html'])); @@ -1063,12 +1148,16 @@ async function showSettingsWindow() { return; } + const titleBarOverlay = await getTitleBarOverlay(); + const options = { width: 700, height: 700, frame: true, resizable: false, title: getLocale().i18n('signalDesktopPreferences'), + titleBarStyle: nonMainTitleBarStyle, + titleBarOverlay, autoHideMenuBar: true, backgroundColor: await getBackgroundColor(), show: false, @@ -1084,7 +1173,7 @@ async function showSettingsWindow() { settingsWindow = new BrowserWindow(options); - handleCommonWindowEvents(settingsWindow); + handleCommonWindowEvents(settingsWindow, titleBarOverlay); settingsWindow.loadURL(prepareFileUrl([__dirname, '../settings.html'])); @@ -1132,6 +1221,7 @@ async function showStickerCreator() { const { x = 0, y = 0 } = windowConfig || {}; + // TODO: DESKTOP-3670 const options = { x: x + 100, y: y + 100, @@ -1191,12 +1281,16 @@ async function showDebugLogWindow() { return; } + const titleBarOverlay = await getTitleBarOverlay(); + const theme = await getThemeSetting(); const options = { width: 700, height: 500, resizable: false, title: getLocale().i18n('debugLog'), + titleBarStyle: nonMainTitleBarStyle, + titleBarOverlay, autoHideMenuBar: true, backgroundColor: await getBackgroundColor(), show: false, @@ -1218,7 +1312,7 @@ async function showDebugLogWindow() { debugLogWindow = new BrowserWindow(options); - handleCommonWindowEvents(debugLogWindow); + handleCommonWindowEvents(debugLogWindow, titleBarOverlay); debugLogWindow.loadURL( prepareFileUrl([__dirname, '../debug_log.html'], { theme }) @@ -1259,6 +1353,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) { height: Math.min(150, size[1]), resizable: false, title: getLocale().i18n('allowAccess'), + titleBarStyle: nonMainTitleBarStyle, autoHideMenuBar: true, backgroundColor: await getBackgroundColor(), show: false, @@ -1681,9 +1776,9 @@ app.on('ready', async () => { ]); }); -function setupMenu(options?: Partial) { +function setupMenu(options?: Partial) { const { platform } = process; - const menuOptions = { + menuOptions = { // options development, devTools: defaultWebPrefs.devTools, @@ -1713,6 +1808,14 @@ function setupMenu(options?: Partial) { const template = createTemplate(menuOptions, getLocale().messages); const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); + + mainWindow?.webContents.send('window:set-menu-options', { + development: menuOptions.development, + devTools: menuOptions.devTools, + includeSetup: menuOptions.includeSetup, + isProduction: menuOptions.isProduction, + platform: menuOptions.platform, + }); } async function requestShutdown() { @@ -1910,12 +2013,6 @@ ipc.on( } ); -ipc.on('close-about', () => { - if (aboutWindow) { - aboutWindow.close(); - } -}); - ipc.on('close-screen-share-controller', () => { if (screenShareWindow) { screenShareWindow.close(); @@ -1941,11 +2038,6 @@ ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => { // Debug Log-related IPC calls ipc.on('show-debug-log', showDebugLogWindow); -ipc.on('close-debug-log', () => { - if (debugLogWindow) { - debugLogWindow.close(); - } -}); ipc.on( 'show-debug-log-save-dialog', async (_event: Electron.Event, logText: string) => { @@ -1973,11 +2065,6 @@ ipc.handle( } } ); -ipc.on('close-permissions-popup', () => { - if (permissionsPopupWindow) { - permissionsPopupWindow.close(); - } -}); // Settings-related IPC calls @@ -1993,11 +2080,6 @@ function removeDarkOverlay() { } ipc.on('show-settings', showSettingsWindow); -ipc.on('close-settings', () => { - if (settingsWindow) { - settingsWindow.close(); - } -}); ipc.on('delete-all-data', () => { if (settingsWindow) { @@ -2188,6 +2270,124 @@ ipc.handle('getScreenCaptureSources', async () => { }); }); +ipc.handle('executeMenuRole', async ({ sender }, untypedRole) => { + const role = untypedRole as MenuItemConstructorOptions['role']; + + const senderWindow = BrowserWindow.fromWebContents(sender); + + switch (role) { + case 'undo': + sender.undo(); + break; + case 'redo': + sender.redo(); + break; + case 'cut': + sender.cut(); + break; + case 'copy': + sender.copy(); + break; + case 'paste': + sender.paste(); + break; + case 'pasteAndMatchStyle': + sender.pasteAndMatchStyle(); + break; + case 'delete': + sender.delete(); + break; + case 'selectAll': + sender.selectAll(); + break; + case 'reload': + sender.reload(); + break; + case 'toggleDevTools': + sender.toggleDevTools(); + break; + + case 'resetZoom': + sender.setZoomLevel(0); + break; + case 'zoomIn': + sender.setZoomLevel(sender.getZoomLevel() + 1); + break; + case 'zoomOut': + sender.setZoomLevel(sender.getZoomLevel() - 1); + break; + + case 'togglefullscreen': + senderWindow?.setFullScreen(!senderWindow?.isFullScreen()); + break; + case 'minimize': + senderWindow?.minimize(); + break; + case 'close': + senderWindow?.close(); + break; + + case 'quit': + app.quit(); + break; + + default: + // ignored + break; + } +}); + +ipc.handle('getMainWindowStats', async () => { + return { + isMaximized: windowConfig?.maximized ?? false, + isFullScreen: windowConfig?.fullscreen ?? false, + }; +}); + +ipc.handle('getMenuOptions', async () => { + return { + development: menuOptions?.development ?? false, + devTools: menuOptions?.devTools ?? false, + includeSetup: menuOptions?.includeSetup ?? false, + isProduction: menuOptions?.isProduction ?? true, + platform: menuOptions?.platform ?? 'unknown', + }; +}); + +ipc.handle('executeMenuAction', async (_event, action: MenuActionType) => { + if (action === 'forceUpdate') { + forceUpdate(); + } else if (action === 'openContactUs') { + openContactUs(); + } else if (action === 'openForums') { + openForums(); + } else if (action === 'openJoinTheBeta') { + openJoinTheBeta(); + } else if (action === 'openReleaseNotes') { + openReleaseNotes(); + } else if (action === 'openSupportPage') { + openSupportPage(); + } else if (action === 'setupAsNewDevice') { + setupAsNewDevice(); + } else if (action === 'setupAsStandalone') { + setupAsStandalone(); + } else if (action === 'showAbout') { + showAbout(); + } else if (action === 'showDebugLog') { + showDebugLogWindow(); + } else if (action === 'showKeyboardShortcuts') { + showKeyboardShortcuts(); + } else if (action === 'showSettings') { + showSettingsWindow(); + } else if (action === 'showStickerCreator') { + showStickerCreator(); + } else if (action === 'showWindow') { + showWindow(); + } else { + throw missingCaseError(action); + } +}); + if (isTestEnvironment(getEnvironment())) { ipc.handle('ci:test-electron:done', async (_event, info) => { if (!process.env.TEST_QUIT_ON_COMPLETE) { diff --git a/app/menu.ts b/app/menu.ts index 04eb4acc3..d5d96d5ec 100644 --- a/app/menu.ts +++ b/app/menu.ts @@ -1,40 +1,19 @@ -// Copyright 2017-2020 Signal Messenger, LLC +// Copyright 2017-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { isString } from 'lodash'; -import type { MenuItemConstructorOptions } from 'electron'; import type { LocaleMessagesType } from '../ts/types/I18N'; +import type { + MenuListType, + MenuOptionsType, + MenuActionsType, +} from '../ts/types/menu'; -export type MenuListType = Array; - -export type MenuOptionsType = { - // options - development: boolean; - devTools: boolean; - includeSetup: boolean; - isProduction: boolean; - platform: string; - - // actions - forceUpdate: () => unknown; - openContactUs: () => unknown; - openForums: () => unknown; - openJoinTheBeta: () => unknown; - openReleaseNotes: () => unknown; - openSupportPage: () => unknown; - setupAsNewDevice: () => unknown; - setupAsStandalone: () => unknown; - showAbout: () => unknown; - showDebugLog: () => unknown; - showKeyboardShortcuts: () => unknown; - showSettings: () => unknown; - showStickerCreator: () => unknown; - showWindow: () => unknown; -}; +export type CreateTemplateOptionsType = MenuOptionsType & MenuActionsType; export const createTemplate = ( - options: MenuOptionsType, + options: CreateTemplateOptionsType, messages: LocaleMessagesType ): MenuListType => { if (!isString(options.platform)) { @@ -131,7 +110,7 @@ export const createTemplate = ( label: messages.viewMenuResetZoom.message, }, { - accelerator: platform === 'darwin' ? 'Command+=' : 'Control+=', + accelerator: 'CmdOrCtrl+=', role: 'zoomIn', label: messages.viewMenuZoomIn.message, }, @@ -265,7 +244,7 @@ export const createTemplate = ( function updateForMac( template: MenuListType, messages: LocaleMessagesType, - options: MenuOptionsType + options: CreateTemplateOptionsType ): MenuListType { const { showAbout, showSettings, showWindow } = options; diff --git a/app/spell_check.ts b/app/spell_check.ts index 1d9320bcd..59a89dce2 100644 --- a/app/spell_check.ts +++ b/app/spell_check.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'url'; import { maybeParseUrl } from '../ts/util/url'; import type { LocaleType } from './locale'; -import type { MenuListType } from './menu'; +import type { MenuListType } from '../ts/types/menu'; export function getLanguages( userLocale: string, diff --git a/background.html b/background.html index 93370a1a3..1c81d3da1 100644 --- a/background.html +++ b/background.html @@ -23,7 +23,7 @@ img-src 'self' blob: data:; media-src 'self' blob:; object-src 'none'; - script-src 'self' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ='; + script-src 'self' 'sha256-Qu05oqDmBO5fZacm7tr/oerJcqsW0G/XqP4PRCziovc=' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ='; style-src 'self' 'unsafe-inline';" /> Signal @@ -81,6 +81,11 @@ rel="stylesheet" type="text/css" /> +