diff --git a/actions/windows.js b/actions/windows.js index d12ae14b4..e2b00e063 100644 --- a/actions/windows.js +++ b/actions/windows.js @@ -17,6 +17,7 @@ export const CLOSE_ALL_WINDOWS = 'CLOSE_ALL_WINDOWS'; export const REGISTER_WINDOW = 'REGISTER_WINDOW'; export const UNREGISTER_WINDOW = 'UNREGISTER_WINDOW'; export const RAISE_WINDOW = 'RAISE_WINDOW'; +export const SET_MENU_MARGIN = 'SET_MENU_MARGIN'; export const SET_SPLIT_SCREEN = 'SET_SPLIT_SCREEN'; export const NotificationType = { @@ -86,3 +87,11 @@ export function setSplitScreen(windowId, side, size) { size: size }; } + +export function setMenuMargin(right, left) { + return { + type: SET_MENU_MARGIN, + right: right, + left: left + }; +} diff --git a/components/AppMenu.jsx b/components/AppMenu.jsx index 718ed1621..285f6bb73 100644 --- a/components/AppMenu.jsx +++ b/components/AppMenu.jsx @@ -13,6 +13,7 @@ import mousetrap from 'mousetrap'; import {remove as removeDiacritics} from 'diacritics'; import isEmpty from 'lodash.isempty'; import classnames from 'classnames'; +import {setMenuMargin} from '../actions/windows'; import {setCurrentTask} from '../actions/task'; import InputContainer from '../components/InputContainer'; import LocaleUtils from '../utils/LocaleUtils'; @@ -32,10 +33,12 @@ class AppMenu extends React.Component { currentTaskBlocked: PropTypes.bool, currentTheme: PropTypes.object, keepMenuOpen: PropTypes.bool, + menuCompact: PropTypes.bool, menuItems: PropTypes.array, onMenuToggled: PropTypes.func, openExternalUrl: PropTypes.func, setCurrentTask: PropTypes.func, + setMenuMargin: PropTypes.func, showFilterField: PropTypes.bool, showOnStartup: PropTypes.bool }; @@ -168,14 +171,16 @@ class AppMenu extends React.Component { if (!this.state.menuVisible && this.props.appMenuClearsTask) { this.props.setCurrentTask(null); } - if (!this.state.menuVisible) { - document.addEventListener('click', this.checkCloseMenu); - document.addEventListener('keydown', this.onKeyPress, true); - document.addEventListener('mousemove', this.onMouseMove, true); - } else { - document.removeEventListener('click', this.checkCloseMenu); - document.removeEventListener('keydown', this.onKeyPress, true); - document.removeEventListener('mousemove', this.onMouseMove, true); + if (!this.props.keepMenuOpen) { + if (!this.state.menuVisible) { + document.addEventListener('click', this.checkCloseMenu); + document.addEventListener('keydown', this.onKeyPress, true); + document.addEventListener('mousemove', this.onMouseMove, true); + } else { + document.removeEventListener('click', this.checkCloseMenu); + document.removeEventListener('keydown', this.onKeyPress, true); + document.removeEventListener('mousemove', this.onMouseMove, true); + } } this.props.onMenuToggled(!this.state.menuVisible); this.setState((state) => ({menuVisible: !state.menuVisible, submenusVisible: [], filter: ""})); @@ -262,12 +267,12 @@ class AppMenu extends React.Component { } }; render() { - let className = ""; - if (this.props.currentTaskBlocked) { - className = "appmenu-blocked"; - } else if (this.state.menuVisible) { - className = "appmenu-visible"; - } + const visible = !this.props.currentTaskBlocked && this.state.menuVisible; + const className = classnames({ + "appmenu-blocked": this.props.currentTaskBlocked, + "appmenu-visible": visible, + "appmenu-compact": this.props.menuCompact + }); const filter = removeDiacritics(this.state.filter.toLowerCase()); return (
{ this.menuEl = el; MiscUtils.setupKillTouchEvents(el); }} @@ -275,7 +280,7 @@ class AppMenu extends React.Component {
{this.props.buttonContents}
-
+
    {this.props.showFilterField ? (
  • @@ -302,6 +307,12 @@ class AppMenu extends React.Component { mousetrap(el).bind(this.props.appMenuShortcut, this.toggleMenu); } }; + storeRightMargin = (el) => { + if (this.props.menuCompact && el?.clientWidth > 0) { + const rightmargin = el.clientWidth - MiscUtils.convertEmToPx(11.5); + this.props.setMenuMargin(rightmargin, 0); + } + }; itemAllowed = (item) => { if (!ThemeUtils.themFlagsAllowed(this.props.currentTheme, item.themeFlagWhitelist, item. themeFlagBlacklist)) { return false; @@ -323,5 +334,6 @@ export default connect((state) => ({ currentTaskBlocked: state.task.blocked, currentTheme: state.theme.current || {} }), { - setCurrentTask: setCurrentTask + setCurrentTask: setCurrentTask, + setMenuMargin: setMenuMargin })(AppMenu); diff --git a/components/ResizeableWindow.jsx b/components/ResizeableWindow.jsx index b40f79002..8c75bf8d5 100644 --- a/components/ResizeableWindow.jsx +++ b/components/ResizeableWindow.jsx @@ -41,6 +41,7 @@ class ResizeableWindow extends React.Component { maxHeight: PropTypes.number, maxWidth: PropTypes.number, maximizeable: PropTypes.bool, + menuMargins: PropTypes.object, minHeight: PropTypes.number, minWidth: PropTypes.number, minimizeable: PropTypes.bool, @@ -117,7 +118,7 @@ class ResizeableWindow extends React.Component { componentWillUnmount() { this.props.unregisterWindow(this.id); if (this.props.splitScreenWhenDocked) { - this.props.setSplitScreen(this.id, null); + this.props.setSplitScreen(this.id, null, null); } } componentDidUpdate(prevProps, prevState) { @@ -135,7 +136,7 @@ class ResizeableWindow extends React.Component { (!this.props.visible && prevProps.visible) || (this.state.geometry.docked === false && prevState.geometry.docked !== false) ) { - this.props.setSplitScreen(this.id, null); + this.props.setSplitScreen(this.id, null, null); } else if (this.props.visible && this.state.geometry.docked) { const dockSide = this.props.dockable === true ? "left" : this.props.dockable; const dockSize = ["left", "right"].includes(dockSide) ? this.state.geometry.width : this.state.geometry.height; @@ -169,7 +170,10 @@ class ResizeableWindow extends React.Component { "resizeable-window-body-scrollable": this.props.scrollable, "resizeable-window-body-nonscrollable": !this.props.scrollable }); - const style = {display: this.props.visible ? 'initial' : 'none'}; + const style = { + display: this.props.visible ? 'initial' : 'none', + right: 'calc(0.25em + ' + this.props.menuMargins.right + 'px)' + }; const maximized = this.state.geometry.maximized ? true : false; const minimized = this.state.geometry.minimized ? true : false; const zIndex = this.props.baseZIndex + this.props.windowStacking.findIndex(item => item === this.id); @@ -327,7 +331,8 @@ class ResizeableWindow extends React.Component { export default connect((state) => ({ windowStacking: state.windows.stacking, topbarHeight: state.map.topbarHeight, - bottombarHeight: state.map.bottombarHeight + bottombarHeight: state.map.bottombarHeight, + menuMargins: state.windows.menuMargins }), { raiseWindow: raiseWindow, registerWindow: registerWindow, diff --git a/components/SideBar.jsx b/components/SideBar.jsx index 908469f58..a8ef661b4 100644 --- a/components/SideBar.jsx +++ b/components/SideBar.jsx @@ -26,6 +26,7 @@ class SideBar extends React.Component { heightResizeable: PropTypes.bool, icon: PropTypes.string, id: PropTypes.string.isRequired, + menuMargins: PropTypes.object, minWidth: PropTypes.string, onHide: PropTypes.func, onShow: PropTypes.func, @@ -80,6 +81,7 @@ class SideBar extends React.Component { const render = visible || this.state.render || this.props.renderWhenHidden; const style = { width: this.props.width, + right: visible ? 'calc(0.25em + ' + this.props.menuMargins.right + 'px)' : 0, minWidth: this.props.minWidth, zIndex: visible ? 5 : 4 }; @@ -166,7 +168,8 @@ class SideBar extends React.Component { } const selector = (state) => ({ - currentTask: state.task + currentTask: state.task, + menuMargins: state.windows.menuMargins }); export default connect(selector, { diff --git a/components/style/AppMenu.css b/components/style/AppMenu.css index 1641ff85d..a951b10c4 100644 --- a/components/style/AppMenu.css +++ b/components/style/AppMenu.css @@ -14,6 +14,10 @@ div.AppMenu.appmenu-visible { overflow: visible; } +div.AppMenu.appmenu-visible.appmenu-compact { + background: none; +} + div.AppMenu .appmenu-label { font-weight: bold; transition: color 0.25s; @@ -53,6 +57,20 @@ div.AppMenu div.appmenu-menu-container { max-height: calc(var(--vh, 1vh) * 100 - 5.8em); } +div.AppMenu.appmenu-compact div.appmenu-menu-container { + right: -11.5em; + width: 15em; + height: calc(100vh - 86px); + transition: transform 0.25s, opacity 0.25s, right 0.5s; + background: var(--app-menu-bg-color); + box-shadow: 0px 0px 4px rgba(136, 136, 136, 0.5); + top: 3.7em; +} + +div.AppMenu.appmenu-compact div.appmenu-menu-container:hover { + right: 0; +} + div.AppMenu ul.appmenu-menu { text-align: left; padding: 0; diff --git a/components/style/ResizeableWindow.css b/components/style/ResizeableWindow.css index 8740282c8..0b4d1dec3 100644 --- a/components/style/ResizeableWindow.css +++ b/components/style/ResizeableWindow.css @@ -6,6 +6,7 @@ div.resizeable-window-container { right: 0; pointer-events: none; } + div.resizeable-window { pointer-events: auto; background-color: var(--container-bg-color); diff --git a/doc/plugins.md b/doc/plugins.md index 5f7a21c52..c71492f1c 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -763,6 +763,7 @@ Top bar, containing the logo, searchbar, task buttons and app menu. | Property | Type | Description | Default value | |----------|------|-------------|---------------| | appMenuClearsTask | `bool` | Whether opening the app menu clears the active task. | `undefined` | +| appMenuCompact | `bool` | Whether show an appMenu compact (menu visible on icons hover) - Only available for desktop client. | `undefined` | | appMenuFilterField | `bool` | Whether to display the filter field in the app menu. | `undefined` | | appMenuShortcut | `string` | The shortcut for tiggering the app menu, i.e. alt+shift+m. | `undefined` | | appMenuVisibleOnStartup | `bool` | Whether to open the app menu on application startup. | `undefined` | diff --git a/plugins/LoginUser.jsx b/plugins/LoginUser.jsx index 9013949c2..e36e1744e 100644 --- a/plugins/LoginUser.jsx +++ b/plugins/LoginUser.jsx @@ -7,6 +7,8 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; import Icon from '../components/Icon'; import ConfigUtils from '../utils/ConfigUtils'; import './style/LoginUser.css'; @@ -15,17 +17,29 @@ import './style/LoginUser.css'; /** * Displays the currently logged in user. */ -export default class LoginUser extends React.Component { +class LoginUser extends React.Component { + static propTypes = { + mapMargins: PropTypes.object + }; render() { const username = ConfigUtils.getConfigProp("username"); + const right = this.props.mapMargins.right; + const style = { + right: 'calc(0.25em + ' + right + 'px)' + }; if (!username) { return null; } return ( -
    +
    {username}
    ); } } + +export default connect((state) => ({ + mapMargins: state.windows.mapMargins +}))(LoginUser); + diff --git a/plugins/TopBar.jsx b/plugins/TopBar.jsx index db4428c05..9f169357e 100644 --- a/plugins/TopBar.jsx +++ b/plugins/TopBar.jsx @@ -28,6 +28,8 @@ class TopBar extends React.Component { static propTypes = { /** Whether opening the app menu clears the active task. */ appMenuClearsTask: PropTypes.bool, + /** Whether show an appMenu compact (menu visible on icons hover) - Only available for desktop client. */ + appMenuCompact: PropTypes.bool, /** Whether to display the filter field in the app menu. */ appMenuFilterField: PropTypes.bool, /** The shortcut for tiggering the app menu, i.e. alt+shift+m. */ @@ -99,7 +101,7 @@ class TopBar extends React.Component { let logo; const assetsPath = ConfigUtils.getAssetsPath(); const tooltip = LocaleUtils.tr("appmenu.menulabel"); - if (this.props.mobile) { + if (this.props.mobile || this.props.appMenuCompact) { buttonContents = ( @@ -128,6 +130,12 @@ class TopBar extends React.Component { const searchOptions = {...this.props.searchOptions}; searchOptions.minScaleDenom = searchOptions.minScaleDenom || searchOptions.minScale; delete searchOptions.minScale; + // Menu compact only available for desktop client + const menuCompact = !this.props.mobile ? this.props.appMenuCompact : false; + // Keep menu open when appMenu is in compact mode (Visible on Hover) + const keepMenuOpen = menuCompact; + // Menu should be visible on startup when appMenu is in compact mode (Visible on Hover) + const showOnStartup = this.props.appMenuVisibleOnStartup || menuCompact; return ( this.props.toggleFullscreen(false)} @@ -150,10 +158,12 @@ class TopBar extends React.Component { appMenuClearsTask={this.props.appMenuClearsTask} appMenuShortcut={this.props.appMenuShortcut} buttonContents={buttonContents} + keepMenuOpen={keepMenuOpen} + menuCompact={menuCompact} menuItems={this.props.menuItems} openExternalUrl={this.openUrl} showFilterField={this.props.appMenuFilterField} - showOnStartup={this.props.appMenuVisibleOnStartup} /> + showOnStartup={showOnStartup} /> ) : null} {this.props.components.FullscreenSwitcher ? ( diff --git a/reducers/windows.js b/reducers/windows.js index 87a2d37f7..8544fa02e 100644 --- a/reducers/windows.js +++ b/reducers/windows.js @@ -14,7 +14,8 @@ import { REGISTER_WINDOW, UNREGISTER_WINDOW, RAISE_WINDOW, - SET_SPLIT_SCREEN + SET_SPLIT_SCREEN, + SET_MENU_MARGIN } from '../actions/windows'; const defaultState = { @@ -23,6 +24,9 @@ const defaultState = { mapMargins: { left: 0, top: 0, right: 0, bottom: 0 }, + menuMargins: { + left: 0, right: 0 + }, entries: {} }; @@ -95,9 +99,9 @@ export default function windows(state = defaultState, action) { } const splitWindows = Object.values(newSplitScreen); const mapMargins = { - right: splitWindows.filter(entry => entry.side === 'right').reduce((res, e) => Math.max(e.size, res), 0), + right: splitWindows.filter(entry => entry.side === 'right').reduce((res, e) => Math.max(e.size, res), 0) + state.menuMargins.right, bottom: splitWindows.filter(entry => entry.side === 'bottom').reduce((res, e) => Math.max(e.size, res), 0), - left: splitWindows.filter(entry => entry.side === 'left').reduce((res, e) => Math.max(e.size, res), 0), + left: splitWindows.filter(entry => entry.side === 'left').reduce((res, e) => Math.max(e.size, res), 0) + state.menuMargins.left, top: splitWindows.filter(entry => entry.side === 'top').reduce((res, e) => Math.max(e.size, res), 0) }; return { @@ -106,6 +110,19 @@ export default function windows(state = defaultState, action) { mapMargins: mapMargins }; } + case SET_MENU_MARGIN: { + const menuMargins = { + right: action.right, + left: action.left + }; + const mapMargins = { + right: state.mapMargins.right + action.right, + bottom: state.mapMargins.bottom, + left: state.mapMargins.left + action.left, + top: state.mapMargins.top + }; + return {...state, menuMargins: menuMargins, mapMargins: mapMargins}; + } default: return state; } diff --git a/utils/MiscUtils.js b/utils/MiscUtils.js index 983c7d260..d7f5c8838 100644 --- a/utils/MiscUtils.js +++ b/utils/MiscUtils.js @@ -110,6 +110,10 @@ const MiscUtils = { return 'https:' + url.substr(5); } return url; + }, + convertEmToPx(emsize) { + const defaultfontsize = getComputedStyle(document.documentElement).fontSize; + return emsize * parseFloat(defaultfontsize); } };