mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
Fix some visual glitches in the Events Sheet when opening and scrolling (#8571)
This commit is contained in:
@@ -18,6 +18,18 @@ ElseEvent::ElseEvent()
|
||||
|
||||
ElseEvent::~ElseEvent(){};
|
||||
|
||||
gd::InstructionsList* ElseEvent::GetInstructionList(const gd::String& label) {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
const gd::InstructionsList* ElseEvent::GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vector<const gd::InstructionsList*> ElseEvent::GetAllConditionsVectors() const {
|
||||
vector<const gd::InstructionsList*> allConditions;
|
||||
allConditions.push_back(&conditions);
|
||||
|
||||
@@ -39,6 +39,9 @@ class GD_CORE_API ElseEvent : public gd::BaseEvent {
|
||||
const gd::InstructionsList& GetActions() const { return actions; };
|
||||
gd::InstructionsList& GetActions() { return actions; };
|
||||
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) override;
|
||||
virtual const gd::InstructionsList* GetInstructionList(const gd::String& label) const override;
|
||||
|
||||
virtual std::vector<const gd::InstructionsList*> GetAllConditionsVectors() const;
|
||||
virtual std::vector<const gd::InstructionsList*> GetAllActionsVectors() const;
|
||||
virtual std::vector<gd::InstructionsList*> GetAllConditionsVectors();
|
||||
|
||||
@@ -19,6 +19,19 @@ ForEachChildVariableEvent::ForEachChildVariableEvent()
|
||||
iterableVariableName(""),
|
||||
variables(gd::VariablesContainer::SourceType::Local) {}
|
||||
|
||||
gd::InstructionsList* ForEachChildVariableEvent::GetInstructionList(
|
||||
const gd::String& label) {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
const gd::InstructionsList* ForEachChildVariableEvent::GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vector<gd::InstructionsList*> ForEachChildVariableEvent::GetAllConditionsVectors() {
|
||||
vector<gd::InstructionsList*> allConditions;
|
||||
allConditions.push_back(&conditions);
|
||||
|
||||
@@ -47,6 +47,9 @@ class GD_CORE_API ForEachChildVariableEvent : public gd::BaseEvent {
|
||||
const gd::InstructionsList& GetActions() const { return actions; };
|
||||
gd::InstructionsList& GetActions() { return actions; };
|
||||
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) override;
|
||||
virtual const gd::InstructionsList* GetInstructionList(const gd::String& label) const override;
|
||||
|
||||
/**
|
||||
* \brief Get the iterable variable name attached to the event.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,19 @@ ForEachEvent::ForEachEvent()
|
||||
limit(""),
|
||||
variables(gd::VariablesContainer::SourceType::Local) {}
|
||||
|
||||
gd::InstructionsList* ForEachEvent::GetInstructionList(
|
||||
const gd::String& label) {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
const gd::InstructionsList* ForEachEvent::GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vector<gd::InstructionsList*> ForEachEvent::GetAllConditionsVectors() {
|
||||
vector<gd::InstructionsList*> allConditions;
|
||||
allConditions.push_back(&conditions);
|
||||
|
||||
@@ -49,6 +49,9 @@ class GD_CORE_API ForEachEvent : public gd::BaseEvent {
|
||||
const gd::InstructionsList& GetActions() const { return actions; };
|
||||
gd::InstructionsList& GetActions() { return actions; };
|
||||
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) override;
|
||||
virtual const gd::InstructionsList* GetInstructionList(const gd::String& label) const override;
|
||||
|
||||
const gd::String& GetObjectToPick() const {
|
||||
return objectsToPick.GetPlainString();
|
||||
};
|
||||
|
||||
@@ -18,6 +18,18 @@ RepeatEvent::RepeatEvent()
|
||||
variables(gd::VariablesContainer::SourceType::Local),
|
||||
repeatNumberExpressionSelected(false) {}
|
||||
|
||||
gd::InstructionsList* RepeatEvent::GetInstructionList(const gd::String& label) {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
const gd::InstructionsList* RepeatEvent::GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vector<gd::InstructionsList*> RepeatEvent::GetAllConditionsVectors() {
|
||||
vector<gd::InstructionsList*> allConditions;
|
||||
allConditions.push_back(&conditions);
|
||||
|
||||
@@ -43,6 +43,9 @@ class GD_CORE_API RepeatEvent : public gd::BaseEvent {
|
||||
const gd::InstructionsList& GetActions() const { return actions; };
|
||||
gd::InstructionsList& GetActions() { return actions; };
|
||||
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) override;
|
||||
virtual const gd::InstructionsList* GetInstructionList(const gd::String& label) const override;
|
||||
|
||||
const gd::Expression& GetRepeatExpression() const {
|
||||
return repeatNumberExpression;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,19 @@ StandardEvent::StandardEvent()
|
||||
|
||||
StandardEvent::~StandardEvent(){};
|
||||
|
||||
gd::InstructionsList* StandardEvent::GetInstructionList(
|
||||
const gd::String& label) {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
const gd::InstructionsList* StandardEvent::GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vector<const gd::InstructionsList*> StandardEvent::GetAllConditionsVectors()
|
||||
const {
|
||||
vector<const gd::InstructionsList*> allConditions;
|
||||
|
||||
@@ -43,6 +43,9 @@ class GD_CORE_API StandardEvent : public gd::BaseEvent {
|
||||
const gd::InstructionsList& GetActions() const { return actions; };
|
||||
gd::InstructionsList& GetActions() { return actions; };
|
||||
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) override;
|
||||
virtual const gd::InstructionsList* GetInstructionList(const gd::String& label) const override;
|
||||
|
||||
virtual std::vector<const gd::InstructionsList*> GetAllConditionsVectors()
|
||||
const;
|
||||
virtual std::vector<const gd::InstructionsList*> GetAllActionsVectors() const;
|
||||
|
||||
@@ -13,6 +13,20 @@ using namespace std;
|
||||
|
||||
namespace gd {
|
||||
|
||||
gd::InstructionsList* WhileEvent::GetInstructionList(const gd::String& label) {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
if (label == BaseEvent::whileConditionsLabel) return &whileConditions;
|
||||
return nullptr;
|
||||
}
|
||||
const gd::InstructionsList* WhileEvent::GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
if (label == BaseEvent::conditionsLabel) return &conditions;
|
||||
if (label == BaseEvent::actionsLabel) return &actions;
|
||||
if (label == BaseEvent::whileConditionsLabel) return &whileConditions;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vector<gd::InstructionsList*> WhileEvent::GetAllConditionsVectors() {
|
||||
vector<gd::InstructionsList*> allConditions;
|
||||
allConditions.push_back(&whileConditions);
|
||||
|
||||
@@ -51,6 +51,9 @@ class GD_CORE_API WhileEvent : public gd::BaseEvent {
|
||||
const gd::InstructionsList& GetActions() const { return actions; };
|
||||
gd::InstructionsList& GetActions() { return actions; };
|
||||
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) override;
|
||||
virtual const gd::InstructionsList* GetInstructionList(const gd::String& label) const override;
|
||||
|
||||
const gd::InstructionsList& GetWhileConditions() const {
|
||||
return whileConditions;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace gd {
|
||||
|
||||
EventsList BaseEvent::badSubEvents;
|
||||
VariablesContainer BaseEvent::badLocalVariables;
|
||||
const gd::String BaseEvent::conditionsLabel = "conditions";
|
||||
const gd::String BaseEvent::actionsLabel = "actions";
|
||||
const gd::String BaseEvent::whileConditionsLabel = "whileConditions";
|
||||
|
||||
BaseEvent::BaseEvent()
|
||||
: totalTimeDuringLastSession(0),
|
||||
|
||||
@@ -117,6 +117,26 @@ class GD_CORE_API BaseEvent {
|
||||
*/
|
||||
bool HasVariables() const;
|
||||
|
||||
/**
|
||||
* \brief Return the instruction list identified by \a label, or nullptr if
|
||||
* the event has no such list.
|
||||
*
|
||||
* Supported labels are "conditions", "actions", and "whileConditions" (only
|
||||
* for WhileEvent). Derived classes should override this; the base
|
||||
* implementation always returns nullptr.
|
||||
*/
|
||||
virtual gd::InstructionsList* GetInstructionList(const gd::String& label) {
|
||||
return nullptr;
|
||||
};
|
||||
virtual const gd::InstructionsList* GetInstructionList(
|
||||
const gd::String& label) const {
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
static const gd::String conditionsLabel;
|
||||
static const gd::String actionsLabel;
|
||||
static const gd::String whileConditionsLabel;
|
||||
|
||||
/**
|
||||
* \brief Return a list of all conditions of the event.
|
||||
* \note Used to preprocess or search in the conditions.
|
||||
|
||||
@@ -2499,6 +2499,8 @@ interface BaseEvent {
|
||||
boolean IsFolded();
|
||||
void SetFolded(boolean folded);
|
||||
|
||||
InstructionsList GetInstructionList([Const] DOMString label);
|
||||
|
||||
void SerializeTo([Ref] SerializerElement element);
|
||||
void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element);
|
||||
|
||||
|
||||
Vendored
+1
@@ -1925,6 +1925,7 @@ export class BaseEvent extends EmscriptenObject {
|
||||
setDisabled(disable: boolean): void;
|
||||
isFolded(): boolean;
|
||||
setFolded(folded: boolean): void;
|
||||
getInstructionList(label: string): InstructionsList;
|
||||
serializeTo(element: SerializerElement): void;
|
||||
unserializeFrom(project: Project, element: SerializerElement): void;
|
||||
getAiGeneratedEventId(): string;
|
||||
|
||||
@@ -15,6 +15,7 @@ declare class gdBaseEvent extends gdBaseEvent {
|
||||
setDisabled(disable: boolean): void;
|
||||
isFolded(): boolean;
|
||||
setFolded(folded: boolean): void;
|
||||
getInstructionList(label: string): gdInstructionsList;
|
||||
serializeTo(element: gdSerializerElement): void;
|
||||
unserializeFrom(project: gdProject, element: gdSerializerElement): void;
|
||||
getAiGeneratedEventId(): string;
|
||||
|
||||
@@ -6,15 +6,22 @@
|
||||
* the size of each event - which we only know after the event has been rendered.
|
||||
*/
|
||||
export default class EventHeightsCache {
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
eventHeights = {};
|
||||
updateTimeoutId: ?TimeoutID = null;
|
||||
eventHeights: { [number]: number } = {};
|
||||
_onHeightsChanged: ?() => void = null;
|
||||
|
||||
setEventHeight(event: gdBaseEvent, height: number) {
|
||||
if (this.eventHeights[event.ptr] === height) return;
|
||||
this.eventHeights[event.ptr] = height;
|
||||
if (this._onHeightsChanged) {
|
||||
this._onHeightsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
getEventHeight(event: gdBaseEvent): number {
|
||||
return this.eventHeights[event.ptr] || 0;
|
||||
}
|
||||
|
||||
setOnHeightsChanged(callback: ?() => void) {
|
||||
this._onHeightsChanged = callback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export default class WhileEvent extends React.Component<
|
||||
projectScopedContainersAccessor={
|
||||
this.props.projectScopedContainersAccessor
|
||||
}
|
||||
idPrefix={this.props.idPrefix}
|
||||
idPrefix={`${this.props.idPrefix}-while`}
|
||||
highlightedSearchText={this.props.highlightedSearchText}
|
||||
highlightedSearchMatchCase={this.props.highlightedSearchMatchCase}
|
||||
/>
|
||||
|
||||
@@ -64,6 +64,7 @@ type Props = {|
|
||||
searchFocusOffset?: ?number,
|
||||
searchFocusedEvent?: ?gdBaseEvent,
|
||||
className?: string,
|
||||
eventsSheetWidth?: number,
|
||||
reactVirtualizedListProps?: {
|
||||
ref?: (list: {
|
||||
scrollToRow: (row: number) => void,
|
||||
@@ -365,6 +366,7 @@ const SortableEventsTree = ({
|
||||
searchFocusOffset,
|
||||
searchFocusedEvent,
|
||||
className,
|
||||
eventsSheetWidth,
|
||||
reactVirtualizedListProps,
|
||||
}: Props): React.MixedElement => {
|
||||
// $FlowFixMe[value-as-type]
|
||||
@@ -390,7 +392,7 @@ const SortableEventsTree = ({
|
||||
[flatData, searchMethod, searchQuery]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
if (!reactVirtualizedListProps || !reactVirtualizedListProps.ref) return;
|
||||
reactVirtualizedListProps.ref({
|
||||
@@ -464,7 +466,7 @@ const SortableEventsTree = ({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<AutoSizer>
|
||||
<AutoSizer defaultWidth={eventsSheetWidth || 0}>
|
||||
{({ width, height }) => (
|
||||
<VariableSizeList
|
||||
ref={listRef}
|
||||
@@ -477,6 +479,7 @@ const SortableEventsTree = ({
|
||||
itemData={itemData}
|
||||
itemKey={itemKey}
|
||||
onScroll={handleScroll}
|
||||
overscanCount={10}
|
||||
>
|
||||
{TreeRow}
|
||||
</VariableSizeList>
|
||||
|
||||
@@ -74,6 +74,10 @@ const getThumbnail = ObjectsRenderingService.getThumbnail.bind(
|
||||
|
||||
const defaultIndentWidth = 22;
|
||||
const smallIndentWidth = 11;
|
||||
// Placeholder height for events whose real height is not yet measured.
|
||||
// Prevents all events from collapsing to 1px at startup, which would cause
|
||||
// the virtualized list to render all of them at once.
|
||||
const defaultEventHeight = 100;
|
||||
|
||||
const styles = {
|
||||
container: { flex: 1, position: 'relative' },
|
||||
@@ -180,8 +184,10 @@ const EventContainer = (props: EventsContainerProps) => {
|
||||
React.useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const height = container ? container.offsetHeight : 0;
|
||||
if (height === 0) {
|
||||
// An empty height means that the event is hidden, when navigating outside of the events sheet tab for example.
|
||||
const width = container ? container.offsetWidth : 0;
|
||||
if (height === 0 || width === 0) {
|
||||
// An empty height or width means the event is hidden (e.g. navigating outside
|
||||
// the events sheet tab), or the container hasn't been sized yet.
|
||||
// Don't store the height in this case.
|
||||
return;
|
||||
}
|
||||
@@ -204,7 +210,10 @@ const EventContainer = (props: EventsContainerProps) => {
|
||||
[onEventContextMenu]
|
||||
);
|
||||
|
||||
const { isPressingRef, contextMenuProps } = useLongTouch(
|
||||
const {
|
||||
isPressingRef,
|
||||
contextMenuProps: longTouchForContextMenuProps,
|
||||
} = useLongTouch(
|
||||
React.useCallback(
|
||||
(domEvent: any) => {
|
||||
onEventContextMenu(domEvent.clientX, domEvent.clientY);
|
||||
@@ -263,7 +272,7 @@ const EventContainer = (props: EventsContainerProps) => {
|
||||
ref={containerRef}
|
||||
onClick={props.onEventClick}
|
||||
onContextMenu={_onEventContextMenu}
|
||||
{...contextMenuProps}
|
||||
{...longTouchForContextMenuProps}
|
||||
>
|
||||
{!!EventComponent && (
|
||||
<div style={styles.eventComponentContainer}>
|
||||
@@ -449,6 +458,11 @@ export type EventsTreeInterface = {|
|
||||
rowIndexes: Array<number>
|
||||
) => Array<EventContext>,
|
||||
scrollToRow: (row: number) => void,
|
||||
scrollToInstruction: (
|
||||
rowIndex: number,
|
||||
listLabel: string,
|
||||
instructionIndex: number
|
||||
) => void,
|
||||
getEventRow: (event: gdBaseEvent) => number,
|
||||
unfoldForEvent: (event: gdBaseEvent) => void,
|
||||
|};
|
||||
@@ -471,6 +485,13 @@ const EventsTree: React.ComponentType<{
|
||||
const _list = React.useRef<?any>(null);
|
||||
const eventsHeightsCache = React.useMemo(() => new EventHeightsCache(), []);
|
||||
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
eventsHeightsCache.setOnHeightsChanged(forceUpdate);
|
||||
return () => eventsHeightsCache.setOnHeightsChanged(null);
|
||||
},
|
||||
[eventsHeightsCache, forceUpdate]
|
||||
);
|
||||
const temporaryUnfoldedNodes = React.useRef<Array<SortableTreeNode>>([]);
|
||||
const _hoverTimerId = React.useRef<?TimeoutID>(null);
|
||||
|
||||
@@ -514,8 +535,28 @@ const EventsTree: React.ComponentType<{
|
||||
|
||||
const scrollToRow = React.useCallback((row: number) => {
|
||||
if (row === -1) return;
|
||||
if (!_list.current) return;
|
||||
_list.current.scrollToRow(row);
|
||||
const list = _list.current;
|
||||
if (!list) return;
|
||||
|
||||
// If the target row is already fully visible in the DOM, skip the scroll.
|
||||
// React-window's scrollToItem uses summed item heights which can be
|
||||
// inaccurate for non-visible rows, causing unnecessary jumps.
|
||||
const container = list.container;
|
||||
if (container) {
|
||||
const element = container.querySelector(`[data-row-index="${row}"]`);
|
||||
if (element) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (
|
||||
elementRect.top >= containerRect.top &&
|
||||
elementRect.bottom <= containerRect.bottom
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.scrollToRow(row);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@@ -578,6 +619,13 @@ const EventsTree: React.ComponentType<{
|
||||
[draggedNode, _restoreFoldedNodes, forceUpdate]
|
||||
);
|
||||
|
||||
// Position-based height snapshot. Used as a fallback in _getRowHeight when
|
||||
// the ptr-based cache has no entry (e.g. right after undo/redo, which
|
||||
// deserializes all events with fresh pointers). Merged rather than replaced
|
||||
// so off-screen events keep their last known height instead of collapsing to
|
||||
// defaultEventHeight.
|
||||
const heightsByRowIndex = React.useRef<{ [number]: number }>({});
|
||||
|
||||
const eventPtrToRowIndex = React.useRef<{ [key: string]: number }>({});
|
||||
const getEventRow = React.useCallback(
|
||||
(searchedEvent: gdBaseEvent) => {
|
||||
@@ -586,6 +634,89 @@ const EventsTree: React.ComponentType<{
|
||||
[eventPtrToRowIndex]
|
||||
);
|
||||
|
||||
const scrollToInstruction = React.useCallback(
|
||||
(rowIndex: number, listLabel: string, instructionIndex: number) => {
|
||||
// Do NOT call scrollToRow here unconditionally. For tall event rows (many
|
||||
// actions), scrollToRow snaps the row's top edge to the viewport top even
|
||||
// when the target instruction is already visible. We only fall back to it
|
||||
// inside tryScrollToElement if the DOM element isn't found (row not yet
|
||||
// rendered by the virtual list).
|
||||
|
||||
const tryScrollToElement = ({
|
||||
retriesLeft,
|
||||
didScrollToRow,
|
||||
}: {
|
||||
retriesLeft: number,
|
||||
didScrollToRow: boolean,
|
||||
}) => {
|
||||
requestAnimationFrame(() => {
|
||||
const list = _list.current;
|
||||
if (!list || !list.container) return;
|
||||
|
||||
// Search by rowIndex (position-based, stable across undo/redo) rather
|
||||
// than by event.ptr which changes after every deserialization.
|
||||
let relativeNodePath = null;
|
||||
const findNode = (nodes: Array<SortableTreeNode>): boolean => {
|
||||
for (const node of nodes) {
|
||||
if (node.rowIndex === rowIndex) {
|
||||
relativeNodePath = node.relativeNodePath;
|
||||
return true;
|
||||
}
|
||||
if (node.children.length > 0 && findNode(node.children))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
findNode(treeDataRoot.current);
|
||||
if (!relativeNodePath) return;
|
||||
|
||||
// For WhileEvent's loop-guard list the idPrefix gets a '-while' suffix
|
||||
// (set in WhileEvent.js) to avoid colliding with the body conditions.
|
||||
const listIdPrefix =
|
||||
listLabel === 'whileConditions'
|
||||
? `event-${relativeNodePath.join('-')}-while`
|
||||
: `event-${relativeNodePath.join('-')}`;
|
||||
const type = listLabel === 'actions' ? 'action' : 'condition';
|
||||
const doc = list.container.ownerDocument;
|
||||
|
||||
// Try the exact index first, then one below (covers undo of add where
|
||||
// the instruction at instructionIndex was removed).
|
||||
const element =
|
||||
doc.getElementById(`${listIdPrefix}-${type}-${instructionIndex}`) ||
|
||||
(instructionIndex > 0
|
||||
? doc.getElementById(
|
||||
`${listIdPrefix}-${type}-${instructionIndex - 1}`
|
||||
)
|
||||
: null);
|
||||
|
||||
if (element) {
|
||||
const containerRect = list.container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const alreadyVisible =
|
||||
elementRect.top >= containerRect.top &&
|
||||
elementRect.bottom <= containerRect.bottom;
|
||||
if (!alreadyVisible) {
|
||||
element.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
} else if (!didScrollToRow) {
|
||||
// Element not in DOM — the row may not be rendered by the virtual
|
||||
// list yet. Scroll to the row first, then retry.
|
||||
scrollToRow(rowIndex);
|
||||
tryScrollToElement({ retriesLeft, didScrollToRow: true });
|
||||
} else if (retriesLeft > 0) {
|
||||
tryScrollToElement({
|
||||
retriesLeft: retriesLeft - 1,
|
||||
didScrollToRow: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryScrollToElement({ retriesLeft: 2, didScrollToRow: false });
|
||||
},
|
||||
[scrollToRow]
|
||||
);
|
||||
|
||||
const temporaryUnfoldNode = React.useCallback(
|
||||
(isOverLazy: boolean, node: SortableTreeNode) => {
|
||||
const { event } = node;
|
||||
@@ -622,7 +753,11 @@ const EventsTree: React.ComponentType<{
|
||||
}
|
||||
|
||||
const height = eventsHeightsCache.getEventHeight(node.event);
|
||||
return height;
|
||||
if (height > 0) return height;
|
||||
// Fall back to the last known height for this row position. This avoids a
|
||||
// visible collapse to defaultEventHeight after undo/redo, which replaces all
|
||||
// event pointers via deserialization and empties the ptr-based cache.
|
||||
return heightsByRowIndex.current[node.rowIndex] || defaultEventHeight;
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -1038,6 +1173,7 @@ const EventsTree: React.ComponentType<{
|
||||
unfoldToLevel,
|
||||
getEventContextAtRowIndexes,
|
||||
scrollToRow,
|
||||
scrollToInstruction,
|
||||
getEventRow,
|
||||
unfoldForEvent,
|
||||
}));
|
||||
@@ -1094,11 +1230,28 @@ const EventsTree: React.ComponentType<{
|
||||
);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
// Recompute the row heights of the tree at each render, because there
|
||||
// is no guarantee that events heights have not changed (resizing, change in event...).
|
||||
// Recompute row heights on every render. This is needed for any change
|
||||
// that affects which event is at which row (deletion, fold/unfold, move)
|
||||
// — not just changes to individual event heights. Scroll-triggered
|
||||
// re-renders (isScrolledTop / isScrolledBottom state flips) are infrequent
|
||||
// and recalculate the same heights, so the extra work is negligible.
|
||||
if (_list.current) {
|
||||
_list.current.recomputeRowHeights();
|
||||
}
|
||||
// Merge newly measured heights into the position-based snapshot so that the
|
||||
// next render after an undo/redo (which replaces all ptrs) can use them as
|
||||
// fallback heights instead of collapsing to defaultEventHeight.
|
||||
const flatData = getFlatDataFromTree({
|
||||
treeData: treeDataRoot.current,
|
||||
getNodeKey,
|
||||
ignoreCollapsed: true,
|
||||
});
|
||||
flatData.forEach(entry => {
|
||||
if (entry.node.event) {
|
||||
const h = eventsHeightsCache.getEventHeight(entry.node.event);
|
||||
if (h > 0) heightsByRowIndex.current[entry.node.rowIndex] = h;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -1151,6 +1304,7 @@ const EventsTree: React.ComponentType<{
|
||||
: null
|
||||
}
|
||||
className={props.searchResults ? eventsTreeWithSearchResults : ''}
|
||||
eventsSheetWidth={props.eventsSheetWidth}
|
||||
reactVirtualizedListProps={{
|
||||
ref: list => {
|
||||
_list.current = list;
|
||||
@@ -1166,7 +1320,11 @@ const EventsTree: React.ComponentType<{
|
||||
event.clientHeight + event.scrollTop >= event.scrollHeight
|
||||
);
|
||||
},
|
||||
scrollToAlignment: 'center',
|
||||
// 'smart': no-op if the row is already visible; centers it only when
|
||||
// it is more than one viewport away. This prevents undo of an in-view
|
||||
// edit from jumping the viewport due to accumulated height-estimate
|
||||
// errors in never-scrolled-to rows.
|
||||
scrollToAlignment: 'smart',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -48,11 +48,7 @@ export type SelectionState = {|
|
||||
selectedEvents: Array<EventContext>,
|
||||
|};
|
||||
|
||||
export const getInitialSelection = (): {
|
||||
selectedEvents: Array<empty>,
|
||||
selectedInstructions: Array<empty>,
|
||||
selectedInstructionsLists: Array<empty>,
|
||||
} => {
|
||||
export const getInitialSelection = (): SelectionState => {
|
||||
return {
|
||||
selectedInstructions: [],
|
||||
selectedEvents: [],
|
||||
@@ -342,11 +338,7 @@ export const selectInstructionsList = (
|
||||
|
||||
export const selectEventsAfterHistoryChange = (
|
||||
eventContexts: Array<EventContext>
|
||||
): {
|
||||
selectedEvents: Array<empty>,
|
||||
selectedInstructions: Array<empty>,
|
||||
selectedInstructionsLists: Array<empty>,
|
||||
} => {
|
||||
): SelectionState => {
|
||||
let newSelection = getInitialSelection();
|
||||
|
||||
eventContexts.forEach(eventContext => {
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
getLastSelectedEventContext,
|
||||
getLastSelectedEventContextWhichCanHaveSubEvents,
|
||||
getLastSelectedInstructionContext,
|
||||
getLastSelectedInstructionsListsContext,
|
||||
getLastSelectedInstructionEventContextWhichCanHaveSubEvents,
|
||||
getLastSelectedEventContextWhichCanHaveVariables,
|
||||
getLastSelectedInstructionEventContextWhichCanHaveVariables,
|
||||
@@ -131,9 +132,26 @@ import { useHighlightedAiGeneratedEvent } from './UseHighlightedAiGeneratedEvent
|
||||
import { findEventByPath } from '../Utils/EventsValidationScanner';
|
||||
import type { SearchFilterParams } from '../Utils/Search';
|
||||
import type { InitialSearchFilterParams } from './SearchPanel';
|
||||
import { isNullPtr } from '../Utils/IsNullPtr';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
// Derives the stable list label stored in history for a given live instruction
|
||||
// list reference. 'whileConditions' identifies the loop-guard list of a WhileEvent,
|
||||
// which shares isCondition=true with the body conditions but is a distinct list.
|
||||
const getInstructionListLabel = (
|
||||
event: gdBaseEvent,
|
||||
instrsList: gdInstructionsList,
|
||||
isCondition: boolean
|
||||
): string => {
|
||||
if (!isCondition) return 'actions';
|
||||
const whileList = event.getInstructionList('whileConditions');
|
||||
// $FlowFixMe[incompatible-exact]
|
||||
if (!isNullPtr(gd, whileList) && whileList.ptr === instrsList.ptr)
|
||||
return 'whileConditions';
|
||||
return 'conditions';
|
||||
};
|
||||
|
||||
const zoomLevel = { min: 1, max: 50 };
|
||||
const loopEventTypes = [
|
||||
'BuiltinCommonInstructions::While',
|
||||
@@ -888,7 +906,22 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
};
|
||||
|
||||
closeInstructionEditor(saveChanges: boolean = false) {
|
||||
const { instruction, eventContext } = this.state.editedInstruction;
|
||||
const {
|
||||
instruction,
|
||||
eventContext,
|
||||
instrsList,
|
||||
indexInList,
|
||||
isCondition,
|
||||
} = this.state.editedInstruction;
|
||||
// Capture instruction index before state is reset:
|
||||
// - for edit: use the existing indexInList
|
||||
// - for add: instruction was just inserted at the end of the list
|
||||
const instructionIndex =
|
||||
indexInList !== undefined && indexInList !== null
|
||||
? indexInList
|
||||
: instrsList
|
||||
? instrsList.size() - 1
|
||||
: undefined;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
@@ -912,6 +945,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
this._saveChangesToHistory('EDIT', {
|
||||
positionsBeforeAction: positions,
|
||||
positionAfterAction: positions,
|
||||
instructionIndex,
|
||||
instructionListLabel: instrsList
|
||||
? getInstructionListLabel(
|
||||
eventContext.event,
|
||||
instrsList,
|
||||
isCondition
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1585,6 +1626,22 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
selectedEvents.forEach(event => eventsRemover.addEventToRemove(event));
|
||||
eventsWithDeletion = eventsWithDeletion.concat(selectedEvents);
|
||||
}
|
||||
// Capture instruction index before deletion for precise undo scroll.
|
||||
const firstDeletedInstrCtx =
|
||||
deleteInstructions && !deleteEvents
|
||||
? getSelectedInstructionsContexts(this.state.selection)[0]
|
||||
: undefined;
|
||||
const deleteInstructionIndex = firstDeletedInstrCtx
|
||||
? firstDeletedInstrCtx.indexInList
|
||||
: undefined;
|
||||
const deleteInstructionListLabel = firstDeletedInstrCtx
|
||||
? getInstructionListLabel(
|
||||
firstDeletedInstrCtx.eventContext.event,
|
||||
firstDeletedInstrCtx.instrsList,
|
||||
firstDeletedInstrCtx.isCondition
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (deleteInstructions) {
|
||||
getSelectedInstructions(this.state.selection).forEach(instruction =>
|
||||
eventsRemover.addInstructionToRemove(instruction)
|
||||
@@ -1615,6 +1672,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
this._saveChangesToHistory('DELETE', {
|
||||
positionsBeforeAction: positions,
|
||||
positionAfterAction: positions,
|
||||
instructionIndex: deleteInstructionIndex,
|
||||
instructionListLabel: deleteInstructionListLabel,
|
||||
});
|
||||
// Deletion of an event/instruction will remove it from the DOM,
|
||||
// potentially losing the focus on the associated DOM elements. Ensure
|
||||
@@ -1658,6 +1717,32 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
};
|
||||
|
||||
pasteInstructions = () => {
|
||||
const lastInstrCtx = getLastSelectedInstructionContext(
|
||||
this.state.selection
|
||||
);
|
||||
const lastListCtx = getLastSelectedInstructionsListsContext(
|
||||
this.state.selection
|
||||
);
|
||||
const pasteInstructionListLabel = lastInstrCtx
|
||||
? getInstructionListLabel(
|
||||
lastInstrCtx.eventContext.event,
|
||||
lastInstrCtx.instrsList,
|
||||
lastInstrCtx.isCondition
|
||||
)
|
||||
: lastListCtx
|
||||
? lastListCtx.isCondition
|
||||
? 'conditions'
|
||||
: 'actions'
|
||||
: undefined;
|
||||
// Capture the selected instruction's ptr and list before paste so we can
|
||||
// find where it ended up after (paste may insert before it, shifting it).
|
||||
const selectedInstructionPtr = lastInstrCtx
|
||||
? lastInstrCtx.instruction.ptr
|
||||
: null;
|
||||
const listSizeBeforePaste = lastListCtx
|
||||
? lastListCtx.instrsList.size()
|
||||
: null;
|
||||
|
||||
if (
|
||||
!pasteInstructionsFromClipboardInSelection(
|
||||
this.props.project,
|
||||
@@ -1666,6 +1751,27 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute instructionIndex AFTER paste by finding where the originally
|
||||
// selected instruction ended up (paste may have inserted before it).
|
||||
// This ensures undo/redo always highlights the original instruction, not
|
||||
// whatever happens to occupy the paste position.
|
||||
let pasteInstructionIndex: ?number = undefined;
|
||||
if (lastInstrCtx && selectedInstructionPtr !== null) {
|
||||
const instrsList = lastInstrCtx.instrsList;
|
||||
for (let i = instrsList.size() - 1; i >= 0; i--) {
|
||||
if (instrsList.get(i).ptr === selectedInstructionPtr) {
|
||||
pasteInstructionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pasteInstructionIndex === undefined) {
|
||||
pasteInstructionIndex = instrsList.size() - 1;
|
||||
}
|
||||
} else if (lastListCtx && listSizeBeforePaste !== null) {
|
||||
pasteInstructionIndex = listSizeBeforePaste;
|
||||
}
|
||||
|
||||
const locatingEvents = getSelectedInstructionsLocatingEvents(
|
||||
this.state.selection
|
||||
);
|
||||
@@ -1675,6 +1781,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
{
|
||||
positionsBeforeAction: positions,
|
||||
positionAfterAction: positions,
|
||||
instructionIndex: pasteInstructionIndex,
|
||||
instructionListLabel: pasteInstructionListLabel,
|
||||
},
|
||||
() => {
|
||||
if (this._eventsTree) this._eventsTree.forceEventsUpdate();
|
||||
@@ -1694,6 +1802,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
eventContext: EventContext,
|
||||
instructionsListContext: InstructionsListContext
|
||||
) => {
|
||||
// Capture index before paste (paste always appends at the end of the list).
|
||||
const instructionIndex = instructionsListContext.instrsList.size();
|
||||
const instructionListLabel = getInstructionListLabel(
|
||||
eventContext.event,
|
||||
instructionsListContext.instrsList,
|
||||
instructionsListContext.isCondition
|
||||
);
|
||||
|
||||
if (
|
||||
!pasteInstructionsFromClipboardInInstructionsList(
|
||||
this.props.project,
|
||||
@@ -1708,6 +1824,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
{
|
||||
positionsBeforeAction: positions,
|
||||
positionAfterAction: positions,
|
||||
instructionIndex,
|
||||
instructionListLabel,
|
||||
},
|
||||
() => {
|
||||
if (this._eventsTree) this._eventsTree.forceEventsUpdate();
|
||||
@@ -1818,6 +1936,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
positions: {
|
||||
positionsBeforeAction: Array<number>,
|
||||
positionAfterAction: Array<number>,
|
||||
instructionIndex?: ?number,
|
||||
instructionListLabel?: ?string,
|
||||
},
|
||||
cb: ?Function
|
||||
) => {
|
||||
@@ -1851,6 +1971,16 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
|
||||
const { _eventsTree: eventsTree } = this;
|
||||
if (!eventsTree) return;
|
||||
|
||||
// Clear selection immediately alongside the history update so that stale
|
||||
// C++ ptrs from the old selection don't accidentally match newly allocated
|
||||
// objects (WASM ptr reuse) and cause a spurious highlight flash during
|
||||
// the re-render triggered by forceEventsUpdate below.
|
||||
this.setState({
|
||||
selection: clearSelection(),
|
||||
eventsHistory: newEventsHistory,
|
||||
});
|
||||
|
||||
eventsTree.forceEventsUpdate(() => {
|
||||
const {
|
||||
changeContext: { positions },
|
||||
@@ -1859,44 +1989,70 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
newEventsHistory.futureActions.length - 1
|
||||
];
|
||||
|
||||
// $FlowFixMe[incompatible-type]
|
||||
let newSelection: SelectionState = getInitialSelection();
|
||||
// If it is a DELETE or EDIT, then the element will be present, so we can select them.
|
||||
// If it is an ADD, then it will not be present, so we can't select them.
|
||||
if (type === 'ADD') {
|
||||
newSelection = clearSelection();
|
||||
} else {
|
||||
const eventContexts = eventsTree.getEventContextAtRowIndexes(
|
||||
positions.positionsBeforeAction
|
||||
);
|
||||
// $FlowFixMe[incompatible-type]
|
||||
newSelection = selectEventsAfterHistoryChange(eventContexts);
|
||||
const eventContexts = eventsTree.getEventContextAtRowIndexes(
|
||||
positions.positionsBeforeAction
|
||||
);
|
||||
|
||||
let newSelection: SelectionState = clearSelection();
|
||||
if (type !== 'ADD') {
|
||||
if (
|
||||
positions.instructionIndex !== undefined &&
|
||||
positions.instructionListLabel !== undefined &&
|
||||
eventContexts[0]
|
||||
) {
|
||||
const maybeInstrsList = eventContexts[0].event.getInstructionList(
|
||||
positions.instructionListLabel
|
||||
);
|
||||
// $FlowFixMe[incompatible-exact]
|
||||
const instrsList = !isNullPtr(gd, maybeInstrsList)
|
||||
? maybeInstrsList
|
||||
: null;
|
||||
// Select the instruction at instructionIndex if it exists, otherwise
|
||||
// select the closest one before it (e.g. after undoing a paste at end).
|
||||
const clampedIdx = instrsList
|
||||
? Math.min(positions.instructionIndex, instrsList.size() - 1)
|
||||
: -1;
|
||||
if (instrsList && clampedIdx >= 0) {
|
||||
newSelection = selectInstruction(
|
||||
eventContexts[0],
|
||||
clearSelection(),
|
||||
{
|
||||
isCondition: positions.instructionListLabel !== 'actions',
|
||||
instrsList,
|
||||
instruction: instrsList.get(clampedIdx),
|
||||
indexInList: clampedIdx,
|
||||
},
|
||||
false
|
||||
);
|
||||
} else {
|
||||
newSelection = selectEventsAfterHistoryChange(eventContexts);
|
||||
}
|
||||
} else {
|
||||
newSelection = selectEventsAfterHistoryChange(eventContexts);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
// $FlowFixMe[incompatible-type]
|
||||
{
|
||||
selection: newSelection,
|
||||
eventsHistory: newEventsHistory,
|
||||
},
|
||||
() => {
|
||||
const row = positions.positionsBeforeAction[0];
|
||||
this.setState({ selection: newSelection }, () => {
|
||||
const row = positions.positionsBeforeAction[0];
|
||||
|
||||
if (row !== undefined) {
|
||||
// Whether it is an ADD, EDIT or DELETE, scroll to the place where it was done.
|
||||
if (row !== undefined) {
|
||||
if (
|
||||
positions.instructionIndex !== undefined &&
|
||||
positions.instructionListLabel !== undefined &&
|
||||
eventContexts[0]
|
||||
) {
|
||||
eventsTree.scrollToInstruction(
|
||||
row,
|
||||
positions.instructionListLabel,
|
||||
positions.instructionIndex
|
||||
);
|
||||
} else {
|
||||
eventsTree.scrollToRow(row);
|
||||
// Hack: because of the virtualization and the undo/redo, we lose the heights of events
|
||||
// (at least some, because they are different objects in memory).
|
||||
// While they are recomputed when rendered, scroll again to be sure we don't end
|
||||
// up at the very beginning (if everything was recomputed from 0) or at
|
||||
// an offset too large.
|
||||
setTimeout(() => {
|
||||
eventsTree.scrollToRow(row);
|
||||
}, 70);
|
||||
}
|
||||
this.updateToolbar();
|
||||
}
|
||||
);
|
||||
this._ensureFocused();
|
||||
this.updateToolbar();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1913,6 +2069,13 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
|
||||
const { _eventsTree: eventsTree } = this;
|
||||
if (!eventsTree) return;
|
||||
|
||||
// Clear selection immediately to prevent stale ptr flashes (see undo above).
|
||||
this.setState({
|
||||
selection: clearSelection(),
|
||||
eventsHistory: newEventsHistory,
|
||||
});
|
||||
|
||||
eventsTree.forceEventsUpdate(() => {
|
||||
const {
|
||||
changeContext: { positions },
|
||||
@@ -1921,44 +2084,74 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
newEventsHistory.previousActions.length - 1
|
||||
];
|
||||
|
||||
// If it is a ADD or EDIT, then the element will be present, so we can select them.
|
||||
// If it is a DELETE, then they will not be present, so we can't select them.
|
||||
// $FlowFixMe[incompatible-type]
|
||||
let newSelection: SelectionState = getInitialSelection();
|
||||
if (type === 'DELETE') {
|
||||
newSelection = clearSelection();
|
||||
} else {
|
||||
const eventContexts = eventsTree.getEventContextAtRowIndexes(
|
||||
positions.positionAfterAction
|
||||
);
|
||||
// $FlowFixMe[incompatible-type]
|
||||
newSelection = selectEventsAfterHistoryChange(eventContexts);
|
||||
const eventContextsForScroll = eventsTree.getEventContextAtRowIndexes(
|
||||
positions.positionsBeforeAction
|
||||
);
|
||||
|
||||
let newSelection: SelectionState = clearSelection();
|
||||
if (type !== 'DELETE') {
|
||||
if (
|
||||
positions.instructionIndex !== undefined &&
|
||||
positions.instructionListLabel !== undefined &&
|
||||
eventContextsForScroll[0]
|
||||
) {
|
||||
const maybeInstrsList = eventContextsForScroll[0].event.getInstructionList(
|
||||
positions.instructionListLabel
|
||||
);
|
||||
// $FlowFixMe[incompatible-exact]
|
||||
const instrsList = !isNullPtr(gd, maybeInstrsList)
|
||||
? maybeInstrsList
|
||||
: null;
|
||||
const clampedIdx = instrsList
|
||||
? Math.min(positions.instructionIndex, instrsList.size() - 1)
|
||||
: -1;
|
||||
if (instrsList && clampedIdx >= 0) {
|
||||
newSelection = selectInstruction(
|
||||
eventContextsForScroll[0],
|
||||
clearSelection(),
|
||||
{
|
||||
isCondition: positions.instructionListLabel !== 'actions',
|
||||
instrsList,
|
||||
instruction: instrsList.get(clampedIdx),
|
||||
indexInList: clampedIdx,
|
||||
},
|
||||
false
|
||||
);
|
||||
} else {
|
||||
const eventContexts = eventsTree.getEventContextAtRowIndexes(
|
||||
positions.positionAfterAction
|
||||
);
|
||||
newSelection = selectEventsAfterHistoryChange(eventContexts);
|
||||
}
|
||||
} else {
|
||||
const eventContexts = eventsTree.getEventContextAtRowIndexes(
|
||||
positions.positionAfterAction
|
||||
);
|
||||
newSelection = selectEventsAfterHistoryChange(eventContexts);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
// $FlowFixMe[incompatible-type]
|
||||
{
|
||||
selection: newSelection,
|
||||
eventsHistory: newEventsHistory,
|
||||
},
|
||||
() => {
|
||||
const row = positions.positionsBeforeAction[0];
|
||||
this.setState({ selection: newSelection }, () => {
|
||||
const row = positions.positionsBeforeAction[0];
|
||||
|
||||
if (row !== undefined) {
|
||||
// Whether it was an ADD, EDIT or DELETE, scroll to the place where it will happen.
|
||||
if (row !== undefined) {
|
||||
if (
|
||||
positions.instructionIndex !== undefined &&
|
||||
positions.instructionListLabel !== undefined &&
|
||||
eventContextsForScroll[0]
|
||||
) {
|
||||
eventsTree.scrollToInstruction(
|
||||
row,
|
||||
positions.instructionListLabel,
|
||||
positions.instructionIndex
|
||||
);
|
||||
} else {
|
||||
eventsTree.scrollToRow(row);
|
||||
// Hack: because of the virtualization and the undo/redo, we lose the heights of events
|
||||
// (at least some, because they are different objects in memory).
|
||||
// While they are recomputed when rendered, scroll again to be sure we don't end
|
||||
// up at the very beginning (if everything was recomputed from 0) or at
|
||||
// an offset too large.
|
||||
setTimeout(() => {
|
||||
eventsTree.scrollToRow(row);
|
||||
}, 70);
|
||||
}
|
||||
this.updateToolbar();
|
||||
}
|
||||
);
|
||||
this._ensureFocused();
|
||||
this.updateToolbar();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const ImageThumbnail = (props: Props): React.MixedElement => {
|
||||
const [error, setError] = React.useState(false);
|
||||
|
||||
// Allow a long press to show the context menu
|
||||
const longTouchForContextMenuProps = useLongTouch(
|
||||
const { contextMenuProps: longTouchForContextMenuProps } = useLongTouch(
|
||||
React.useCallback(
|
||||
event => {
|
||||
if (onContextMenu) onContextMenu(event.clientX, event.clientY);
|
||||
|
||||
Reference in New Issue
Block a user