Compare commits

...

2 Commits

Author SHA1 Message Date
Florian Rival
ffa7708a6d Add back the project manager icon in the home page for now
To avoid disturbing existing users too much and because otherwise it's super hidden in the main menu.
Will be better once "existing projects" listing is improved with a button to open the project manager.

Don't show in changelog
2022-12-08 16:13:13 +01:00
Florian Rival
2185c25e35 Improve menus on the web-app
* Faster navigation: submenus open when their item is hovered.
* Shortcuts are now shown like on the desktop app.
* Menu items height are fixed and always the same.
2022-12-08 16:00:20 +01:00
4 changed files with 155 additions and 37 deletions

View File

@@ -1,25 +1,29 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { I18n } from '@lingui/react'; import { I18n } from '@lingui/react';
import { Trans } from '@lingui/macro'; import { t, Trans } from '@lingui/macro';
import TranslateIcon from '@material-ui/icons/Translate'; import TranslateIcon from '@material-ui/icons/Translate';
import FlatButton from '../../../UI/FlatButton'; import FlatButton from '../../../UI/FlatButton';
import { Column } from '../../../UI/Grid'; import { Column } from '../../../UI/Grid';
import { LineStackLayout } from '../../../UI/Layout'; import { LineStackLayout } from '../../../UI/Layout';
import UserChip from '../../../UI/User/UserChip'; import UserChip from '../../../UI/User/UserChip';
import ProjectManager from '../../../UI/CustomSvgIcons/ProjectManager';
import Window from '../../../Utils/Window'; import Window from '../../../Utils/Window';
import optionalRequire from '../../../Utils/OptionalRequire'; import optionalRequire from '../../../Utils/OptionalRequire';
import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer'; import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer';
import TextButton from '../../../UI/TextButton'; import TextButton from '../../../UI/TextButton';
import IconButton from '../../../UI/IconButton';
const electron = optionalRequire('electron'); const electron = optionalRequire('electron');
type Props = {| type Props = {|
hasProject: boolean,
onOpenProjectManager: () => void, onOpenProjectManager: () => void,
onOpenProfile: () => void, onOpenProfile: () => void,
onOpenLanguageDialog: () => void, onOpenLanguageDialog: () => void,
|}; |};
export const HomePageHeader = ({ export const HomePageHeader = ({
hasProject,
onOpenProjectManager, onOpenProjectManager,
onOpenProfile, onOpenProfile,
onOpenLanguageDialog, onOpenLanguageDialog,
@@ -30,11 +34,21 @@ export const HomePageHeader = ({
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (
<LineStackLayout <LineStackLayout
justifyContent="flex-end" justifyContent="space-between"
alignItems="center" alignItems="center"
noMargin noMargin
expand expand
> >
<IconButton
size="small"
id="main-toolbar-project-manager-button"
onClick={onOpenProjectManager}
tooltip={t`Project Manager`}
color="default"
disabled={!hasProject}
>
<ProjectManager />
</IconButton>
<Column> <Column>
<LineStackLayout noMargin alignItems="center"> <LineStackLayout noMargin alignItems="center">
{!electron && windowWidth !== 'small' && ( {!electron && windowWidth !== 'small' && (

View File

@@ -132,13 +132,20 @@ export const HomePage = React.memo<Props>(
if (setToolbar) if (setToolbar)
setToolbar( setToolbar(
<HomePageHeader <HomePageHeader
hasProject={!!project}
onOpenLanguageDialog={onOpenLanguageDialog} onOpenLanguageDialog={onOpenLanguageDialog}
onOpenProfile={onOpenProfile} onOpenProfile={onOpenProfile}
onOpenProjectManager={onOpenProjectManager} onOpenProjectManager={onOpenProjectManager}
/> />
); );
}, },
[setToolbar, onOpenLanguageDialog, onOpenProfile, onOpenProjectManager] [
setToolbar,
onOpenLanguageDialog,
onOpenProfile,
onOpenProjectManager,
project,
]
); );
const forceUpdateEditor = React.useCallback(() => { const forceUpdateEditor = React.useCallback(() => {

View File

@@ -1,6 +1,10 @@
// @flow // @flow
import { isMacLike, isMobile } from '../Utils/Platform'; import { isMacLike, isMobile } from '../Utils/Platform';
/**
* Transform a Electron-like accelerator string (https://www.electronjs.org/docs/latest/api/accelerator)
* so that it's user friendly.
*/
export const adaptAcceleratorString = (accelerator: string): string => { export const adaptAcceleratorString = (accelerator: string): string => {
if (isMobile()) { if (isMobile()) {
return ''; // Do not display accelerators on mobile devices return ''; // Do not display accelerators on mobile devices
@@ -13,13 +17,23 @@ export const adaptAcceleratorString = (accelerator: string): string => {
.replace(/Alt\+/, '⌥') .replace(/Alt\+/, '⌥')
.replace(/Option\+/, '⌥') .replace(/Option\+/, '⌥')
.replace(/Delete/, '⌦') .replace(/Delete/, '⌦')
.replace(/Backspace/, '⌫'); .replace(/Backspace/, '⌫')
.replace(/numadd/, '+')
.replace(/numsub/, '-')
.replace(/num1/, '1')
.replace(/num2/, '2')
.replace(/num3/, '3');
} else { } else {
return accelerator return accelerator
.replace(/CmdOrCtrl\+/, 'Ctrl+') .replace(/CmdOrCtrl\+/, 'Ctrl+')
.replace(/CommandOrControl\+/, 'Ctrl+') .replace(/CommandOrControl\+/, 'Ctrl+')
.replace(/Super\+/, 'Win+') .replace(/Super\+/, 'Win+')
.replace(/Option\+/, 'Alt+') .replace(/Option\+/, 'Alt+')
.replace(/Delete/, 'DEL'); .replace(/Delete/, 'DEL')
.replace(/numadd/, '+')
.replace(/numsub/, '-')
.replace(/num1/, '1')
.replace(/num2/, '2')
.replace(/num3/, '3');
} }
}; };

View File

@@ -1,4 +1,5 @@
import React, { useState, useRef, useCallback } from 'react'; // @flow
import * as React from 'react';
import Menu from '@material-ui/core/Menu'; import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import ArrowRightIcon from '@material-ui/icons/ArrowRight';
@@ -8,18 +9,73 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import Divider from '@material-ui/core/Divider'; import Divider from '@material-ui/core/Divider';
import Fade from '@material-ui/core/Fade'; import Fade from '@material-ui/core/Fade';
import makeStyles from '@material-ui/styles/makeStyles';
import { adaptAcceleratorString } from '../AcceleratorString';
import { type MenuItemTemplate } from './Menu.flow';
const useStyles = makeStyles({
popOverRoot: {
// Put a `pointer-events: none` on the root of the "popover" which is showing
// submenus as only the menu is supposed to receive clicks.
pointerEvents: 'none',
},
});
const styles = { const styles = {
menuItemWithSubMenu: { justifyContent: 'space-between' },
divider: { marginLeft: 16, marginRight: 16 }, divider: { marginLeft: 16, marginRight: 16 },
labelWithAccelerator: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
accelerator: { opacity: 0.65, marginLeft: 16 },
menuItemWithSubMenu: { height: 32, justifyContent: 'space-between' },
menuItem: {
// Force every menu item to have the same height, even if it's a submenu
// or if it has an icon.
height: 32,
},
}; };
const SubMenuItem = ({ item, buildFromTemplate }) => { const SubMenuItem = ({ item, buildFromTemplate }) => {
const [menuOpen, setMenuOpen] = useState(false); const popoverStyles = useStyles();
const anchorElement = useRef(null); const currentlyHovering = React.useRef(false);
const setAnchorElement = useCallback(element => { const [anchorElement, setAnchorElement] = React.useState(null);
anchorElement.current = element;
}, []); const handleOpen = event => {
// $FlowFixMe - even if not defined, not a problem.
if (item.enabled === false) {
return;
}
if (!anchorElement) {
setAnchorElement(event.currentTarget);
}
};
function handleHover() {
// When we hover the menu item or the submenu, we remember it
// so it's not closed.
currentlyHovering.current = true;
}
function handleClose() {
setAnchorElement(null);
}
function handleLeave() {
// Unless overwrote in the meantime, we consider that
// we're not hovering the menu anymore...
currentlyHovering.current = false;
// ...But give 75ms to the user before closing the menu,
// if it the menu or the item was not hovered again in the meantime.
setTimeout(() => {
if (!currentlyHovering.current) {
handleClose();
}
}, 75);
}
return ( return (
<React.Fragment> <React.Fragment>
@@ -27,27 +83,39 @@ const SubMenuItem = ({ item, buildFromTemplate }) => {
dense dense
style={styles.menuItemWithSubMenu} style={styles.menuItemWithSubMenu}
key={item.label} key={item.label}
disabled={item.enabled === false} disabled={
onClick={event => { // $FlowFixMe - even if not defined, not a problem.
if (item.enabled === false) { item.enabled === false
return;
} }
onClick={handleOpen}
if (!anchorElement.current) { onPointerOver={handleOpen}
setAnchorElement(event.currentTarget); onPointerLeave={handleLeave}
}
setMenuOpen(!menuOpen);
}}
> >
{item.label} {item.label}
<ArrowRightIcon ref={anchorElement} /> <ArrowRightIcon />
</MenuItem> </MenuItem>
<Menu <Menu
open={menuOpen} open={!!anchorElement}
anchorEl={anchorElement.current} anchorEl={anchorElement}
onClose={() => setMenuOpen(false)} onClose={handleClose}
TransitionComponent={Fade} TransitionComponent={Fade}
MenuListProps={{
onPointerEnter: handleHover,
onPointerLeave: handleLeave,
// Only the menu, when shown, can receive clicks
// (not the background, see `popoverStyles.popOverRoot`).
style: { pointerEvents: 'auto' },
}}
getContentAnchorEl={
// Counterintuitive, but necessary
// as per https://github.com/mui/material-ui/issues/7961#issuecomment-326116559.
null
}
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
PopoverClasses={{
root: popoverStyles.popOverRoot,
}}
> >
{buildFromTemplate(item.submenu)} {buildFromTemplate(item.submenu)}
</Menu> </Menu>
@@ -70,19 +138,19 @@ const SubMenuItem = ({ item, buildFromTemplate }) => {
* - submenu * - submenu
*/ */
export default class MaterialUIMenuImplementation { export default class MaterialUIMenuImplementation {
constructor({ onClose }) { _onClose: () => void;
constructor({ onClose }: {| onClose: () => void |}) {
this._onClose = onClose; this._onClose = onClose;
} }
buildFromTemplate(template) { buildFromTemplate(template: Array<MenuItemTemplate>) {
return template return template
.map((item, id) => { .map((item, id) => {
if (item.visible === false) return null; if (item.visible === false) return null;
// Accelerator is not implemented for Material-UI menus const accelerator = item.accelerator
// const accelerator = item.accelerator ? adaptAcceleratorString(item.accelerator)
// ? adaptAcceleratorString(item.accelerator) : undefined;
// : undefined;
if (item.type === 'separator') { if (item.type === 'separator') {
return <Divider key={'separator' + id} style={styles.divider} />; return <Divider key={'separator' + id} style={styles.divider} />;
@@ -91,8 +159,14 @@ export default class MaterialUIMenuImplementation {
<MenuItem <MenuItem
dense dense
key={'checkbox' + item.label} key={'checkbox' + item.label}
checked={item.checked} checked={
disabled={item.enabled === false} // $FlowFixMe - existence should be inferred by Flow.
item.checked
}
disabled={
// $FlowFixMe - existence should be inferred by Flow.
item.enabled === false
}
onClick={() => { onClick={() => {
if (item.enabled === false) { if (item.enabled === false) {
return; return;
@@ -103,6 +177,7 @@ export default class MaterialUIMenuImplementation {
} }
this._onClose(); this._onClose();
}} }}
style={styles.menuItem}
> >
<ListItemIcon> <ListItemIcon>
{item.checked ? <CheckBoxIcon /> : <CheckBoxOutlineBlankIcon />} {item.checked ? <CheckBoxIcon /> : <CheckBoxOutlineBlankIcon />}
@@ -134,8 +209,16 @@ export default class MaterialUIMenuImplementation {
this._onClose(); this._onClose();
} }
}} }}
style={styles.menuItem}
> >
{item.label} {!accelerator ? (
item.label
) : (
<div style={styles.labelWithAccelerator}>
<span>{item.label}</span>
<span style={styles.accelerator}>{accelerator}</span>
</div>
)}
</MenuItem> </MenuItem>
); );
} }