diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-09-13 13:33:54 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-09-13 13:33:54 +0200 |
| commit | 343fd4ff64150920f14de07c66385f5bb3fff9ed (patch) | |
| tree | 00cf12cabe0545acec7d9f457d8cdd30ffd2888f /gui/src | |
| parent | 8c7653b4f16d48d39154b149f9c49a0be5209caa (diff) | |
| parent | 6ab3a737364bfa5e593581d968081c5b86bdb372 (diff) | |
| download | mullvadvpn-343fd4ff64150920f14de07c66385f5bb3fff9ed.tar.xz mullvadvpn-343fd4ff64150920f14de07c66385f5bb3fff9ed.zip | |
Merge branch 'detect-macos-scrollbar-visibility'
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/index.ts | 47 | ||||
| -rw-r--r-- | gui/src/main/window-controller.ts | 2 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 16 | ||||
| -rw-r--r-- | gui/src/renderer/components/CustomScrollbars.tsx | 42 | ||||
| -rw-r--r-- | gui/src/renderer/components/MacOsScrollbarDetection.tsx | 41 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBar.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLanguage.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocation.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/SplitTunnelingSettings.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/actions.ts | 19 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/reducers.ts | 6 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 16 |
12 files changed, 173 insertions, 34 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 8fa09576c7..ff7e9e64a7 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -1,4 +1,4 @@ -import { execFile } from 'child_process'; +import { exec, execFile } from 'child_process'; import { app, BrowserWindow, @@ -8,11 +8,13 @@ import { screen, session, shell, + systemPreferences, Tray, } from 'electron'; import os from 'os'; import * as path from 'path'; import { sprintf } from 'sprintf-js'; +import util from 'util'; import * as uuid from 'uuid'; import config from '../config.json'; import { closeToExpiry, hasExpired } from '../shared/account-expiry'; @@ -72,7 +74,9 @@ import { resolveBin } from './proc'; import ReconnectionBackoff from './reconnection-backoff'; import TrayIconController, { TrayIconType } from './tray-icon-controller'; import WindowController from './window-controller'; -import { ITranslations } from '../shared/ipc-schema'; +import { ITranslations, MacOsScrollbarVisibility } from '../shared/ipc-schema'; + +const execAsync = util.promisify(exec); // Only import split tunneling library on correct OS. const linuxSplitTunneling = process.platform === 'linux' && require('./linux-split-tunneling'); @@ -235,6 +239,8 @@ class ApplicationMain { private windowsSplitTunnelingApplications?: IApplication[]; + private macOsScrollbarVisibility?: MacOsScrollbarVisibility; + public run() { // Remove window animations to combat window flickering when opening window. Can be removed when // this issue has been resolved: https://github.com/electron/electron/issues/12130 @@ -437,6 +443,13 @@ class ApplicationMain { ); this.connectToDaemon(); + if (process.platform === 'darwin') { + await this.updateMacOsScrollbarVisibility(); + systemPreferences.subscribeNotification('AppleShowScrollBarsSettingChanged', async () => { + await this.updateMacOsScrollbarVisibility(); + }); + } + const window = await this.createWindow(); const tray = this.createTray(); @@ -1032,7 +1045,7 @@ class ApplicationMain { private registerWindowListener(windowController: WindowController) { windowController.window?.on('focus', () => { - IpcMainEventChannel.windowFocus.notify(windowController.webContents, true); + IpcMainEventChannel.window.notifyFocus(windowController.webContents, true); this.blurNavigationResetScheduler.cancel(); @@ -1049,7 +1062,7 @@ class ApplicationMain { }); windowController.window?.on('blur', () => { - IpcMainEventChannel.windowFocus.notify(windowController.webContents, false); + IpcMainEventChannel.window.notifyFocus(windowController.webContents, false); // ensure notification guard is reset this.notificationController.resetTunnelStateAnnouncements(); @@ -1087,6 +1100,7 @@ class ApplicationMain { wireguardPublicKey: this.wireguardPublicKey, translations: this.translations, windowsSplitTunnelingApplications: this.windowsSplitTunnelingApplications, + macOsScrollbarVisibility: this.macOsScrollbarVisibility, })); IpcMainEventChannel.settings.handleSetAllowLan((allowLan: boolean) => @@ -1994,6 +2008,31 @@ class ApplicationMain { private getProblemReportPath(id: string): string { return path.join(app.getPath('temp'), `${id}.log`); } + + private async updateMacOsScrollbarVisibility(): Promise<void> { + const command = + 'defaults read kCFPreferencesAnyApplication AppleShowScrollBars || echo Automatic'; + const { stdout } = await execAsync(command); + switch (stdout.trim()) { + case 'WhenScrolling': + this.macOsScrollbarVisibility = MacOsScrollbarVisibility.whenScrolling; + break; + case 'Always': + this.macOsScrollbarVisibility = MacOsScrollbarVisibility.always; + break; + case 'Automatic': + default: + this.macOsScrollbarVisibility = MacOsScrollbarVisibility.automatic; + break; + } + + if (this.windowController?.webContents) { + IpcMainEventChannel.window.notifyMacOsScrollbarVisibility( + this.windowController.webContents, + this.macOsScrollbarVisibility, + ); + } + } } const applicationMain = new ApplicationMain(); diff --git a/gui/src/main/window-controller.ts b/gui/src/main/window-controller.ts index 50ceab01e4..298f767fb5 100644 --- a/gui/src/main/window-controller.ts +++ b/gui/src/main/window-controller.ts @@ -230,7 +230,7 @@ export default class WindowController { if (this.window) { const shapeParameters = this.windowPositioning.getWindowShapeParameters(this.window); - IpcMainEventChannel.windowShape.notify(this.webContentsValue, shapeParameters); + IpcMainEventChannel.window.notifyShape(this.webContentsValue, shapeParameters); } } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 3f65002340..cf7a354694 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -4,6 +4,7 @@ import { Router } from 'react-router'; import { bindActionCreators } from 'redux'; import AppRouter from './components/AppRouter'; +import MacOsScrollbarDetection from './components/MacOsScrollbarDetection'; import ErrorBoundary from './components/ErrorBoundary'; import { AppContext } from './context'; @@ -132,7 +133,7 @@ export default class AppRenderer { log.addOutput(new ConsoleOutput(LogLevel.debug)); log.addOutput(new IpcOutput(LogLevel.debug)); - IpcRendererEventChannel.windowShape.listen((windowShapeParams) => { + IpcRendererEventChannel.window.listenShape((windowShapeParams) => { if (typeof windowShapeParams.arrowPosition === 'number') { this.reduxActions.userInterface.updateWindowArrowPosition(windowShapeParams.arrowPosition); } @@ -199,10 +200,14 @@ export default class AppRenderer { this.reduxActions.settings.setSplitTunnelingApplications(applications); }); - IpcRendererEventChannel.windowFocus.listen((focus: boolean) => { + IpcRendererEventChannel.window.listenFocus((focus: boolean) => { this.reduxActions.userInterface.setWindowFocused(focus); }); + IpcRendererEventChannel.window.listenMacOsScrollbarVisibility((visibility) => { + this.reduxActions.userInterface.setMacOsScrollbarVisibility(visibility); + }); + IpcRendererEventChannel.navigation.listenReset(() => this.history.dismiss(true)); // Request the initial state from the main process @@ -237,6 +242,12 @@ export default class AppRenderer { this.storeAutoStart(initialState.autoStart); this.setWireguardPublicKey(initialState.wireguardPublicKey); + if (initialState.macOsScrollbarVisibility !== undefined) { + this.reduxActions.userInterface.setMacOsScrollbarVisibility( + initialState.macOsScrollbarVisibility, + ); + } + if (initialState.isConnected) { void this.onDaemonConnected(); } @@ -265,6 +276,7 @@ export default class AppRenderer { <Router history={this.history.asHistory}> <ErrorBoundary> <AppRouter /> + {window.env.platform === 'darwin' && <MacOsScrollbarDetection />} </ErrorBoundary> </Router> </Provider> diff --git a/gui/src/renderer/components/CustomScrollbars.tsx b/gui/src/renderer/components/CustomScrollbars.tsx index 3dacd26626..139c4381de 100644 --- a/gui/src/renderer/components/CustomScrollbars.tsx +++ b/gui/src/renderer/components/CustomScrollbars.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import styled from 'styled-components'; +import { MacOsScrollbarVisibility } from '../../shared/ipc-schema'; import { Scheduler } from '../../shared/scheduler'; +import { useSelector } from '../redux/store'; const ScrollableContent = styled.div({ display: 'flex', @@ -12,8 +14,8 @@ const ScrollableContent = styled.div({ const AUTOHIDE_TIMEOUT = 1000; interface IProps { - autoHide: boolean; - trackPadding: { x: number; y: number }; + autoHide?: boolean; + trackPadding?: { x: number; y: number }; onScroll?: (value: IScrollEvent) => void; className?: string; fillContainer?: boolean; @@ -44,10 +46,26 @@ interface IScrollbarUpdateContext { position: boolean; } -export default class CustomScrollbars extends React.Component<IProps, IState> { - public static defaultProps: IProps = { - // auto-hide on macOS by default - autoHide: window.env.platform === 'darwin', +export default React.forwardRef(function CustomScrollbarsContainer( + props: IProps, + forwardRef: React.Ref<CustomScrollbars>, +) { + const macOsScrollbarVisibility = useSelector( + (state) => state.userInterface.macOsScrollbarVisibility, + ); + const autoHide = + props.autoHide ?? + (window.env.platform === 'darwin' && + (macOsScrollbarVisibility === undefined || + macOsScrollbarVisibility === MacOsScrollbarVisibility.whenScrolling)); + + return <CustomScrollbars {...props} autoHide={autoHide} ref={forwardRef} />; +}); + +export type CustomScrollbarsRef = CustomScrollbars; + +class CustomScrollbars extends React.Component<IProps, IState> { + public static defaultProps: Partial<IProps> = { trackPadding: { x: 2, y: 2 }, }; @@ -161,8 +179,8 @@ export default class CustomScrollbars extends React.Component<IProps, IState> { return ( prevProps.children !== nextProps.children || prevProps.autoHide !== nextProps.autoHide || - prevProps.trackPadding.x !== nextProps.trackPadding.x || - prevProps.trackPadding.y !== nextProps.trackPadding.y || + prevProps.trackPadding?.x !== nextProps.trackPadding?.x || + prevProps.trackPadding?.y !== nextProps.trackPadding?.y || prevState.canScroll !== nextState.canScroll || prevState.showScrollIndicators !== nextState.showScrollIndicators || prevState.showTrack !== nextState.showTrack || @@ -350,7 +368,7 @@ export default class CustomScrollbars extends React.Component<IProps, IState> { // a thumb at the lowest point matches the bottom of scrollable view const thumbBoundary = this.computeTrackLength(scrollable) - thumb.clientHeight; const thumbTop = - pointInScrollContainer.y - this.state.dragStart.y - this.props.trackPadding.y; + pointInScrollContainer.y - this.state.dragStart.y - (this.props.trackPadding?.y ?? 0); const newScrollTop = (thumbTop / thumbBoundary) * maxScrollTop; scrollable.scrollTop = newScrollTop; @@ -410,7 +428,7 @@ export default class CustomScrollbars extends React.Component<IProps, IState> { } private computeTrackLength(scrollable: HTMLElement) { - return scrollable.offsetHeight - this.props.trackPadding.y * 2; + return scrollable.offsetHeight - (this.props.trackPadding?.y ?? 0) * 2; } // Computes the position of child element within scrollable container @@ -471,10 +489,10 @@ export default class CustomScrollbars extends React.Component<IProps, IState> { // calculate thumb position based on scroll progress and thumb boundary // adding vertical inset to adjust the thumb's appearance - const thumbPosition = thumbBoundary * scrollPosition + this.props.trackPadding.y; + const thumbPosition = thumbBoundary * scrollPosition + (this.props.trackPadding?.y ?? 0); return { - x: -this.props.trackPadding.x, + x: -(this.props.trackPadding?.x ?? 0), y: thumbPosition, }; } diff --git a/gui/src/renderer/components/MacOsScrollbarDetection.tsx b/gui/src/renderer/components/MacOsScrollbarDetection.tsx new file mode 100644 index 0000000000..69de63cc6a --- /dev/null +++ b/gui/src/renderer/components/MacOsScrollbarDetection.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import useActions from '../lib/actionsHook'; +import userInterface from '../redux/userinterface/actions'; +import { useSelector } from '../redux/store'; +import { MacOsScrollbarVisibility } from '../../shared/ipc-schema'; + +const StyledContainer = styled.div({ + position: 'absolute', + visibility: 'hidden', + overflowY: 'scroll', + overflowX: 'hidden', + width: '1px', + height: '0px', +}); + +// This component is used to determine wheter scrollbars should be always visible or only visible +// while scrolling when the system setting for this is set to "Automatic". This is detected by +// testing if any space is taken by a scrollbar. +export default function MacOsScrollbarDetection() { + const visibility = useSelector((state) => state.userInterface.macOsScrollbarVisibility); + const { setMacOsScrollbarVisibility } = useActions(userInterface); + const ref = useRef() as React.RefObject<HTMLDivElement>; + + useEffect(() => { + if (visibility === MacOsScrollbarVisibility.automatic) { + // If the width is 0 then the 1 px width of the parent has been used by the scrollbar. + const newVisibility = + ref.current?.offsetWidth === 0 + ? MacOsScrollbarVisibility.always + : MacOsScrollbarVisibility.whenScrolling; + setMacOsScrollbarVisibility(newVisibility); + } + }, [visibility]); + + return ( + <StyledContainer> + <div ref={ref} /> + </StyledContainer> + ); +} diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx index 9af5a4b615..e02ae99387 100644 --- a/gui/src/renderer/components/NavigationBar.tsx +++ b/gui/src/renderer/components/NavigationBar.tsx @@ -6,7 +6,7 @@ import { useHistory } from '../lib/history'; import { useCombinedRefs } from '../lib/utilityHooks'; import { useSelector } from '../redux/store'; import userInterface from '../redux/userinterface/actions'; -import CustomScrollbars, { IScrollEvent } from './CustomScrollbars'; +import CustomScrollbars, { CustomScrollbarsRef, IScrollEvent } from './CustomScrollbars'; import { StyledBackBarItemButton, StyledBackBarItemIcon, @@ -112,12 +112,12 @@ interface INavigationScrollbarsProps { export const NavigationScrollbars = React.forwardRef(function NavigationScrollbarsT( props: INavigationScrollbarsProps, - forwardedRef?: React.Ref<CustomScrollbars>, + forwardedRef?: React.Ref<CustomScrollbarsRef>, ) { const history = useHistory(); const { onScroll } = useContext(NavigationScrollContext); - const ref = useRef<CustomScrollbars>(); + const ref = useRef<CustomScrollbarsRef>(); const combinedRefs = useCombinedRefs(forwardedRef, ref); const { addScrollPosition, removeScrollPosition } = useActions(userInterface); diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx index 7c67927182..38d20b09e3 100644 --- a/gui/src/renderer/components/SelectLanguage.tsx +++ b/gui/src/renderer/components/SelectLanguage.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import { AriaInputGroup } from './AriaGroup'; -import CustomScrollbars from './CustomScrollbars'; +import { CustomScrollbarsRef } from './CustomScrollbars'; import { Container, Layout } from './Layout'; import { BackBarItem, @@ -40,7 +40,7 @@ const StyledSelector = (styled(Selector)({ }) as unknown) as new <T>() => Selector<T>; export default class SelectLanguage extends React.Component<IProps, IState> { - private scrollView = React.createRef<CustomScrollbars>(); + private scrollView = React.createRef<CustomScrollbarsRef>(); private selectedCellRef = React.createRef<HTMLButtonElement>(); constructor(props: IProps) { diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index 2867ddaf45..528e8129ef 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -6,7 +6,7 @@ import { messages } from '../../shared/gettext'; import { IRelayLocationRedux } from '../redux/settings/reducers'; import { LocationScope } from '../redux/userinterface/reducers'; import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations'; -import CustomScrollbars from './CustomScrollbars'; +import { CustomScrollbarsRef } from './CustomScrollbars'; import ExitLocations from './ExitLocations'; import ImageView from './ImageView'; import { Layout } from './Layout'; @@ -66,7 +66,7 @@ interface ISelectLocationSnapshot { export default class SelectLocation extends React.Component<IProps, IState> { public state = { showFilterMenu: false, headingHeight: 0 }; - private scrollView = React.createRef<CustomScrollbars>(); + private scrollView = React.createRef<CustomScrollbarsRef>(); private spacePreAllocationViewRef = React.createRef<SpacePreAllocationView>(); private selectedExitLocationRef = React.createRef<React.ReactInstance>(); private selectedBridgeLocationRef = React.createRef<React.ReactInstance>(); diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx index 64faf178f2..97a4e5189c 100644 --- a/gui/src/renderer/components/SplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx @@ -11,7 +11,7 @@ import { IReduxState } from '../redux/store'; import Accordion from './Accordion'; import * as AppButton from './AppButton'; import * as Cell from './cell'; -import CustomScrollbars from './CustomScrollbars'; +import { CustomScrollbarsRef } from './CustomScrollbars'; import ImageView from './ImageView'; import { Layout } from './Layout'; import { ModalContainer, ModalAlert, ModalAlertType } from './Modal'; @@ -50,7 +50,7 @@ import { export default function SplitTunneling() { const { pop } = useHistory(); const [browsing, setBrowsing] = useState(false); - const scrollbarsRef = useRef() as React.RefObject<CustomScrollbars>; + const scrollbarsRef = useRef() as React.RefObject<CustomScrollbarsRef>; const scrollToTop = useCallback(() => scrollbarsRef.current?.scrollToTop(true), [scrollbarsRef]); diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts index d0d1378a88..fd08dccd54 100644 --- a/gui/src/renderer/redux/userinterface/actions.ts +++ b/gui/src/renderer/redux/userinterface/actions.ts @@ -1,3 +1,4 @@ +import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema'; import { LocationScope } from './reducers'; export interface IUpdateLocaleAction { @@ -35,6 +36,11 @@ export interface IRemoveScrollPosition { path: string; } +export interface ISetMacOsScrollbarVisibility { + type: 'SET_MACOS_SCROLLBAR_VISIBILITY'; + visibility: MacOsScrollbarVisibility; +} + export type UserInterfaceAction = | IUpdateLocaleAction | IUpdateWindowArrowPositionAction @@ -42,7 +48,8 @@ export type UserInterfaceAction = | ISetLocationScopeAction | ISetWindowFocusedAction | IAddScrollPosition - | IRemoveScrollPosition; + | IRemoveScrollPosition + | ISetMacOsScrollbarVisibility; function updateLocale(locale: string): IUpdateLocaleAction { return { @@ -93,6 +100,15 @@ function removeScrollPosition(path: string): IRemoveScrollPosition { }; } +function setMacOsScrollbarVisibility( + visibility: MacOsScrollbarVisibility, +): ISetMacOsScrollbarVisibility { + return { + type: 'SET_MACOS_SCROLLBAR_VISIBILITY', + visibility, + }; +} + export default { updateLocale, updateWindowArrowPosition, @@ -101,4 +117,5 @@ export default { setWindowFocused, addScrollPosition, removeScrollPosition, + setMacOsScrollbarVisibility, }; diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts index 05e091797e..846007f20a 100644 --- a/gui/src/renderer/redux/userinterface/reducers.ts +++ b/gui/src/renderer/redux/userinterface/reducers.ts @@ -1,3 +1,4 @@ +import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema'; import { ReduxAction } from '../store'; export enum LocationScope { @@ -12,6 +13,7 @@ export interface IUserInterfaceReduxState { locationScope: LocationScope; windowFocused: boolean; scrollPosition: Record<string, [number, number]>; + macOsScrollbarVisibility?: MacOsScrollbarVisibility; } const initialState: IUserInterfaceReduxState = { @@ -20,6 +22,7 @@ const initialState: IUserInterfaceReduxState = { locationScope: LocationScope.relay, windowFocused: false, scrollPosition: {}, + macOsScrollbarVisibility: undefined, }; export default function ( @@ -54,6 +57,9 @@ export default function ( return { ...state, scrollPosition }; } + case 'SET_MACOS_SCROLLBAR_VISIBILITY': + return { ...state, macOsScrollbarVisibility: action.visibility }; + default: return state; } diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index a20213f424..cd39ac8c8a 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -39,6 +39,12 @@ export interface IRelayListPair { export type LaunchApplicationResult = { success: true } | { error: string }; +export enum MacOsScrollbarVisibility { + always, + whenScrolling, + automatic, +} + export interface IAppStateSnapshot { isConnected: boolean; autoStart: boolean; @@ -53,6 +59,7 @@ export interface IAppStateSnapshot { wireguardPublicKey?: IWireguardPublicKey; translations: ITranslations; windowsSplitTunnelingApplications?: IApplication[]; + macOsScrollbarVisibility?: MacOsScrollbarVisibility; } // The different types of requests are: @@ -98,11 +105,10 @@ export const ipcSchema = { state: { get: invokeSync<void, IAppStateSnapshot>(), }, - windowShape: { - '': notifyRenderer<IWindowShapeParameters>(), - }, - windowFocus: { - '': notifyRenderer<boolean>(), + window: { + shape: notifyRenderer<IWindowShapeParameters>(), + focus: notifyRenderer<boolean>(), + macOsScrollbarVisibility: notifyRenderer<MacOsScrollbarVisibility>(), }, navigation: { reset: notifyRenderer<void>(), |
