import { Menu, MenuItem } from "@electron/remote";
import React from "react";
import { IObservableValue, action, makeObservable, runInAction } from "mobx";
import { observer } from "mobx-react";
import classNames from "classnames";
import { Icon } from "eez-studio-ui/icon";
import type {
History,
IAppStore,
SelectHistoryItemsSpecification
} from "instrument/window/history/history";
import type { IHistoryItem } from "instrument/window/history/item";
import { historySessions, SESSION_FREE_ID } from "./session/store";
////////////////////////////////////////////////////////////////////////////////
const CONF_AUTO_RELOAD_TIMEOUT = 1000 / 30;
const CONF_AUTO_RELOAD_Y_THRESHOLD = 20;
export const CLIPBOARD_DATA_TYPE = "application/eez-studio-history-item";
////////////////////////////////////////////////////////////////////////////////
export class ErrorBoundary extends React.Component<
{ children?: React.ReactNode; id: string },
{ hasError: boolean }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: any) {
return { hasError: true };
}
componentDidCatch(error: any, info: any) {
console.error(error, info);
}
render() {
if (this.state.hasError) {
return (
Error while rendering history item {this.props.id}!
);
}
return this.props.children;
}
}
////////////////////////////////////////////////////////////////////////////////
export interface ISelection {
items: IHistoryItem[];
selectItems(historyItems: IHistoryItem[]): void;
}
export const HistoryItems = observer(
class HistoryItems extends React.Component<{
appStore: IAppStore;
historyItems: IHistoryItem[];
selection: ISelection;
selectHistoryItemsSpecification:
| SelectHistoryItemsSpecification
| undefined;
getAllItemsBetween: (
fromItem: IHistoryItem,
toItem: IHistoryItem
) => IHistoryItem[];
isDeletedItemsHistory: boolean;
deleteSelectedHistoryItems: () => void;
viewType: "chat" | "thumbs";
thumbnailSize: number;
showInHistory?: () => void;
}> {
render() {
return this.props.historyItems.map(historyItem => {
let element = historyItem.getListItemElement(
this.props.appStore,
this.props.viewType
);
let showCheckbox = false;
if (this.props.selectHistoryItemsSpecification) {
if (
this.props.selectHistoryItemsSpecification
.historyItemType === "chart"
) {
if (historyItem.canBePartOfMultiChart) {
showCheckbox = true;
} else {
element = ;
}
} else {
showCheckbox = true;
}
}
let className = classNames(
`EezStudio_HistoryItemEnclosure EezStudio_HistoryItem_${historyItem.id}`,
{
selected:
!this.props.selectHistoryItemsSpecification &&
historyItem.selected,
disablePreview: showCheckbox
},
this.props.viewType
);
let style;
if (this.props.viewType === "thumbs") {
style = {
"--historyItemThumbnailSize":
this.props.thumbnailSize + "px"
} as React.CSSProperties;
}
return (
);
});
}
}
);
class LoadMoreButton extends React.Component<{
icon: string;
loadMore: () => void;
}> {
render() {
const { icon, loadMore } = this.props;
return (
);
}
}
////////////////////////////////////////////////////////////////////////////////
export class HistoryListComponentClass extends React.Component<{
appStore: IAppStore;
history: History;
jumpToPresentCondition: IObservableValue;
}> {
animationFrameRequestId: any;
fromBottom: number | undefined;
fromTop: number | undefined = 0;
autoReloadEnabled: boolean = true;
timeOfLastAutoLoad: number;
lastScrollHeight: number;
lastClientHeight: number;
findCenterItemTimeut: any;
lastItemInTheCenterId: string | undefined;
ref = React.createRef();
get div() {
return this.ref.current!.parentElement as Element;
}
constructor(props: any) {
super(props);
makeObservable(this, {
showHistoryItem: action
});
}
componentDidMount() {
this.autoScroll();
this.div.addEventListener("scroll", this.onScroll);
this.lastItemInTheCenterId = undefined;
}
componentDidUpdate(prevProps: any) {
if (this.props.history != prevProps.history) {
this.fromBottom = undefined;
this.fromTop = 0;
}
// // make sure scroll bar is recalculated after render
// $(this.div).css("overflow", "hidden");
// setTimeout(() => {
// $(this.div).css("overflow", "auto");
// }, 1);
this.lastItemInTheCenterId = undefined;
}
componentWillUnmount() {
window.cancelAnimationFrame(this.animationFrameRequestId);
this.div.removeEventListener("scroll", this.onScroll);
}
moveToTop() {
this.fromBottom = 0;
this.fromTop = undefined;
}
moveToBottom() {
this.fromBottom = undefined;
this.fromTop = 0;
}
showHistoryItem(historyItem: IHistoryItem) {
this.autoReloadEnabled = false;
let c = 0;
const scrollIntoView = () => {
const element = $(this.div).find(
`.EezStudio_HistoryItem_${historyItem.id}`
)[0];
if (element) {
element.scrollIntoView({ block: "nearest" });
}
if (++c < 5) {
setTimeout(scrollIntoView, 10);
} else {
this.autoReloadEnabled = true;
}
};
scrollIntoView();
}
autoScroll = () => {
if ($(this.div).is(":visible")) {
if (this.fromBottom !== undefined) {
if (this.fromBottom != this.div.scrollTop) {
this.div.scrollTop = this.fromBottom;
}
} else if (this.fromTop !== undefined) {
let scrollTop =
this.div.scrollHeight -
this.div.clientHeight -
this.fromTop;
if (scrollTop != this.div.scrollTop) {
this.div.scrollTop = scrollTop;
}
}
}
const jumpToPresentCondition =
this.div.scrollHeight -
(this.div.scrollTop + this.div.clientHeight) >
this.div.clientHeight;
if (jumpToPresentCondition != this.props.jumpToPresentCondition.get()) {
runInAction(() =>
this.props.jumpToPresentCondition.set(jumpToPresentCondition)
);
}
// automatically load more content
const time = Date.now();
if (
this.autoReloadEnabled &&
this.div.clientHeight > 0 &&
this.div.scrollHeight >= this.div.clientHeight &&
(this.timeOfLastAutoLoad === undefined ||
time - this.timeOfLastAutoLoad > CONF_AUTO_RELOAD_TIMEOUT)
) {
this.timeOfLastAutoLoad = Date.now();
if (
this.props.history.navigator.hasOlder &&
this.div.scrollTop < CONF_AUTO_RELOAD_Y_THRESHOLD
) {
this.loadOlder();
}
if (
this.props.history.navigator.hasNewer &&
this.div.scrollHeight -
(this.div.scrollTop + this.div.clientHeight) <
CONF_AUTO_RELOAD_Y_THRESHOLD
) {
this.loadNewer();
}
}
this.animationFrameRequestId = window.requestAnimationFrame(
this.autoScroll
);
};
onScroll = (event: any) => {
if (
this.div.scrollHeight === this.lastScrollHeight &&
this.div.clientHeight === this.lastClientHeight
) {
if (this.fromBottom !== undefined) {
this.fromBottom = this.div.scrollTop;
} else if (this.fromTop !== undefined) {
this.fromTop =
this.div.scrollHeight -
this.div.clientHeight -
this.div.scrollTop;
}
}
this.lastScrollHeight = this.div.scrollHeight;
this.lastClientHeight = this.div.clientHeight;
// find item in the center of the view
if (this.findCenterItemTimeut) {
clearTimeout(this.findCenterItemTimeut);
}
this.findCenterItemTimeut = setTimeout(() => {
this.findCenterItemTimeut = undefined;
const itemElements = $(this.div).find(
".EezStudio_HistoryItemEnclosure"
);
if (itemElements.length > 0) {
let foundItemElement = itemElements[0];
const rect = foundItemElement.getBoundingClientRect();
let minDistance = Math.abs(
this.div.clientHeight - (rect.top + rect.height / 2)
);
for (let i = 1; i < itemElements.length; ++i) {
const rect = itemElements[i].getBoundingClientRect();
const distance = Math.abs(
this.div.clientHeight / 2 - (rect.top + rect.height / 2)
);
if (distance < minDistance) {
minDistance = distance;
foundItemElement = itemElements[i];
}
}
const matches = foundItemElement.className.match(
/EezStudio_HistoryItem_(\d*)/
);
if (matches && matches.length >= 1) {
const itemId = matches[1];
if (itemId !== this.lastItemInTheCenterId) {
// found it
this.props.history.setItemInTheCenterOfTheView(itemId);
}
}
}
}, 100);
};
loadOlder = async () => {
this.autoReloadEnabled = false;
this.fromBottom = undefined;
this.fromTop = undefined;
// when we load older items, we don't want scroll position to change
// find first item above scrollTop
let firstItem: HTMLDivElement | undefined;
const items = this.div.children[0].children;
for (let i = 0; i < items.length; ++i) {
const item = items[i] as HTMLDivElement;
if (
item.className.indexOf("EezStudio_HistoryItemEnclosure") !==
-1 &&
item.offsetTop > this.div.scrollTop
) {
firstItem = item;
break;
}
}
if (firstItem) {
// remember offset of the firstItem from scrollTop
let offset = firstItem.offsetTop - this.div.scrollTop;
await this.props.history.navigator.loadOlder();
window.setTimeout(() => {
// make sure firstItem is again at the same offset
this.div.scrollTop = firstItem!.offsetTop - offset;
this.autoReloadEnabled = true;
}, 5);
} else {
await this.props.history.navigator.loadOlder();
}
};
loadNewer = () => {
this.fromBottom = undefined;
this.fromTop = undefined;
this.props.history.navigator.loadNewer();
};
render() {
return (
{
if (
$(event.target).closest(
".EezStudio_HistoryItemEnclosure"
).length === 0
) {
this.props.history.selection.selectItems([]);
}
}}
>
{this.props.history.navigator.hasOlder && (
)}
this.props.history.getAllItemsBetween(fromItem, toItem)
}
isDeletedItemsHistory={
this.props.history ===
this.props.appStore.deletedItemsHistory
}
deleteSelectedHistoryItems={() =>
this.props.history.deleteSelectedHistoryItems()
}
viewType="chat"
thumbnailSize={480}
/>
{this.props.history.navigator.hasNewer && (
)}
);
}
}
export const HistoryListComponent = observer(HistoryListComponentClass);