mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Async execution controls (#7592)
- Closes https://github.com/enso-org/cloud-v2/issues/619 - Async execution controls # Important Notes There is no design for this, as such, implementation details use placeholder designs. - The context menu uses a play icon. An icon similar in style to the "copy" icon *may* work to represent "run in background", but it may be difficult to visually represent that it is being run in the background, without obstructing it with a foreground window - The icon for projects being run in the background have a green tint, to distinguish them from projects that will be (or are currently) opened in the editor. - this will ***almost certainly*** need to be replaced with a proper design - This *may* also make sense for the local backend, *however* as I don't know whether there is a way to access the completion progress of execution from the PM API, local backend support is currently *not* implemented in this PR. - On a related note: as far as I am aware, there is also no such endpoint for the cloud backend. However, support for async execution was recently added, so I am adding the basic functionality corresponding to the `executeAsync` project state. - Whether a project is being run in the background is currently lost on refresh. This is because the async execution state is currently not sent by the backend. - Placeholder shortcuts have been added (Shift+Enter - Cmd+Enter is already taken by the "share" action, and shift+double click). These are totally optional, and can easily be removed.
This commit is contained in:
parent
4213bf9983
commit
58fbd8e9e8
5
app/ide-desktop/lib/assets/play2.svg
Normal file
5
app/ide-desktop/lib/assets/play2.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.48492 3.22332L11.5227 7.12545C12.2095 7.50624 12.2095 8.49379 11.5227 8.87458L4.48491 12.7767C3.8184 13.1462 3.00001 12.6642 3.00001 11.9021L3.00001 4.09789C3.00001 3.33578 3.8184 2.85377 4.48492 3.22332Z"
|
||||
fill="black" />
|
||||
</svg>
|
After Width: | Height: | Size: 356 B |
@ -603,6 +603,7 @@ export interface ProjectUpdateRequestBody {
|
||||
/** HTTP request body for the "open project" endpoint. */
|
||||
export interface OpenProjectRequestBody {
|
||||
forceCreate: boolean
|
||||
executeAsync: boolean
|
||||
}
|
||||
|
||||
/** HTTP request body for the "create secret" endpoint. */
|
||||
|
@ -110,6 +110,23 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: asset.id,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{asset.type === backendModule.AssetType.project &&
|
||||
backend.type === backendModule.BackendType.remote && (
|
||||
<MenuEntry
|
||||
hidden={hidden}
|
||||
action={shortcuts.KeyboardAction.run}
|
||||
doAction={() => {
|
||||
unsetModal()
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: asset.id,
|
||||
shouldAutomaticallySwitchPage: false,
|
||||
runInBackground: true,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
@ -297,6 +297,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: projectToLoad.id,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
}
|
||||
setNameOfProjectToImmediatelyOpen(null)
|
||||
@ -774,6 +775,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: projectId,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
},
|
||||
[/* should never change */ dispatchAssetEvent]
|
||||
|
@ -131,6 +131,7 @@ export default function Dashboard(props: DashboardProps) {
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: savedProjectStartupInfo.project.projectId,
|
||||
shouldAutomaticallySwitchPage: page === pageSwitcher.Page.editor,
|
||||
runInBackground: false,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
|
@ -28,6 +28,8 @@ export default function Modal(props: ModalProps) {
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
// This MUST still be z-10, unlike all other elements, because it MUST show above the
|
||||
// IDE.
|
||||
className={`inset-0 z-10 ${
|
||||
centered ? 'fixed w-screen h-screen grid place-items-center' : ''
|
||||
} ${className ?? ''}`}
|
||||
|
@ -93,7 +93,7 @@ export default function PermissionSelector(props: PermissionSelectorProps) {
|
||||
: function Child() {
|
||||
return (
|
||||
<Modal
|
||||
className="fixed w-full h-full z-10"
|
||||
className="fixed w-full h-full z-1"
|
||||
onClick={() => {
|
||||
setTheChild(null)
|
||||
}}
|
||||
|
@ -27,7 +27,7 @@ import SvgMask from '../../authentication/components/svgMask'
|
||||
/** The size of the icon, in pixels. */
|
||||
const ICON_SIZE_PX = 24
|
||||
/** The styles of the icons. */
|
||||
const ICON_STYLE = { width: ICON_SIZE_PX, height: ICON_SIZE_PX } satisfies React.CSSProperties
|
||||
const ICON_CLASSES = 'w-6 h-6'
|
||||
const LOADING_MESSAGE =
|
||||
'Your environment is being created. It will take some time, please be patient.'
|
||||
/** The corresponding {@link SpinnerState} for each {@link backendModule.ProjectState},
|
||||
@ -114,6 +114,9 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
((state: spinner.SpinnerState | null) => void) | null
|
||||
>(null)
|
||||
const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false)
|
||||
const [isRunningInBackground, setIsRunningInBackground] = React.useState(
|
||||
item.projectState.execute_async ?? false
|
||||
)
|
||||
const [shouldSwitchPage, setShouldSwitchPage] = React.useState(false)
|
||||
const [toastId, setToastId] = React.useState<toast.Id | null>(null)
|
||||
const [openProjectAbortController, setOpenProjectAbortController] =
|
||||
@ -124,61 +127,78 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
backend.type !== backendModule.BackendType.local &&
|
||||
item.projectState.opened_by !== organization?.email
|
||||
|
||||
const openProject = React.useCallback(async () => {
|
||||
closeProjectAbortController?.abort()
|
||||
setCloseProjectAbortController(null)
|
||||
setState(backendModule.ProjectState.openInProgress)
|
||||
try {
|
||||
switch (backend.type) {
|
||||
case backendModule.BackendType.remote: {
|
||||
if (!backendModule.DOES_PROJECT_STATE_INDICATE_VM_EXISTS[state]) {
|
||||
setToastId(toast.toast.loading(LOADING_MESSAGE))
|
||||
await backend.openProject(item.id, null, item.title)
|
||||
const openProject = React.useCallback(
|
||||
async (shouldRunInBackground: boolean) => {
|
||||
closeProjectAbortController?.abort()
|
||||
setCloseProjectAbortController(null)
|
||||
setState(backendModule.ProjectState.openInProgress)
|
||||
try {
|
||||
switch (backend.type) {
|
||||
case backendModule.BackendType.remote: {
|
||||
if (!backendModule.DOES_PROJECT_STATE_INDICATE_VM_EXISTS[state]) {
|
||||
setToastId(toast.toast.loading(LOADING_MESSAGE))
|
||||
await backend.openProject(
|
||||
item.id,
|
||||
{
|
||||
forceCreate: false,
|
||||
executeAsync: shouldRunInBackground,
|
||||
},
|
||||
item.title
|
||||
)
|
||||
}
|
||||
const abortController = new AbortController()
|
||||
setOpenProjectAbortController(abortController)
|
||||
await remoteBackend.waitUntilProjectIsReady(backend, item, abortController)
|
||||
setToastId(null)
|
||||
if (!abortController.signal.aborted) {
|
||||
setState(oldState =>
|
||||
oldState === backendModule.ProjectState.openInProgress
|
||||
? backendModule.ProjectState.opened
|
||||
: oldState
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
const abortController = new AbortController()
|
||||
setOpenProjectAbortController(abortController)
|
||||
await remoteBackend.waitUntilProjectIsReady(backend, item, abortController)
|
||||
setToastId(null)
|
||||
if (!abortController.signal.aborted) {
|
||||
case backendModule.BackendType.local: {
|
||||
await backend.openProject(
|
||||
item.id,
|
||||
{
|
||||
forceCreate: false,
|
||||
executeAsync: shouldRunInBackground,
|
||||
},
|
||||
item.title
|
||||
)
|
||||
setState(oldState =>
|
||||
oldState === backendModule.ProjectState.openInProgress
|
||||
? backendModule.ProjectState.opened
|
||||
: oldState
|
||||
)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case backendModule.BackendType.local: {
|
||||
await backend.openProject(item.id, null, item.title)
|
||||
setState(oldState =>
|
||||
oldState === backendModule.ProjectState.openInProgress
|
||||
? backendModule.ProjectState.opened
|
||||
: oldState
|
||||
)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
const project = await backend.getProjectDetails(item.id, item.title)
|
||||
setItem(oldItem => ({
|
||||
...oldItem,
|
||||
projectState: project.state,
|
||||
}))
|
||||
toastAndLog(
|
||||
errorModule.tryGetMessage(error)?.slice(0, -1) ??
|
||||
`Could not open project '${item.title}'`
|
||||
)
|
||||
setState(backendModule.ProjectState.closed)
|
||||
}
|
||||
} catch (error) {
|
||||
const project = await backend.getProjectDetails(item.id, item.title)
|
||||
setItem(oldItem => ({
|
||||
...oldItem,
|
||||
projectState: project.state,
|
||||
}))
|
||||
toastAndLog(
|
||||
errorModule.tryGetMessage(error)?.slice(0, -1) ??
|
||||
`Could not open project '${item.title}'`
|
||||
)
|
||||
setState(backendModule.ProjectState.closed)
|
||||
}
|
||||
}, [
|
||||
state,
|
||||
backend,
|
||||
item,
|
||||
closeProjectAbortController,
|
||||
/* should never change */ toastAndLog,
|
||||
/* should never change */ setState,
|
||||
/* should never change */ setItem,
|
||||
])
|
||||
},
|
||||
[
|
||||
state,
|
||||
backend,
|
||||
item,
|
||||
closeProjectAbortController,
|
||||
/* should never change */ toastAndLog,
|
||||
/* should never change */ setState,
|
||||
/* should never change */ setItem,
|
||||
]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
setItem(oldItem => ({ ...oldItem, projectState: { ...oldItem.projectState, type: state } }))
|
||||
@ -230,14 +250,17 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
}
|
||||
case assetEventModule.AssetEventType.openProject: {
|
||||
if (event.id !== item.id) {
|
||||
setShouldOpenWhenReady(false)
|
||||
if (!isOtherUserUsingProject) {
|
||||
void closeProject(false)
|
||||
if (!event.runInBackground && !isRunningInBackground) {
|
||||
setShouldOpenWhenReady(false)
|
||||
if (!isOtherUserUsingProject) {
|
||||
void closeProject(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setShouldOpenWhenReady(true)
|
||||
setShouldOpenWhenReady(!event.runInBackground)
|
||||
setShouldSwitchPage(event.shouldAutomaticallySwitchPage)
|
||||
void openProject()
|
||||
setIsRunningInBackground(event.runInBackground)
|
||||
void openProject(event.runInBackground)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -249,13 +272,15 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects: {
|
||||
setShouldOpenWhenReady(false)
|
||||
onSpinnerStateChange?.(null)
|
||||
setOnSpinnerStateChange(null)
|
||||
openProjectAbortController?.abort()
|
||||
setOpenProjectAbortController(null)
|
||||
if (!isOtherUserUsingProject) {
|
||||
void closeProject(false)
|
||||
if (!isRunningInBackground) {
|
||||
setShouldOpenWhenReady(false)
|
||||
onSpinnerStateChange?.(null)
|
||||
setOnSpinnerStateChange(null)
|
||||
openProjectAbortController?.abort()
|
||||
setOpenProjectAbortController(null)
|
||||
if (!isOtherUserUsingProject) {
|
||||
void closeProject(false)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -271,9 +296,11 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldOpenWhenReady && state === backendModule.ProjectState.opened) {
|
||||
openIde(shouldSwitchPage)
|
||||
setShouldOpenWhenReady(false)
|
||||
if (state === backendModule.ProjectState.opened) {
|
||||
if (shouldOpenWhenReady) {
|
||||
openIde(shouldSwitchPage)
|
||||
setShouldOpenWhenReady(false)
|
||||
}
|
||||
}
|
||||
// `openIde` is a callback, not a dependency.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -331,7 +358,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
doOpenManually(item.id)
|
||||
}}
|
||||
>
|
||||
<SvgMask style={ICON_STYLE} src={PlayIcon} />
|
||||
<SvgMask className={ICON_CLASSES} src={PlayIcon} />
|
||||
</button>
|
||||
)
|
||||
case backendModule.ProjectState.openInProgress:
|
||||
@ -347,13 +374,16 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
onClick={async clickEvent => {
|
||||
clickEvent.stopPropagation()
|
||||
unsetModal()
|
||||
await closeProject()
|
||||
await closeProject(!isRunningInBackground)
|
||||
}}
|
||||
>
|
||||
<div className="relative h-0">
|
||||
<div className={`relative h-0 ${isRunningInBackground ? 'text-green' : ''}`}>
|
||||
<Spinner size={ICON_SIZE_PX} state={spinnerState} />
|
||||
</div>
|
||||
<SvgMask style={ICON_STYLE} src={StopIcon} />
|
||||
<SvgMask
|
||||
src={StopIcon}
|
||||
className={`${ICON_CLASSES} ${isRunningInBackground ? 'text-green' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
case backendModule.ProjectState.opened:
|
||||
@ -368,15 +398,22 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
onClick={async clickEvent => {
|
||||
clickEvent.stopPropagation()
|
||||
unsetModal()
|
||||
await closeProject()
|
||||
await closeProject(!isRunningInBackground)
|
||||
}}
|
||||
>
|
||||
<div className="relative h-0">
|
||||
<div
|
||||
className={`relative h-0 ${isRunningInBackground ? 'text-green' : ''}`}
|
||||
>
|
||||
<Spinner size={24} state={spinnerState} />
|
||||
</div>
|
||||
<SvgMask style={ICON_STYLE} src={StopIcon} />
|
||||
<SvgMask
|
||||
src={StopIcon}
|
||||
className={`${ICON_CLASSES} ${
|
||||
isRunningInBackground ? 'text-green' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{!isOtherUserUsingProject && (
|
||||
{!isOtherUserUsingProject && !isRunningInBackground && (
|
||||
<button
|
||||
className="w-6 h-6"
|
||||
onClick={clickEvent => {
|
||||
@ -385,7 +422,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
openIde(true)
|
||||
}}
|
||||
>
|
||||
<SvgMask style={ICON_STYLE} src={ArrowUpIcon} />
|
||||
<SvgMask src={ArrowUpIcon} className={ICON_CLASSES} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -129,6 +129,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: createdProject.projectId,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
} catch (error) {
|
||||
dispatchAssetListEvent({
|
||||
@ -225,12 +226,20 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
onClick={event => {
|
||||
if (rowState.isEditingName || isOtherUserUsingProject) {
|
||||
// The project should neither be edited nor opened in these cases.
|
||||
} else if (eventModule.isDoubleClick(event)) {
|
||||
} else if (shortcuts.matchesMouseAction(shortcutsModule.MouseAction.open, event)) {
|
||||
// It is a double click; open the project.
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: asset.id,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
} else if (shortcuts.matchesMouseAction(shortcutsModule.MouseAction.run, event)) {
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
id: asset.id,
|
||||
shouldAutomaticallySwitchPage: false,
|
||||
runInBackground: true,
|
||||
})
|
||||
} else if (
|
||||
!isRunning &&
|
||||
|
@ -86,6 +86,7 @@ export interface AssetNewSecretEvent extends AssetBaseEvent<AssetEventType.newSe
|
||||
export interface AssetOpenProjectEvent extends AssetBaseEvent<AssetEventType.openProject> {
|
||||
id: backendModule.ProjectId
|
||||
shouldAutomaticallySwitchPage: boolean
|
||||
runInBackground: boolean
|
||||
}
|
||||
|
||||
/** A signal to close the specified project. */
|
||||
|
@ -23,6 +23,7 @@ const STATUS_SERVER_ERROR = 500
|
||||
/** Default HTTP body for an "open project" request. */
|
||||
const DEFAULT_OPEN_PROJECT_BODY: backendModule.OpenProjectRequestBody = {
|
||||
forceCreate: false,
|
||||
executeAsync: false,
|
||||
}
|
||||
|
||||
// ============================
|
||||
|
@ -16,6 +16,7 @@ import DuplicateIcon from 'enso-assets/duplicate.svg'
|
||||
import OpenIcon from 'enso-assets/open.svg'
|
||||
import PenIcon from 'enso-assets/pen.svg'
|
||||
import PeopleIcon from 'enso-assets/people.svg'
|
||||
import Play2Icon from 'enso-assets/play2.svg'
|
||||
import ScissorsIcon from 'enso-assets/scissors.svg'
|
||||
import SignInIcon from 'enso-assets/sign_in.svg'
|
||||
import SignOutIcon from 'enso-assets/sign_out.svg'
|
||||
@ -39,6 +40,9 @@ export const ICON_SIZE_PX = 16
|
||||
|
||||
/** All possible mouse actions for which shortcuts can be registered. */
|
||||
export enum MouseAction {
|
||||
open = 'open',
|
||||
/** Run without opening the editor. */
|
||||
run = 'run',
|
||||
editName = 'edit-name',
|
||||
selectAdditional = 'select-additional',
|
||||
selectRange = 'select-range',
|
||||
@ -48,6 +52,8 @@ export enum MouseAction {
|
||||
/** All possible keyboard actions for which shortcuts can be registered. */
|
||||
export enum KeyboardAction {
|
||||
open = 'open',
|
||||
/** Run without opening the editor. */
|
||||
run = 'run',
|
||||
close = 'close',
|
||||
uploadToCloud = 'upload-to-cloud',
|
||||
rename = 'rename',
|
||||
@ -103,6 +109,7 @@ export interface KeyboardShortcut extends Modifiers {
|
||||
export interface MouseShortcut extends Modifiers {
|
||||
button: MouseButton
|
||||
action: MouseAction
|
||||
clicks: number
|
||||
}
|
||||
|
||||
/** All possible modifier keys. */
|
||||
@ -146,6 +153,7 @@ export function isTextInputEvent(event: KeyboardEvent | React.KeyboardEvent) {
|
||||
function makeKeyboardActionMap<T>(make: () => T): Record<KeyboardAction, T> {
|
||||
return {
|
||||
[KeyboardAction.open]: make(),
|
||||
[KeyboardAction.run]: make(),
|
||||
[KeyboardAction.close]: make(),
|
||||
[KeyboardAction.uploadToCloud]: make(),
|
||||
[KeyboardAction.rename]: make(),
|
||||
@ -279,7 +287,11 @@ export class ShortcutRegistry {
|
||||
shortcut: MouseShortcut,
|
||||
event: MouseEvent | React.MouseEvent
|
||||
) {
|
||||
return shortcut.button === event.button && modifiersMatchEvent(shortcut, event)
|
||||
return (
|
||||
shortcut.button === event.button &&
|
||||
event.detail >= shortcut.clicks &&
|
||||
modifiersMatchEvent(shortcut, event)
|
||||
)
|
||||
}
|
||||
|
||||
/** Return `true` if the action is being triggered by the keyboard event. */
|
||||
@ -387,11 +399,13 @@ function keybind(action: KeyboardAction, modifiers: ModifierKey[], key: string):
|
||||
function mousebind(
|
||||
action: MouseAction,
|
||||
modifiers: ModifierKey[],
|
||||
button: MouseButton
|
||||
button: MouseButton,
|
||||
clicks: number
|
||||
): MouseShortcut {
|
||||
return {
|
||||
button,
|
||||
action,
|
||||
clicks,
|
||||
ctrl: modifiers.includes('Ctrl'),
|
||||
alt: modifiers.includes('Alt'),
|
||||
shift: modifiers.includes('Shift'),
|
||||
@ -412,6 +426,7 @@ const DELETE = detect.isOnMacOS() ? 'Backspace' : 'Delete'
|
||||
/** The default keyboard shortcuts. */
|
||||
const DEFAULT_KEYBOARD_SHORTCUTS: Record<KeyboardAction, KeyboardShortcut[]> = {
|
||||
[KeyboardAction.open]: [keybind(KeyboardAction.open, [], 'Enter')],
|
||||
[KeyboardAction.run]: [keybind(KeyboardAction.run, ['Shift'], 'Enter')],
|
||||
[KeyboardAction.close]: [],
|
||||
[KeyboardAction.uploadToCloud]: [],
|
||||
[KeyboardAction.rename]: [keybind(KeyboardAction.rename, [CTRL], 'R')],
|
||||
@ -440,6 +455,7 @@ const DEFAULT_KEYBOARD_SHORTCUTS: Record<KeyboardAction, KeyboardShortcut[]> = {
|
||||
/** The default UI data for every keyboard shortcut. */
|
||||
const DEFAULT_KEYBOARD_SHORTCUT_INFO: Record<KeyboardAction, ShortcutInfo> = {
|
||||
[KeyboardAction.open]: { name: 'Open', icon: OpenIcon },
|
||||
[KeyboardAction.run]: { name: 'Run', icon: Play2Icon },
|
||||
[KeyboardAction.close]: { name: 'Close', icon: CloseIcon },
|
||||
[KeyboardAction.uploadToCloud]: { name: 'Upload To Cloud', icon: CloudToIcon },
|
||||
[KeyboardAction.rename]: { name: 'Rename', icon: PenIcon },
|
||||
@ -474,12 +490,14 @@ const DEFAULT_KEYBOARD_SHORTCUT_INFO: Record<KeyboardAction, ShortcutInfo> = {
|
||||
|
||||
/** The default mouse shortcuts. */
|
||||
const DEFAULT_MOUSE_SHORTCUTS: Record<MouseAction, MouseShortcut[]> = {
|
||||
[MouseAction.editName]: [mousebind(MouseAction.editName, [CTRL], MouseButton.left)],
|
||||
[MouseAction.open]: [mousebind(MouseAction.open, [], MouseButton.left, 2)],
|
||||
[MouseAction.run]: [mousebind(MouseAction.run, ['Shift'], MouseButton.left, 2)],
|
||||
[MouseAction.editName]: [mousebind(MouseAction.editName, [CTRL], MouseButton.left, 1)],
|
||||
[MouseAction.selectAdditional]: [
|
||||
mousebind(MouseAction.selectAdditional, [CTRL], MouseButton.left),
|
||||
mousebind(MouseAction.selectAdditional, [CTRL], MouseButton.left, 1),
|
||||
],
|
||||
[MouseAction.selectRange]: [mousebind(MouseAction.selectRange, ['Shift'], MouseButton.left)],
|
||||
[MouseAction.selectRange]: [mousebind(MouseAction.selectRange, ['Shift'], MouseButton.left, 1)],
|
||||
[MouseAction.selectAdditionalRange]: [
|
||||
mousebind(MouseAction.selectAdditionalRange, [CTRL, 'Shift'], MouseButton.left),
|
||||
mousebind(MouseAction.selectAdditionalRange, [CTRL, 'Shift'], MouseButton.left, 1),
|
||||
],
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ export const theme = {
|
||||
cloud: '#0666be',
|
||||
share: '#64b526',
|
||||
inversed: '#ffffff',
|
||||
green: '#3e8b29',
|
||||
delete: 'rgba(243, 24, 10, 0.87)',
|
||||
v3: '#252423',
|
||||
youtube: '#c62421',
|
||||
|
Loading…
Reference in New Issue
Block a user