* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import React, { useRef, useEffect, useState } from 'react';
import styled from '@emotion/styled';
import type { File, Session } from '@/entity/session';
import type { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { setBaselineData, setCompareData, cancelBaselineData, cancelCompareData } from '@/utils/Compare';
import type { DataSource, LayerType } from '@/centralServer/websocket/defs';
interface Position {
x: number;
y: number;
};
interface MenuItemModel {
label: React.ReactNode;
key: string;
action: () => void;
visible?: boolean;
disabled?: boolean;
}
const MenuItem = styled.div`
padding: 4px 16px;
color: ${(props): string => props.theme.textColorPrimary};
&:not(.disabled):hover{
background: ${(props): string => props.theme.primaryColorHover};
color: #ffffff;
}
&.disabled{
color: ${(props): string => props.theme.textColorDisabled};
}
`;
export const isSameFile = (one: File, anotherOne: File): boolean => {
return one.projectName === anotherOne.projectName && one.filePath === anotherOne.filePath && one.rankId?.replace('Baseline_', '') === anotherOne.rankId?.replace('Baseline_', '');
};
function checkProjectFileIsClusterComparable<T extends File>(projectFile: T, project?: DataSource): [boolean, string] {
const clusterList = project?.children?.filter((item) => item.type === 'CLUSTER') ?? [];
let clusterListNum = clusterList.length;
let clusterPath = '';
if (clusterListNum === 1) {
clusterPath = clusterList[0].path;
}
if (clusterListNum === 0 && (project?.children?.every((child) => child.type === 'RANK') ?? false)) {
clusterListNum = 1;
}
if (clusterPath === '') { clusterPath = projectFile.filePath; }
if (clusterListNum <= 0) {
return [false, clusterPath];
} else if (clusterListNum === 1) {
return [projectFile.fileType === 'PROJECT', clusterPath];
} else {
return [projectFile.fileType === 'CLUSTER', clusterPath];
}
}
function checkProjectFileIsOtherComparable<T extends { fileType: LayerType; projectName: string }>(projectFile: T): boolean {
return projectFile.fileType !== 'PROJECT' && projectFile.fileType !== 'CLUSTER';
}
function getClusterIsComparableAndSelectedClusterPath(baseline: File, comparison: File, selectedFile: File, session: Session): {
isBaseline: boolean; isComparison: boolean; hasBaseline: boolean; isComparable: boolean; isCanBeBaseline: boolean; selectedClusterPath: string;
} {
const baselineProject = session.getDataSourceByProjectName(baseline.projectName);
const selectedProject = session.getDataSourceByProjectName(selectedFile.projectName);
const isBaseline = isSameFile(selectedFile, baseline);
const isComparison = isSameFile(selectedFile, comparison);
const hasBaseline = baseline.projectName !== '';
const [isClusterComparableBaseline] = checkProjectFileIsClusterComparable(baseline, baselineProject);
const [isClusterComparableSelected, selectedClusterPath] = checkProjectFileIsClusterComparable(selectedFile, selectedProject);
const isOtherComparableBaseline = checkProjectFileIsOtherComparable(baseline);
const isOtherComparableSelected = checkProjectFileIsOtherComparable(selectedFile);
const isComparable = (isClusterComparableBaseline && isClusterComparableSelected) || (isOtherComparableBaseline && isOtherComparableSelected);
const isCanBeBaseline = !isBaseline && (isClusterComparableSelected || isOtherComparableSelected);
return { isBaseline, isComparison, hasBaseline, isComparable, isCanBeBaseline, selectedClusterPath };
}
const openInExplorer = (selectedFile: File): void => {
window.ipc?.postMessage(`openProjectInExplorer|${selectedFile.filePath}`);
};
const getMenuItems = ({ session }: IProps, t: TFunction): MenuItemModel[] => {
const { activeDataSource, selectedFile, compareSet: { baseline, comparison } } = session;
if (selectedFile.fileType === 'UNKNOWN') { return []; }
const {
isBaseline, isComparison, hasBaseline, isComparable, isCanBeBaseline, selectedClusterPath,
} = getClusterIsComparableAndSelectedClusterPath(baseline, comparison, selectedFile, session);
const currentClusterPath = session.getClusterPath(activeDataSource);
const allMenuItems: MenuItemModel[] = [
{
label: t('Set as Baseline Data'),
key: 'setAsBaselineData',
action: (): void => { setBaselineData({ ...selectedFile, baselineClusterPath: selectedClusterPath, currentClusterPath }); },
visible: isCanBeBaseline,
},
{
label: t('Unset as Baseline Data'),
key: 'unsetAsBaselineData',
action: cancelBaselineData,
visible: hasBaseline && isBaseline,
},
{
label: t('Set as Comparison Data'),
key: 'setAsComparisonData',
action: (): void => { setCompareData(selectedFile); },
visible: selectedFile.fileType !== 'PROJECT' && selectedFile.fileType !== 'CLUSTER' && hasBaseline && !isBaseline && !isComparison && isComparable,
disabled: activeDataSource.projectName !== selectedFile.projectName,
},
{
label: t('Unset as Comparison Data'),
key: 'unsetAsComparisonData',
action: cancelCompareData,
visible: isComparison,
},
{
label: t('Open in Explorer'),
key: 'openInExplorer',
action: () => openInExplorer(selectedFile),
visible: window.ipc !== undefined,
},
];
return allMenuItems.filter(menuItem => menuItem.visible !== false);
};
function openMenu(session: Session): void {
runInAction(() => {
session.contextMenu.visible = true;
});
}
function closeMenu(session: Session): void {
runInAction(() => {
session.contextMenu.visible = false;
});
}
function adjustMenuPosition({ menu, setMenuPosition, mousePosition }: {
menu: HTMLDivElement;
setMenuPosition: (_: Position) => void;
mousePosition: Position;
}): void {
const winWidth = document.documentElement.clientWidth || document.body.clientWidth;
const winHeight = document.documentElement.clientHeight || document.body.clientHeight;
let x = mousePosition.x;
let y = mousePosition.y;
if (x + menu.offsetWidth >= winWidth) {
x -= menu.offsetWidth;
}
if (y + menu.offsetHeight > winHeight) {
y -= menu.offsetHeight;
}
setMenuPosition({ x, y });
menu.focus();
};
const MenuContainer = styled.div`
padding: 3px 0;
min-width: 100px;
border-radius: ${(props): string => props.theme.borderRadiusBase};
background-color: ${(props): string => props.theme.contextMenuBgColor};
position: fixed;
z-index: 99999;
box-shadow: ${(props): string => props.theme.boxShadowLight};
user-select: none;
`;
interface IProps {
session: Session;
}
const Menu = ({ session }: IProps): JSX.Element => {
const menuRef = useRef<HTMLDivElement>(null);
const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
const [menuPosition, setMenuPosition] = useState<Position>({ x: 0, y: 0 });
const { t } = useTranslation('framework');
const handleContextMenu = (event: MouseEvent): void => {
event.preventDefault();
event.stopPropagation();
const targetElement = event.target as HTMLElement;
if (targetElement?.closest('.can-right-click')) {
setMousePosition({ x: event.clientX, y: event.clientY });
setMenuPosition({ x: event.clientX, y: event.clientY });
openMenu(session);
}
};
const handleMouseDown = (e: MouseEvent): void => {
if ((e.target as HTMLElement)?.parentNode !== menuRef.current) {
closeMenu(session);
}
};
const handleCloseMenu = (): void => {
closeMenu(session);
};
useEffect(() => {
document.addEventListener('contextmenu', handleContextMenu);
window.addEventListener('mousedown', handleMouseDown);
window.addEventListener('wheel', handleCloseMenu);
return () => {
document.removeEventListener('contextmenu', handleContextMenu);
window.removeEventListener('mousedown', handleMouseDown);
window.removeEventListener('wheel', handleCloseMenu);
};
});
useEffect(() => {
const menu = menuRef.current;
if (session.contextMenu.visible && menu !== null) {
adjustMenuPosition({ menu, setMenuPosition, mousePosition });
}
}, [session.contextMenu.visible, mousePosition]);
const menuList = React.useMemo(() => {
return getMenuItems({ session }, t);
}, [session.dataSources, session.activeDataSource, session.selectedFile, session.compareSet.baseline, session.compareSet.comparison, t]);
return (
session.contextMenu.visible && menuList.length > 0
? <MenuContainer ref={menuRef} style={{ left: `${menuPosition.x}px`, top: `${menuPosition.y}px` }} tabIndex={-1} onBlur={(): void => { closeMenu(session); }} >
{menuList.map((item: MenuItemModel): JSX.Element => (
<MenuItem className={`menu-item ${item.disabled ? 'disabled' : ''}`} key={item.key}
onClick={(): void => {
if (item.disabled) {
return;
}
item.action();
closeMenu(session);
}}>
{item.label}
</MenuItem>))}
</MenuContainer>
: <></>
);
};
const ContextMenu = observer(Menu);
export default ContextMenu;