mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
464 lines
14 KiB
JavaScript
464 lines
14 KiB
JavaScript
// @flow
|
|
import { t } from '@lingui/macro';
|
|
import { type I18n as I18nType } from '@lingui/core';
|
|
import { type ElectronUpdateStatus } from './UpdaterTools';
|
|
import { type FileMetadataAndStorageProviderName } from '../ProjectsStorage';
|
|
import { type ShortcutMap } from '../KeyboardShortcuts/DefaultShortcuts';
|
|
import {
|
|
type MenuDeclarativeItemTemplate,
|
|
type MenuItemTemplate,
|
|
} from '../UI/Menu/Menu.flow';
|
|
import { getElectronAccelerator } from '../KeyboardShortcuts';
|
|
import { isMacLike } from '../Utils/Platform';
|
|
import Window from '../Utils/Window';
|
|
import optionalRequire from '../Utils/OptionalRequire';
|
|
const electron = optionalRequire('electron');
|
|
|
|
const isDesktop = !!electron;
|
|
|
|
export type BuildMainMenuProps = {|
|
|
i18n: I18nType,
|
|
project: ?gdProject,
|
|
canSaveProjectAs: boolean,
|
|
recentProjectFiles: Array<FileMetadataAndStorageProviderName>,
|
|
shortcutMap: ShortcutMap,
|
|
isApplicationTopLevelMenu: boolean,
|
|
hideAskAi: boolean,
|
|
|};
|
|
|
|
export type MainMenuCallbacks = {|
|
|
onChooseProject: () => void,
|
|
onOpenRecentFile: (
|
|
fileMetadataAndStorageProviderName: FileMetadataAndStorageProviderName
|
|
) => Promise<void>,
|
|
onSaveProject: () => Promise<void>,
|
|
onSaveProjectAs: () => void,
|
|
onShowVersionHistory: () => void,
|
|
onCloseProject: () => Promise<boolean>,
|
|
onCloseApp: () => void,
|
|
onExportProject: () => void,
|
|
onInviteCollaborators: () => void,
|
|
onCreateProject: () => void,
|
|
onOpenProjectManager: (open?: boolean) => void,
|
|
onOpenHomePage: () => void,
|
|
onOpenDebugger: () => void,
|
|
onOpenAbout: (open?: boolean) => void,
|
|
onOpenPreferences: (open?: boolean) => void,
|
|
onOpenLanguage: (open?: boolean) => void,
|
|
onOpenProfile: (open?: boolean) => void,
|
|
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
|
setElectronUpdateStatus: ElectronUpdateStatus => void,
|
|
|};
|
|
|
|
export type MainMenuExtraCallbacks = {|
|
|
onClosePreview?: ?(windowId: number) => void,
|
|
|};
|
|
|
|
export type MainMenuEvent =
|
|
| 'main-menu-open'
|
|
| 'main-menu-open-recent'
|
|
| 'main-menu-save'
|
|
| 'main-menu-save-as'
|
|
| 'main-menu-show-version-history'
|
|
| 'main-menu-close'
|
|
| 'main-menu-close-app'
|
|
| 'main-menu-export'
|
|
| 'main-menu-invite-collaborators'
|
|
| 'main-menu-create-project'
|
|
| 'main-menu-create-blank'
|
|
| 'main-menu-open-project-manager'
|
|
| 'main-menu-open-home-page'
|
|
| 'main-menu-open-debugger'
|
|
| 'main-menu-open-about'
|
|
| 'main-menu-open-preferences'
|
|
| 'main-menu-open-language'
|
|
| 'main-menu-open-profile'
|
|
| 'main-menu-open-ask-ai'
|
|
| 'update-status';
|
|
|
|
const getMainMenuEventCallback = (
|
|
mainMenuEvent: string,
|
|
callbacks: MainMenuCallbacks
|
|
): Function => {
|
|
const mapping = {
|
|
'main-menu-open': callbacks.onChooseProject,
|
|
'main-menu-open-recent': callbacks.onOpenRecentFile,
|
|
'main-menu-save': callbacks.onSaveProject,
|
|
'main-menu-save-as': callbacks.onSaveProjectAs,
|
|
'main-menu-show-version-history': callbacks.onShowVersionHistory,
|
|
'main-menu-close': callbacks.onCloseProject,
|
|
'main-menu-close-app': callbacks.onCloseApp,
|
|
'main-menu-export': callbacks.onExportProject,
|
|
'main-menu-invite-collaborators': callbacks.onInviteCollaborators,
|
|
'main-menu-create-project': callbacks.onCreateProject,
|
|
'main-menu-open-project-manager': callbacks.onOpenProjectManager,
|
|
'main-menu-open-home-page': callbacks.onOpenHomePage,
|
|
'main-menu-open-debugger': callbacks.onOpenDebugger,
|
|
'main-menu-open-about': callbacks.onOpenAbout,
|
|
'main-menu-open-preferences': callbacks.onOpenPreferences,
|
|
'main-menu-open-language': callbacks.onOpenLanguage,
|
|
'main-menu-open-profile': callbacks.onOpenProfile,
|
|
'main-menu-open-ask-ai': callbacks.onOpenAskAi,
|
|
'update-status': callbacks.setElectronUpdateStatus,
|
|
};
|
|
|
|
return mapping[mainMenuEvent] || (() => {});
|
|
};
|
|
|
|
export const buildMainMenuDeclarativeTemplate = ({
|
|
shortcutMap,
|
|
i18n,
|
|
recentProjectFiles,
|
|
project,
|
|
canSaveProjectAs,
|
|
isApplicationTopLevelMenu,
|
|
hideAskAi,
|
|
}: BuildMainMenuProps): Array<MenuDeclarativeItemTemplate> => {
|
|
const fileTemplate: MenuDeclarativeItemTemplate = {
|
|
label: i18n._(t`File`),
|
|
submenu: [
|
|
{
|
|
label: i18n._(t`Create a game`),
|
|
accelerator: getElectronAccelerator(shortcutMap['CREATE_NEW_PROJECT']),
|
|
onClickSendEvent: 'main-menu-create-project',
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Open...`),
|
|
accelerator: getElectronAccelerator(shortcutMap['OPEN_PROJECT']),
|
|
onClickSendEvent: 'main-menu-open',
|
|
},
|
|
{
|
|
label: i18n._(t`Open Recent`),
|
|
submenu:
|
|
recentProjectFiles.length > 0
|
|
? recentProjectFiles.map(item => ({
|
|
label: item.fileMetadata.fileIdentifier,
|
|
onClickSendEvent: 'main-menu-open-recent',
|
|
eventArgs: item,
|
|
}))
|
|
: [
|
|
{
|
|
label: i18n._(t`No recent project`),
|
|
enabled: false,
|
|
},
|
|
],
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Save`),
|
|
accelerator: getElectronAccelerator(shortcutMap['SAVE_PROJECT']),
|
|
onClickSendEvent: 'main-menu-save',
|
|
enabled: !!project,
|
|
},
|
|
{
|
|
label: i18n._(t`Save as...`),
|
|
accelerator: getElectronAccelerator(shortcutMap['SAVE_PROJECT_AS']),
|
|
onClickSendEvent: 'main-menu-save-as',
|
|
enabled: canSaveProjectAs,
|
|
},
|
|
{
|
|
label: i18n._(t`Show version history`),
|
|
onClickSendEvent: 'main-menu-show-version-history',
|
|
enabled: !!project,
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Invite collaborators`),
|
|
accelerator: getElectronAccelerator(
|
|
shortcutMap['INVITE_COLLABORATORS']
|
|
),
|
|
onClickSendEvent: 'main-menu-invite-collaborators',
|
|
enabled: !!project,
|
|
},
|
|
{
|
|
label: i18n._(t`Export (web, iOS, Android)...`),
|
|
accelerator: getElectronAccelerator(shortcutMap['EXPORT_GAME']),
|
|
onClickSendEvent: 'main-menu-export',
|
|
enabled: !!project,
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Close Project`),
|
|
accelerator: getElectronAccelerator(shortcutMap['CLOSE_PROJECT']),
|
|
onClickSendEvent: 'main-menu-close',
|
|
enabled: !!project,
|
|
},
|
|
...(!isMacLike() || !isApplicationTopLevelMenu
|
|
? [
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Preferences`),
|
|
onClickSendEvent: 'main-menu-open-preferences',
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
};
|
|
|
|
// The window is only useful on the desktop app. It will be skipped on the web-app.
|
|
const editTemplate: MenuDeclarativeItemTemplate = {
|
|
label: i18n._(t`Edit`),
|
|
submenu: [
|
|
{ label: i18n._(t`Undo`), role: 'undo' },
|
|
{ label: i18n._(t`Redo`), role: 'redo' },
|
|
{ type: 'separator' },
|
|
{ label: i18n._(t`Cut`), role: 'cut' },
|
|
{ label: i18n._(t`Copy`), role: 'copy' },
|
|
{ label: i18n._(t`Paste`), role: 'paste' },
|
|
{ label: i18n._(t`Paste and Match Style`), role: 'pasteandmatchstyle' },
|
|
{ label: i18n._(t`Delete`), role: 'delete' },
|
|
{ label: i18n._(t`Select All`), role: 'selectall' },
|
|
],
|
|
};
|
|
|
|
const viewTemplate: MenuDeclarativeItemTemplate = {
|
|
label: i18n._(t`View`),
|
|
submenu: [
|
|
{
|
|
label: i18n._(t`Show Project Manager`),
|
|
accelerator: getElectronAccelerator(
|
|
shortcutMap['OPEN_PROJECT_MANAGER']
|
|
),
|
|
onClickSendEvent: 'main-menu-open-project-manager',
|
|
enabled: !!project,
|
|
},
|
|
{
|
|
label: i18n._(t`Show Home`),
|
|
onClickSendEvent: 'main-menu-open-home-page',
|
|
},
|
|
{
|
|
label: i18n._(t`Open Debugger`),
|
|
onClickSendEvent: 'main-menu-open-debugger',
|
|
enabled: !!project,
|
|
},
|
|
// Some Electron specific menu items, not shown in the web-app.
|
|
...(!!electron
|
|
? [
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Toggle Developer Tools`),
|
|
role: 'toggledevtools',
|
|
},
|
|
{ type: 'separator' },
|
|
{ label: i18n._(t`Toggle Fullscreen`), role: 'togglefullscreen' },
|
|
]
|
|
: []),
|
|
],
|
|
};
|
|
|
|
// The window is only useful on the desktop app. It will be skipped on the web-app.
|
|
const windowTemplate: MenuDeclarativeItemTemplate = {
|
|
label: i18n._(t`Window`),
|
|
role: 'window',
|
|
submenu: [{ label: i18n._(t`Minimize`), role: 'minimize' }],
|
|
};
|
|
|
|
// The help menu is mostly a collection of links.
|
|
const helpTemplate: MenuDeclarativeItemTemplate = {
|
|
label: i18n._(t`Help`),
|
|
role: 'help',
|
|
submenu: [
|
|
...(hideAskAi
|
|
? []
|
|
: [
|
|
{
|
|
label: i18n._(t`Ask AI (AI agent and chatbot)`),
|
|
onClickSendEvent: 'main-menu-open-ask-ai',
|
|
},
|
|
]),
|
|
{
|
|
label: i18n._(t`GDevelop website`),
|
|
onClickOpenLink: 'http://gdevelop.io',
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`GDevelop games on gd.games`),
|
|
onClickOpenLink: 'https://gd.games',
|
|
},
|
|
{
|
|
label: i18n._(t`Community Forums`),
|
|
onClickOpenLink: 'https://forum.gdevelop.io',
|
|
},
|
|
{
|
|
label: i18n._(t`Community Discord Chat`),
|
|
onClickOpenLink: 'https://discord.gg/gdevelop',
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`YouTube channel (tutorials and more)`),
|
|
onClickOpenLink: 'https://www.youtube.com/c/gdevelopapp',
|
|
},
|
|
{
|
|
label: i18n._(t`TikTok`),
|
|
onClickOpenLink: 'https://tiktok.com/@gdevelop',
|
|
},
|
|
{
|
|
label: i18n._(t`Twitter`),
|
|
onClickOpenLink: 'https://twitter.com/gdevelopapp',
|
|
},
|
|
{
|
|
label: i18n._(t`Instagram`),
|
|
onClickOpenLink: 'https://www.instagram.com/gdevelopapp',
|
|
},
|
|
{
|
|
label: i18n._(t`Facebook`),
|
|
onClickOpenLink: 'https://facebook.com/gdevelopapp',
|
|
},
|
|
{
|
|
label: i18n._(t`Reddit`),
|
|
onClickOpenLink: 'https://www.reddit.com/r/gdevelop',
|
|
},
|
|
{
|
|
label: i18n._(t`GitHub repository`),
|
|
onClickOpenLink: 'https://github.com/4ian/GDevelop',
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Buy GDevelop goodies and swag`),
|
|
onClickOpenLink: 'https://goodies.gdevelop.io',
|
|
},
|
|
{
|
|
label: i18n._(t`Contribute to GDevelop`),
|
|
onClickOpenLink: 'https://gdevelop.io/page/contribute',
|
|
},
|
|
{
|
|
label: i18n._(t`Create Extensions for GDevelop`),
|
|
onClickOpenLink:
|
|
'https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md',
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Help translate GDevelop`),
|
|
onClickOpenLink: 'https://crowdin.com/project/gdevelop',
|
|
},
|
|
{
|
|
label: i18n._(t`Report a wrong translation`),
|
|
onClickOpenLink: 'https://github.com/4ian/GDevelop/issues/969',
|
|
},
|
|
...(isMacLike() && isDesktop
|
|
? []
|
|
: [
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`About GDevelop`),
|
|
onClickSendEvent: 'main-menu-open-about',
|
|
},
|
|
]),
|
|
],
|
|
};
|
|
|
|
// Structure of the menu. Some electron specific menus are not even shown
|
|
// on the web-app, because they would not work and make sense at all.
|
|
const template: Array<MenuDeclarativeItemTemplate> = [
|
|
fileTemplate,
|
|
...(!!electron && isApplicationTopLevelMenu ? [editTemplate] : []),
|
|
...(!!electron && isApplicationTopLevelMenu ? [viewTemplate] : []),
|
|
...(!!electron && isApplicationTopLevelMenu ? [windowTemplate] : []),
|
|
helpTemplate,
|
|
];
|
|
|
|
// macOS has a menu with the name of the app.
|
|
if (isMacLike() && isApplicationTopLevelMenu) {
|
|
template.unshift({
|
|
label: i18n._(t`GDevelop 5`),
|
|
submenu: [
|
|
{
|
|
label: i18n._(t`About GDevelop`),
|
|
onClickSendEvent: 'main-menu-open-about',
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`My Profile`),
|
|
onClickSendEvent: 'main-menu-open-profile',
|
|
},
|
|
{
|
|
label: i18n._(t`Preferences`),
|
|
onClickSendEvent: 'main-menu-open-preferences',
|
|
},
|
|
{
|
|
label: i18n._(t`Language`),
|
|
onClickSendEvent: 'main-menu-open-language',
|
|
},
|
|
{ type: 'separator' },
|
|
{ role: 'services', submenu: [] },
|
|
{ type: 'separator' },
|
|
{ role: 'hide' },
|
|
{ role: 'hideothers' },
|
|
{ role: 'unhide' },
|
|
{ type: 'separator' },
|
|
{ role: 'quit' },
|
|
],
|
|
});
|
|
|
|
// $FlowFixMe - submenu is guaranteed to exist.
|
|
editTemplate.submenu.push(
|
|
{ type: 'separator' },
|
|
{
|
|
label: i18n._(t`Speech`),
|
|
submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }],
|
|
}
|
|
);
|
|
|
|
// $FlowFixMe - submenu is guaranteed to exist.
|
|
windowTemplate.submenu = [
|
|
{ role: 'minimize' },
|
|
{ role: 'zoom' },
|
|
{ type: 'separator' },
|
|
{ role: 'front' },
|
|
];
|
|
}
|
|
|
|
return template;
|
|
};
|
|
|
|
export const adaptFromDeclarativeTemplate = (
|
|
menuDeclarativeTemplate: Array<MenuDeclarativeItemTemplate>,
|
|
callbacks: MainMenuCallbacks
|
|
): Array<MenuItemTemplate> => {
|
|
const adaptMenuDeclarativeItemTemplate = (
|
|
menuTemplate: Array<MenuDeclarativeItemTemplate>
|
|
): Array<MenuItemTemplate> =>
|
|
menuTemplate.map((menuItemTemplate: MenuDeclarativeItemTemplate) => {
|
|
const {
|
|
// $FlowFixMe - property can be undefined.
|
|
onClickSendEvent,
|
|
// $FlowFixMe - property can be undefined.
|
|
onClickOpenLink,
|
|
// $FlowFixMe - property can be undefined.
|
|
eventArgs,
|
|
...menuItemTemplateRest
|
|
} = menuItemTemplate;
|
|
|
|
const hasOnClick = onClickSendEvent || onClickOpenLink;
|
|
|
|
// $FlowFixMe - we're putting both a click and a submenu, so not strictly following the schema.
|
|
return {
|
|
...menuItemTemplateRest,
|
|
click: hasOnClick
|
|
? function() {
|
|
if (menuItemTemplate.onClickSendEvent) {
|
|
const mainMenuEvent = menuItemTemplate.onClickSendEvent;
|
|
const callback = getMainMenuEventCallback(
|
|
mainMenuEvent,
|
|
callbacks
|
|
);
|
|
|
|
if (eventArgs) callback(eventArgs);
|
|
else callback();
|
|
}
|
|
|
|
if (menuItemTemplate.onClickOpenLink) {
|
|
Window.openExternalURL(menuItemTemplate.onClickOpenLink);
|
|
}
|
|
}
|
|
: undefined,
|
|
submenu: menuItemTemplate.submenu
|
|
? adaptMenuDeclarativeItemTemplate(menuItemTemplate.submenu)
|
|
: undefined,
|
|
};
|
|
});
|
|
|
|
return adaptMenuDeclarativeItemTemplate(menuDeclarativeTemplate);
|
|
};
|