Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Core/GDCore/Events/Event.h
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,20 @@ class GD_CORE_API BaseEvent {
const gd::String& GetAiGeneratedEventId() const {
return aiGeneratedEventId;
}

/**
* \brief Set the event bookmark ID.
*/
void SetEventBookmarkId(const gd::String& eventBookmarkId_) {
eventBookmarkId = eventBookmarkId_;
}

/**
* \brief Get the event bookmark ID.
*/
const gd::String& GetEventBookmarkId() const {
return eventBookmarkId;
}
///@}

std::weak_ptr<gd::BaseEvent>
Expand All @@ -319,6 +333,7 @@ class GD_CORE_API BaseEvent {
gd::String type; ///< Type of the event. Must be assigned at the creation.
///< Used for saving the event for instance.
gd::String aiGeneratedEventId; ///< When generated by an AI/external tool.
gd::String eventBookmarkId; ///< Bookmark identifier for the event.

static gd::EventsList badSubEvents;
static gd::VariablesContainer badLocalVariables;
Expand Down
4 changes: 4 additions & 0 deletions Core/GDCore/Events/Serialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ void EventsListSerialization::UnserializeEventsFrom(
event->SetFolded(eventElem.GetBoolAttribute("folded", false));
event->SetAiGeneratedEventId(
eventElem.GetStringAttribute("aiGeneratedEventId", ""));
event->SetEventBookmarkId(
eventElem.GetStringAttribute("eventBookmarkId", ""));

list.InsertEvent(event, list.GetEventsCount());
}
Expand All @@ -240,6 +242,8 @@ void EventsListSerialization::SerializeEventsTo(const EventsList& list,
if (event.IsFolded()) eventElem.SetAttribute("folded", event.IsFolded());
if (!event.GetAiGeneratedEventId().empty())
eventElem.SetAttribute("aiGeneratedEventId", event.GetAiGeneratedEventId());
if (!event.GetEventBookmarkId().empty())
eventElem.SetAttribute("eventBookmarkId", event.GetEventBookmarkId());
eventElem.AddChild("type").SetValue(event.GetType());

event.SerializeTo(eventElem);
Expand Down
3 changes: 3 additions & 0 deletions GDevelop.js/Bindings/Bindings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -2462,6 +2462,9 @@ interface BaseEvent {

[Const, Ref] DOMString GetAiGeneratedEventId();
void SetAiGeneratedEventId([Const] DOMString aiGeneratedEventId);

[Const, Ref] DOMString GetEventBookmarkId();
void SetEventBookmarkId([Const] DOMString eventBookmarkId);
};

interface StandardEvent {
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,8 @@ export class BaseEvent extends EmscriptenObject {
unserializeFrom(project: Project, element: SerializerElement): void;
getAiGeneratedEventId(): string;
setAiGeneratedEventId(aiGeneratedEventId: string): void;
getEventBookmarkId(): string;
setEventBookmarkId(eventBookmarkId: string): void;
}

export class StandardEvent extends BaseEvent {
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/types/gdbaseevent.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ declare class gdBaseEvent extends gdBaseEvent {
unserializeFrom(project: gdProject, element: gdSerializerElement): void;
getAiGeneratedEventId(): string;
setAiGeneratedEventId(aiGeneratedEventId: string): void;
getEventBookmarkId(): string;
setEventBookmarkId(eventBookmarkId: string): void;
delete(): void;
ptr: number;
};
72 changes: 72 additions & 0 deletions newIDE/app/src/EventsSheet/Bookmarks/BookmarksPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.bookmarksPanelContainer {
display: flex;
flex-direction: column;
z-index: 10;
pointer-events: auto;
}

.bookmarksPanelContainer.mobile {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: min(300px, 60vh);
max-height: 80vh;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.2);
transform: translateY(100%);
transition: transform 0.3s ease-in-out;
}

.bookmarksPanelContainer.mobile.open {
transform: translateY(0);
}

.bookmarksPanelContainer.desktop {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 280px;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
}

.bookmarksPanelContainer.desktop.open {
transform: translateX(0%);
}

.emptyContainer {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
padding: 20px;
}

.bookmarkItem {
display: flex;
align-items: center;
padding: 8px 4px 8px 5px;
gap: 4px;
border-radius: 4px;
cursor: pointer;
border-bottom: 1px solid;
transition: background-color 0.2s ease;
}

.bookmarkItem:hover {
background-color: rgba(0, 0, 0, 0.05);
}

.bookmarkItemName {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.starIcon {
margin-right: 8px;
}
230 changes: 230 additions & 0 deletions newIDE/app/src/EventsSheet/Bookmarks/BookmarksUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// @flow
import { rgbToHex } from '../../Utils/ColorTransformer';

const gd: libGDevelop = global.gd;

export type Bookmark = {|
eventPtr: number,
name: string,
eventType: string,
id: string,
timestamp: number,
borderLeftColor?: ?string,
|};

/**
* Scan all events recursively and collect those that are bookmarked
*/
export const scanEventsForBookmarks = (
events: gdEventsList
): Array<Bookmark> => {
if (!events) return [];

const bookmarks: Array<Bookmark> = [];

const scanEventsList = (eventsList: gdEventsList) => {
try {
// Safety check: ensure eventsList is valid and has the getEventsCount method
if (!eventsList || typeof eventsList.getEventsCount !== 'function') {
return;
}

for (let i = 0; i < eventsList.getEventsCount(); i++) {
const event = eventsList.getEventAt(i);
if (!event) continue;

try {
// Check if event has a bookmark ID
const bookmarkId =
event.getEventBookmarkId && event.getEventBookmarkId();
if (bookmarkId && bookmarkId.length > 0) {
const bookmark: Bookmark = {
eventPtr: event.ptr,
name: generateBookmarkName(event),
eventType: event.getType(),
id: bookmarkId,
timestamp: Date.now(),
borderLeftColor: getEventTypeColor(event),
};
bookmarks.push(bookmark);
}

// Recursively scan sub-events
if (event.canHaveSubEvents && event.canHaveSubEvents()) {
const subEvents = event.getSubEvents();
if (subEvents) {
scanEventsList(subEvents);
}
}
} catch (err) {
console.error('Error processing event for bookmarks:', err);
}
}
} catch (err) {
console.error('Error scanning event list for bookmarks:', err);
}
};

scanEventsList(events);

return bookmarks;
};

/**
* Get the border color for a bookmark based on the event type
*/
const getEventTypeColor = (event: gdBaseEvent): ?string => {
const eventType = event.getType();

if (eventType === 'BuiltinCommonInstructions::Comment') {
const commentEvent = gd.asCommentEvent(event);
return `#${rgbToHex(
commentEvent.getBackgroundColorRed(),
commentEvent.getBackgroundColorGreen(),
commentEvent.getBackgroundColorBlue()
)}`;
}

if (eventType === 'BuiltinCommonInstructions::Group') {
const groupEvent = gd.asGroupEvent(event);
return `#${rgbToHex(
groupEvent.getBackgroundColorR(),
groupEvent.getBackgroundColorG(),
groupEvent.getBackgroundColorB()
)}`;
}

return null;
};

/**
* Generate a default name for a bookmark based on the event
*/
export const generateBookmarkName = (event: gdBaseEvent): string => {
const eventType = event.getType();

// For comment events, use the comment text
if (eventType === 'BuiltinCommonInstructions::Comment') {
const commentEvent = gd.asCommentEvent(event);
const comment = commentEvent.getComment();
if (comment.length > 0) {
return comment.length > 50 ? comment.substring(0, 50) + '...' : comment;
}
return 'Comment';
}

// For group events, use the group name
if (eventType === 'BuiltinCommonInstructions::Group') {
const groupEvent = gd.asGroupEvent(event);
const name = groupEvent.getName();
if (name.length > 0) {
return name.length > 50 ? name.substring(0, 50) + '...' : name;
}
return 'Group';
}

// For standard events, try to extract first condition or action text
if (eventType === 'BuiltinCommonInstructions::Standard') {
const standardEvent = gd.asStandardEvent(event);

// Try to get first condition
const conditions = standardEvent.getConditions();
if (conditions.size() > 0) {
const firstCondition = conditions.get(0);
const type = firstCondition.getType();
if (type.length > 0) {
const conditionText = type.replace(/:/g, ' ');
return conditionText.length > 50
? conditionText.substring(0, 50) + '...'
: conditionText;
}
}

// Try to get first action
const actions = standardEvent.getActions();
if (actions.size() > 0) {
const firstAction = actions.get(0);
const type = firstAction.getType();
if (type.length > 0) {
const actionText = type.replace(/:/g, ' ');
return actionText.length > 50
? actionText.substring(0, 50) + '...'
: actionText;
}
}

return 'Standard Event';
}

// For other event types, use a generic name based on the type
const readableType = eventType
.replace('BuiltinCommonInstructions::', '')
.replace(/([A-Z])/g, ' $1')
.trim();

return readableType || 'Event';
};

/**
* Recursively search for an event by its pointer in an event list
*/
export const findEventByPtr = (
events: gdEventsList,
ptr: number
): ?gdBaseEvent => {
if (!events || !ptr) return null;

for (let i = 0; i < events.getEventsCount(); i++) {
const event = events.getEventAt(i);
if (!event) continue;

if (event.ptr === ptr) return event;

// Recursively search sub-events
if (event.canHaveSubEvents && event.canHaveSubEvents()) {
const subEvents = event.getSubEvents();
if (subEvents) {
const found = findEventByPtr(subEvents, ptr);
if (found) return found;
}
}
}

return null;
};

/**
* Recursively search for an event by its pointer and return it with its parent list and index
*/
export type EventLocation = {|
event: gdBaseEvent,
eventsList: gdEventsList,
indexInList: number,
|};

export const findEventLocationByPtr = (
events: gdEventsList,
ptr: number
): ?EventLocation => {
if (!events || !ptr) return null;

for (let i = 0; i < events.getEventsCount(); i++) {
const event = events.getEventAt(i);
if (!event) continue;

if (event.ptr === ptr) {
return { event, eventsList: events, indexInList: i };
}

// Recursively search sub-events
if (event.canHaveSubEvents && event.canHaveSubEvents()) {
const subEvents = event.getSubEvents();
if (subEvents) {
const found = findEventLocationByPtr(subEvents, ptr);
if (found) return found;
}
}
}

return null;
};
Loading