/* Copyright (c) 2021 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "shell/ShellKeyCodes.h" #include "uitoolkit/UIMenuBar.h" #include "uitoolkit/UIToolkitAppearance.h" #include "uitoolkit/UIToolkitContext.h" #include "uitoolkit/UIToolkitDrawing.h" #include "utilities/IOUtilities.h" #include #include #include #include #define stemobject_implementation UIMenuBar stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_entry(hitTest); stemobject_vtable_entry(mouseDown); stemobject_vtable_entry(mouseUp); stemobject_vtable_entry(mouseMoved); stemobject_vtable_entry(mouseDragged); stemobject_vtable_entry(scrollWheel); stemobject_vtable_entry(keyDown); stemobject_vtable_entry(menuActionDown); stemobject_vtable_entry(menuActionUp); stemobject_vtable_entry(menuDirectionDown); stemobject_vtable_entry(acceptsFocus); stemobject_vtable_entry(getBounds); stemobject_vtable_entry(getFocusBounds); stemobject_vtable_entry(draw); stemobject_vtable_entry(listRenderables); stemobject_vtable_end(); #define SEPARATOR_HEIGHT 1 #define MOUSE_UP_CLOSE_INTERVAL 0.5 #define SUBMENU_OPEN_DELAY 0.3 #define CONTEXT_MENU_OPEN_INDEX (UINT_MAX - 1) struct UIMenuContents { unsigned int itemCount; struct UIMenuItem_private * items; unsigned int highlightIndex; Vector2f topLeft; Vector2f size; bool validationDirty; bool layoutDirty; }; struct UIMenuItem_private { String title; int identifier; bool enabled; bool checked; UIKeyShortcut * shortcut; String shortcutString; float offsetY; float titleWidth; UIAtlasEntry customIcon; Color4f textColor; bool animateWhenInvoked; struct UIMenuContents contents; }; struct UIMenu_private { String title; int identifier; float offsetX; float titleWidth; struct UIMenuContents contents; }; UIMenuBar * UIMenuBar_create(unsigned int menuCount, UIMenu * menus, Vector2f position, Vector2f relativeOrigin, float width, UIMenuBar_actionCallback actionCallback, UIMenuBar_validateCallback validateCallback, UIMenuBar_contextMenuCallback contextMenuCallback, void * callbackContext, UIAppearance appearance) { stemobject_create_implementation(init, menuCount, menus, position, relativeOrigin, width, actionCallback, validateCallback, contextMenuCallback, callbackContext, appearance) } static String createKeyShortcutString(UIKeyShortcut * shortcut) { if (shortcut == NULL) { return STR_NULL; } char shortcutString[40] = {0}; UIKeyShortcut_getHumanReadableString(shortcut, shortcutString, sizeof(shortcutString)); return String_copy(STR(shortcutString)); } static struct UIMenuContents copyMenuContents(UIMenuBar * self, unsigned int itemCount, struct UIMenuItem * items); static struct UIMenuItem_private copyMenuItem(UIMenuBar * self, UIMenuItem item) { struct UIMenuItem_private copy; copy.title = String_copy(item.title); copy.identifier = item.identifier; copy.enabled = item.enabled; copy.checked = item.checked; copy.customIcon.bounds = RECT4f_EMPTY; copy.customIcon.size = VECTOR2f_ZERO; copy.textColor = getAppearanceColor4f(self->appearance, UIMenuBar_textColor); copy.shortcut = UIKeyShortcut_copy(item.shortcut); copy.shortcutString = createKeyShortcutString(copy.shortcut); UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); if (copy.title.bytes == NULL) { copy.titleWidth = 0.0f; } else { copy.titleWidth = ceilf(call_virtual(measureString, typeface, copy.title)); } copy.contents = copyMenuContents(self, item.subitemCount, item.subitems); copy.titleWidth += getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding).x * 4 + self->leftRightIconPadding * 2; if (copy.shortcutString.bytes != NULL && copy.title.bytes != NULL && copy.contents.itemCount == 0) { copy.titleWidth += ceilf(call_virtual(measureString, typeface, copy.shortcutString)) - self->leftRightIconPadding + getAppearanceFloat(self->appearance, UIMenuBar_itemShortcutPadding); } copy.animateWhenInvoked = copy.shortcut != NULL && (copy.shortcut->modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT); return copy; } static struct UIMenuContents copyMenuContents(UIMenuBar * self, unsigned int itemCount, struct UIMenuItem * items) { struct UIMenuContents contents; contents.itemCount = itemCount; contents.highlightIndex = UINT_MAX; contents.validationDirty = true; contents.layoutDirty = false; contents.size = VECTOR2f_ZERO; if (itemCount == 0) { contents.items = NULL; } else { contents.items = malloc(itemCount * sizeof(*contents.items)); UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float lineHeight = call_virtual(getLineHeight, typeface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); for (unsigned int itemIndex = 0; itemIndex < itemCount; itemIndex++) { contents.items[itemIndex] = copyMenuItem(self, items[itemIndex]); contents.items[itemIndex].offsetY = -contents.size.y; if (contents.items[itemIndex].title.bytes == NULL) { contents.size.y += SEPARATOR_HEIGHT; } else { if (contents.items[itemIndex].titleWidth > contents.size.x) { contents.size.x = contents.items[itemIndex].titleWidth; } contents.size.y += lineHeight + itemPadding.y * 2; } } } return contents; } static void assignSubmenuPositions(UIMenuBar * self, struct UIMenuContents * contents, Vector2f topLeft) { contents->topLeft = topLeft; for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { assignSubmenuPositions(self, &contents->items[itemIndex].contents, VECTOR2f(topLeft.x + contents->size.x, topLeft.y + contents->items[itemIndex].offsetY)); } } bool UIMenuBar_init(UIMenuBar * self, unsigned int menuCount, UIMenu * menus, Vector2f position, Vector2f relativeOrigin, float width, UIMenuBar_actionCallback actionCallback, UIMenuBar_validateCallback validateCallback, UIMenuBar_contextMenuCallback contextMenuCallback, void * callbackContext, UIAppearance appearance) { call_super(init, self, position, relativeOrigin, appearance); Vector2f submenuArrowSize = getAppearanceAtlasEntry(self->appearance, UIMenuBar_menuSubmenuArrow).size; Vector2f checkmarkSize = getAppearanceAtlasEntry(self->appearance, UIMenuBar_menuCheckmark).size; self->leftRightIconPadding = submenuArrowSize.x; if (checkmarkSize.x > self->leftRightIconPadding) { self->leftRightIconPadding = checkmarkSize.x; } self->menuCount = menuCount; self->menus = malloc(self->menuCount * sizeof(*self->menus)); float offsetX = 0.0f; UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float menuPadding = getAppearanceFloat(self->appearance, UIMenuBar_menuPadding); float menuHeight = getAppearanceFloat(self->appearance, UIMenuBar_menuHeight); for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { self->menus[menuIndex].title = String_copy(menus[menuIndex].title); self->menus[menuIndex].identifier = menus[menuIndex].identifier; self->menus[menuIndex].offsetX = offsetX; self->menus[menuIndex].titleWidth = ceilf(call_virtual(measureString, typeface, self->menus[menuIndex].title) + menuPadding * 2); self->menus[menuIndex].contents = copyMenuContents(self, menus[menuIndex].itemCount, menus[menuIndex].items); assignSubmenuPositions(self, &self->menus[menuIndex].contents, VECTOR2f(offsetX, -menuHeight)); offsetX += self->menus[menuIndex].titleWidth; } self->width = width; self->actionCallback = actionCallback; self->validateCallback = validateCallback; self->contextMenuCallback = contextMenuCallback; self->callbackContext = callbackContext; self->openItems[0] = UINT_MAX; self->openTime = 0.0; self->contextMenuContents = NULL; self->submenuToOpenAfterDelay = UINT_MAX; self->openingSubmenuDepth = UINT_MAX; self->submenuOpenTimerID = SHELL_TIMER_INVALID; self->actionAnimationEndTime = 0.0; self->pendingMenuIdentifier = self->pendingMenuItemIdentifier = INT_MIN; self->pendingMenuTimerID = SHELL_TIMER_INVALID; return true; } static void disposeMenuContents(struct UIMenuContents * contents); static void disposeMenuItem(struct UIMenuItem_private item) { String_free(item.title); String_free(item.shortcutString); free(item.shortcut); disposeMenuContents(&item.contents); } static void disposeMenuContents(struct UIMenuContents * contents) { for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { disposeMenuItem(contents->items[itemIndex]); } free(contents->items); contents->itemCount = 0; contents->items = NULL; } void UIMenuBar_dispose(UIMenuBar * self) { for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { String_free(self->menus[menuIndex].title); disposeMenuContents(&self->menus[menuIndex].contents); } free(self->menus); if (self->contextMenuContents != NULL) { disposeMenuContents(self->contextMenuContents); free(self->contextMenuContents); } call_super_virtual(dispose, self); } bool UIMenuBar_hitTest(UIMenuBar * self, float x, float y, UIHitTestType type, int * outPriority, bool * outForwardNext) { if (self->openItems[0] != UINT_MAX) { *outPriority = MENU_BAR_OPEN_HIT_TEST_PRIORITY; return true; } if (type == HIT_TEST_KEY_DOWN) { *outPriority = MENU_BAR_KEY_DOWN_PRIORITY; return true; } bool result = call_super_virtual(hitTest, self, x, y, type, outPriority, outForwardNext); if (result) { *outPriority = MENU_BAR_CLOSED_HIT_TEST_PRIORITY; } return result; } static struct UIMenuItem_private * getMenuItemAtPosition(UIMenuBar * self, float x, float y, struct UIMenuContents ** outContents, unsigned int * outItemIndex, unsigned int * outDepth) { unsigned int depth; for (depth = 0; depth < MENU_DEPTH_MAX; depth++) { if (self->openItems[depth] == UINT_MAX) { break; } } if (depth == 0) { return NULL; } Rect4f bounds = call_virtual(getBounds, self); struct UIMenuContents * openContents[depth]; openContents[0] = &self->menus[self->openItems[0]].contents; for (unsigned int openDepth = 1; openDepth < depth; openDepth++) { if (self->openItems[openDepth] == CONTEXT_MENU_OPEN_INDEX) { openContents[openDepth] = self->contextMenuContents; } else { openContents[openDepth] = &openContents[openDepth - 1]->items[self->openItems[openDepth]].contents; } } do { depth--; if (x >= bounds.xMin + openContents[depth]->topLeft.x && x <= bounds.xMin + openContents[depth]->topLeft.x + openContents[depth]->size.x && y <= bounds.yMax + openContents[depth]->topLeft.y && y >= bounds.yMax + openContents[depth]->topLeft.y - openContents[depth]->size.y) { float offsetYFromTopOfMenu = bounds.yMax + openContents[depth]->topLeft.y - y; for (unsigned int itemIndex = openContents[depth]->itemCount - 1; itemIndex < openContents[depth]->itemCount; itemIndex--) { if (offsetYFromTopOfMenu >= -openContents[depth]->items[itemIndex].offsetY) { if (outContents != NULL) { *outContents = openContents[depth]; } if (outItemIndex != NULL) { *outItemIndex = itemIndex; } if (outDepth != NULL) { *outDepth = depth; } return &openContents[depth]->items[itemIndex]; } } } if (openContents[depth] == self->contextMenuContents) { break; } } while (depth > 0); return NULL; } static void resetMenuContentsValidation(UIMenuBar * self, struct UIMenuContents * contents) { contents->validationDirty = true; for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { resetMenuContentsValidation(self, &contents->items[itemIndex].contents); } } static void resetValidation(UIMenuBar * self) { for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { resetMenuContentsValidation(self, &self->menus[menuIndex].contents); } } static void validateMenuItem(UIMenuBar * self, int menuIdentifier, struct UIMenuItem_private * item, struct UIMenuContents * parentContents) { UIMenuItemProperties properties; properties.enabled = item->enabled; properties.checked = item->checked; String_strncpy_utf8(properties.title, item->title, sizeof(properties.title)); properties.textColor = item->textColor; properties.customIcon = item->customIcon; self->validateCallback(self, menuIdentifier, item->identifier, &properties, self->callbackContext); item->enabled = properties.enabled; item->checked = properties.checked; item->textColor = properties.textColor; item->customIcon = properties.customIcon; if (String_strcmp_utf8(item->title, properties.title)) { String_free(item->title); item->title = String_copy(STR(properties.title)); parentContents->layoutDirty = true; } } static void validateMenuContents(UIMenuBar * self, int menuIdentifier, struct UIMenuContents * contents) { if (self->validateCallback != NULL && contents->validationDirty) { for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { if (contents->items[itemIndex].title.bytes != NULL) { validateMenuItem(self, menuIdentifier, &contents->items[itemIndex], contents); } } contents->validationDirty = false; } if (contents->layoutDirty) { contents->size.x = 0; UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); float shortcutPadding = getAppearanceFloat(self->appearance, UIMenuBar_itemShortcutPadding); for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { if (contents->items[itemIndex].title.bytes != NULL) { contents->items[itemIndex].titleWidth = ceilf(call_virtual(measureString, typeface, contents->items[itemIndex].title)) + itemPadding.x * 4 + self->leftRightIconPadding * 2; if (contents->items[itemIndex].shortcutString.bytes != NULL && contents->items[itemIndex].contents.itemCount == 0) { contents->items[itemIndex].titleWidth += ceilf(call_virtual(measureString, typeface, contents->items[itemIndex].shortcutString)) - self->leftRightIconPadding + shortcutPadding; } if (contents->items[itemIndex].titleWidth > contents->size.x) { contents->size.x = contents->items[itemIndex].titleWidth; } } } assignSubmenuPositions(self, contents, contents->topLeft); contents->layoutDirty = false; } } static void openSubmenu(UIMenuBar * self, unsigned int itemIndex, unsigned int depth) { if (self->openItems[0] == UINT_MAX || depth >= MENU_DEPTH_MAX) { return; } self->openItems[depth + 1] = itemIndex; if (depth + 2 < MENU_DEPTH_MAX) { self->openItems[depth + 2] = UINT_MAX; } struct UIMenuContents * contents = &self->menus[self->openItems[0]].contents; for (unsigned int submenuDepth = 1; submenuDepth <= depth; submenuDepth++) { contents = &contents[submenuDepth - 1].items[self->openItems[submenuDepth]].contents; } validateMenuContents(self, self->menus[self->openItems[0]].identifier, &contents->items[itemIndex].contents); self->submenuToOpenAfterDelay = UINT_MAX; self->openingSubmenuDepth = UINT_MAX; Shell_cancelTimer(self->submenuOpenTimerID); self->submenuOpenTimerID = SHELL_TIMER_INVALID; Shell_redisplay(); } static void displayContextMenuCallback(unsigned int itemCount, UIMenuItem * items, void * menuBarContext) { UIMenuBar * self = menuBarContext; if (self->contextMenuContents == NULL) { self->contextMenuContents = malloc(sizeof(*self->contextMenuContents)); } else { disposeMenuContents(self->contextMenuContents); } *self->contextMenuContents = copyMenuContents(self, itemCount, items); } UIEventResponse UIMenuBar_mouseDown(UIMenuBar * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (buttonNumber == 0) { unsigned int itemIndex, itemDepth; struct UIMenuItem_private * item = getMenuItemAtPosition(self, x, y, NULL, &itemIndex, &itemDepth); if (item != NULL && item->title.bytes != NULL && item->enabled && item->contents.itemCount > 0) { openSubmenu(self, itemIndex, itemDepth); } Rect4f bounds = call_virtual(getBounds, self); if (self->openItems[0] != UINT_MAX) { self->openTime = DBL_MAX; } else if (self->openItems[0] == UINT_MAX && y >= bounds.yMin && y <= bounds.yMax) { for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (x >= bounds.xMin + self->menus[menuIndex].offsetX && x <= bounds.xMin + self->menus[menuIndex].offsetX + self->menus[menuIndex].titleWidth) { resetValidation(self); self->openItems[0] = menuIndex; self->menus[menuIndex].contents.highlightIndex = UINT_MAX; self->openItems[1] = UINT_MAX; self->openTime = referenceTime; validateMenuContents(self, self->menus[menuIndex].identifier, &self->menus[menuIndex].contents); break; } } } return RESPONSE_HANDLED; } if (buttonNumber == 1 && (self->contextMenuContents == NULL || self->contextMenuContents->itemCount == 0)) { unsigned int itemIndex, itemDepth; struct UIMenuItem_private * item = getMenuItemAtPosition(self, x, y, NULL, &itemIndex, &itemDepth); if (item != NULL && item->title.bytes != NULL && item->enabled && self->contextMenuCallback != NULL) { if (self->contextMenuContents != NULL) { disposeMenuContents(self->contextMenuContents); } self->contextMenuCallback(self, self->menus[self->openItems[0]].identifier, item->identifier, displayContextMenuCallback, self, self->callbackContext); if (self->contextMenuContents != NULL && self->contextMenuContents->itemCount > 0 && itemDepth + 2 < MENU_DEPTH_MAX) { Rect4f bounds = call_virtual(getBounds, self); self->contextMenuContents->topLeft = VECTOR2f(x - bounds.xMin, y - bounds.yMax); self->openItems[itemDepth + 1] = CONTEXT_MENU_OPEN_INDEX; self->openItems[itemDepth + 2] = UINT_MAX; return RESPONSE_HANDLED; } } } return RESPONSE_HANDLED; } static void openSubmenuTimerCallback(ShellTimer timerID, void * context) { UIMenuBar * self = context; openSubmenu(self, self->submenuToOpenAfterDelay, self->openingSubmenuDepth); } static bool mouseMovedOrDragged(UIMenuBar * self, float x, float y) { Rect4f bounds = call_virtual(getBounds, self); if (self->openItems[0] != UINT_MAX) { struct UIMenuContents * contents; unsigned int itemIndex, depth; struct UIMenuItem_private * item = getMenuItemAtPosition(self, x, y, &contents, &itemIndex, &depth); if (item != NULL) { if (item->title.bytes != NULL && item->enabled) { contents->highlightIndex = itemIndex; } else { contents->highlightIndex = UINT_MAX; } if (depth + 1 < MENU_DEPTH_MAX && self->openItems[depth + 1] != itemIndex && (self->submenuToOpenAfterDelay != itemIndex || self->openingSubmenuDepth != depth)) { if (contents->items[itemIndex].contents.itemCount > 0 && item->enabled) { self->openItems[depth + 1] = UINT_MAX; self->openingSubmenuDepth = depth; self->submenuToOpenAfterDelay = itemIndex; self->submenuOpenTimerID = Shell_setTimer(SUBMENU_OPEN_DELAY, false, openSubmenuTimerCallback, self); contents->items[itemIndex].contents.highlightIndex = UINT_MAX; } else { Shell_cancelTimer(self->submenuOpenTimerID); self->openingSubmenuDepth = UINT_MAX; self->submenuToOpenAfterDelay = UINT_MAX; self->openItems[depth + 1] = UINT_MAX; } } return true; } if (y >= bounds.yMin && y <= bounds.yMax) { for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (x >= bounds.xMin + self->menus[menuIndex].offsetX && x <= bounds.xMin + self->menus[menuIndex].offsetX + self->menus[menuIndex].titleWidth) { self->openItems[0] = menuIndex; self->menus[menuIndex].contents.highlightIndex = UINT_MAX; self->openItems[1] = UINT_MAX; validateMenuContents(self, self->menus[menuIndex].identifier, &self->menus[menuIndex].contents); return true; } } } bool changed = false; if (self->contextMenuContents == NULL || self->contextMenuContents->itemCount == 0) { contents = &self->menus[self->openItems[0]].contents; if (contents->highlightIndex != UINT_MAX) { contents->highlightIndex = UINT_MAX; changed = true; } for (unsigned int depth = 1; depth < MENU_DEPTH_MAX && self->openItems[depth] != UINT_MAX; depth++) { contents = &contents->items[self->openItems[depth]].contents; if (contents->highlightIndex != UINT_MAX) { contents->highlightIndex = UINT_MAX; changed = true; } } } else if (self->contextMenuContents->highlightIndex != UINT_MAX) { self->contextMenuContents->highlightIndex = UINT_MAX; changed = true; } return changed; } return false; } bool UIMenuBar_mouseUp(UIMenuBar * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { if (buttonNumber == 0 && self->openItems[0] != UINT_MAX) { unsigned int depth; struct UIMenuItem_private * item = getMenuItemAtPosition(self, x, y, NULL, NULL, &depth); if (item != NULL) { if (item->title.bytes != NULL && item->enabled && item->contents.itemCount == 0) { self->pendingMenuIdentifier = self->menus[self->openItems[0]].identifier; self->pendingMenuItemIdentifier = item->identifier; self->pendingMenuModifiers = modifiers; self->pendingMenuReferenceTime = referenceTime; if (self->openItems[depth] == CONTEXT_MENU_OPEN_INDEX) { self->openItems[depth] = UINT_MAX; disposeMenuContents(self->contextMenuContents); } else { self->openItems[0] = UINT_MAX; } } return true; } if (self->contextMenuContents != NULL && self->contextMenuContents->itemCount > 0) { for (unsigned int depth = 0; depth < MENU_DEPTH_MAX && self->openItems[depth] != UINT_MAX; depth++) { if (self->openItems[depth] == CONTEXT_MENU_OPEN_INDEX) { self->openItems[depth] = UINT_MAX; disposeMenuContents(self->contextMenuContents); mouseMovedOrDragged(self, x, y); return true; } } } if (self->openTime == DBL_MAX || self->openTime + MOUSE_UP_CLOSE_INTERVAL < referenceTime) { UIMenuBar_close(self); return true; } } return false; } bool UIMenuBar_mouseMoved(UIMenuBar * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { return mouseMovedOrDragged(self, x, y); } bool UIMenuBar_mouseDragged(UIMenuBar * self, unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { return mouseMovedOrDragged(self, x, y); } UIEventResponse UIMenuBar_scrollWheel(UIMenuBar * self, float x, float y, int deltaX, int deltaY, unsigned int modifiers, bool isFinalTarget, double referenceTime) { // TODO: Scroll open menu return RESPONSE_UNHANDLED; } static bool handleMenuContentsKeyDown(UIMenuBar * self, unsigned int menuIndex, struct UIMenuContents * contents, unsigned int keyCode, unsigned int modifiers, double referenceTime); static bool handleMenuItemKeyDown(UIMenuBar * self, unsigned int menuIndex, struct UIMenuItem_private * item, struct UIMenuContents * parentContents, unsigned int keyCode, unsigned int modifiers, double referenceTime) { if (item->contents.itemCount > 0) { return handleMenuContentsKeyDown(self, menuIndex, &item->contents, keyCode, modifiers, referenceTime); } if (UIKeyShortcut_isMatch(item->shortcut, keyCode, modifiers)) { validateMenuItem(self, self->menus[menuIndex].identifier, item, parentContents); if (item->enabled) { bool animateWhenInvoked = item->animateWhenInvoked; // If actionCallback changes this menu's contents, item might no longer exist self->actionCallback(self, self->menus[menuIndex].identifier, item->identifier, modifiers, referenceTime, self->callbackContext); if (animateWhenInvoked) { self->actionAnimationEndTime = referenceTime + getAppearanceDouble(self->appearance, UIToolkit_actionAnimationDuration); self->actionAnimationMenuIndex = menuIndex; } return true; } } return false; } static bool handleMenuContentsKeyDown(UIMenuBar * self, unsigned int menuIndex, struct UIMenuContents * contents, unsigned int keyCode, unsigned int modifiers, double referenceTime) { for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { if (contents->items[itemIndex].title.bytes != NULL && handleMenuItemKeyDown(self, menuIndex, &contents->items[itemIndex], contents, keyCode, modifiers, referenceTime)) { return true; } } return false; } UIEventResponse UIMenuBar_keyDown(UIMenuBar * self, unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, bool isFinalTarget, double referenceTime) { if (self->openItems[0] != UINT_MAX) { // TODO: Full keyboard controls for menu navigation; escape should only close innermost submenu if (keyCode == KEY_CODE_ESCAPE) { if (self->contextMenuContents != NULL && self->contextMenuContents->itemCount > 0) { for (unsigned int depth = 0; depth < MENU_DEPTH_MAX && self->openItems[depth] != UINT_MAX; depth++) { if (self->openItems[depth] == CONTEXT_MENU_OPEN_INDEX) { self->openItems[depth] = UINT_MAX; disposeMenuContents(self->contextMenuContents); return RESPONSE_HANDLED; } } } UIMenuBar_close(self); } return RESPONSE_HANDLED; } for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (handleMenuContentsKeyDown(self, menuIndex, &self->menus[menuIndex].contents, keyCode, modifiers, referenceTime)) { return RESPONSE_HANDLED; } } return RESPONSE_UNHANDLED; } bool UIMenuBar_menuActionDown(UIMenuBar * self, unsigned int actionNumber, bool isRepeat, double referenceTime) { // TODO return false; } bool UIMenuBar_menuActionUp(UIMenuBar * self, unsigned int actionNumber, double referenceTime) { // TODO return false; } bool UIMenuBar_menuDirectionDown(UIMenuBar * self, UINavigationDirection direction, bool isRepeat, double referenceTime) { // TODO return false; } bool UIMenuBar_acceptsFocus(UIMenuBar * self) { return true; } Rect4f UIMenuBar_getBounds(UIMenuBar * self) { return UIElement_boundsRectWithOrigin(self->position, self->relativeOrigin, VECTOR2f(self->width, getAppearanceFloat(self->appearance, UIMenuBar_menuHeight))); } Rect4f UIMenuBar_getFocusBounds(UIMenuBar * self) { return call_super_virtual(getFocusBounds, self); } static void pendingMenuTimerCallback(ShellTimer timerID, void * context) { UIMenuBar * self = context; self->actionCallback(self, self->pendingMenuIdentifier, self->pendingMenuItemIdentifier, self->pendingMenuModifiers, self->pendingMenuReferenceTime, self->callbackContext); self->pendingMenuIdentifier = self->pendingMenuItemIdentifier = INT_MIN; self->pendingMenuTimerID = SHELL_TIMER_INVALID; Shell_redisplay(); } static void drawMenuContents(UIMenuBar * self, struct UIMenuContents * contents, unsigned int depth, Vector2f offset, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { Rect4f menuBounds; menuBounds.xMin = contents->topLeft.x + offset.x; menuBounds.xMax = menuBounds.xMin + contents->size.x; menuBounds.yMax = contents->topLeft.y + offset.y; menuBounds.yMin = menuBounds.yMax - contents->size.y; float dropShadowOutset = getAppearanceFloat(self->appearance, UIMenuBar_dropShadowOutset); Vector2f dropShadowOffset = getAppearanceVector2f(self->appearance, UIMenuBar_dropShadowOffset); Rect4f whiteEntry = getAppearanceAtlasEntry(self->appearance, UIToolkit_white).bounds; call_virtual(drawSlicedQuad3x3, drawingInterface, Rect4f_offset(Rect4f_inset(menuBounds, VECTOR2f(-dropShadowOutset, -dropShadowOutset)), dropShadowOffset), UIAtlasEntry_boundsForScale(getAppearanceAtlasEntry(self->appearance, UIMenuBar_menuDropShadow), drawingInterface->scaleFactor), getAppearanceSliceGrid3x3(self->appearance, UIMenuBar_dropShadowSlices), getAppearanceColor4f(self->appearance, UIMenuBar_dropShadowColor), vertexIO); call_virtual(drawQuad, drawingInterface, Rect4f_inset(menuBounds, VECTOR2f(-1, -1)), whiteEntry, getAppearanceColor4f(self->appearance, UIMenuBar_outlineColor), vertexIO); call_virtual(drawQuad, drawingInterface, menuBounds, whiteEntry, getAppearanceColor4f(self->appearance, UIMenuBar_backgroundColor), vertexIO); // TODO: Constrain to safe bounds and scroll UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, drawingInterface); float lineHeight = call_virtual(getLineHeight, typeface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); Color4f separatorColor = getAppearanceColor4f(self->appearance, UIMenuBar_separatorColor); Color4f highlightColor = getAppearanceColor4f(self->appearance, UIMenuBar_highlightColor); Color4f highlightTextColor = getAppearanceColor4f(self->appearance, UIMenuBar_highlightTextColor); Color4f disabledTextColor = getAppearanceColor4f(self->appearance, UIMenuBar_disabledTextColor); UIAtlasEntry checkmarkEntry = getAppearanceAtlasEntry(self->appearance, UIMenuBar_menuCheckmark); UIAtlasEntry submenuArrowEntry = getAppearanceAtlasEntry(self->appearance, UIMenuBar_menuSubmenuArrow); float itemHeight = lineHeight + itemPadding.y * 2; for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { if (contents->items[itemIndex].title.bytes == NULL) { call_virtual(drawQuad, drawingInterface, RECT4f(menuBounds.xMin, menuBounds.xMax, menuBounds.yMax + contents->items[itemIndex].offsetY - SEPARATOR_HEIGHT, menuBounds.yMax + contents->items[itemIndex].offsetY), whiteEntry, separatorColor, vertexIO); } else { Color4f textColor; if (!contents->items[itemIndex].enabled) { textColor = disabledTextColor; } else if (contents->highlightIndex == itemIndex || self->openItems[depth] == itemIndex) { call_virtual(drawQuad, drawingInterface, RECT4f(menuBounds.xMin, menuBounds.xMax, menuBounds.yMax + contents->items[itemIndex].offsetY - itemHeight, menuBounds.yMax + contents->items[itemIndex].offsetY), whiteEntry, highlightColor, vertexIO); textColor = highlightTextColor; } else { textColor = contents->items[itemIndex].textColor; } call_virtual(drawString, drawingInterface, typeface, contents->items[itemIndex].title, VECTOR2f(menuBounds.xMin + 1 + itemPadding.x * 2 + checkmarkEntry.size.x, menuBounds.yMax + contents->items[itemIndex].offsetY - itemHeight / 2), VECTOR2f(0.0f, 0.5f), 1.0f, textColor, vertexIO); if (contents->items[itemIndex].checked) { call_virtual(drawQuad, drawingInterface, UIElement_boundsRectWithOrigin(VECTOR2f(menuBounds.xMin + itemPadding.x + self->leftRightIconPadding / 2, menuBounds.yMax + contents->items[itemIndex].offsetY - itemHeight / 2), VECTOR2f(0.5f, 0.5f), checkmarkEntry.size), UIAtlasEntry_boundsForScale(checkmarkEntry, drawingInterface->scaleFactor), textColor, vertexIO); } else if (!Vector2f_isZero(contents->items[itemIndex].customIcon.size)) { call_virtual(drawQuad, drawingInterface, UIElement_boundsRectWithOrigin(VECTOR2f(menuBounds.xMin + itemPadding.x + self->leftRightIconPadding / 2, menuBounds.yMax + contents->items[itemIndex].offsetY - itemHeight / 2), VECTOR2f(0.5f, 0.5f), contents->items[itemIndex].customIcon.size), UIAtlasEntry_boundsForScale(contents->items[itemIndex].customIcon, drawingInterface->scaleFactor), textColor, vertexIO); } if (contents->items[itemIndex].contents.itemCount > 0) { call_virtual(drawQuad, drawingInterface, UIElement_boundsRectWithOrigin(VECTOR2f(menuBounds.xMax - itemPadding.x - self->leftRightIconPadding / 2, menuBounds.yMax + contents->items[itemIndex].offsetY - itemHeight / 2), VECTOR2f(0.5f, 0.5f), submenuArrowEntry.size), UIAtlasEntry_boundsForScale(submenuArrowEntry, drawingInterface->scaleFactor), textColor, vertexIO); } else if (contents->items[itemIndex].shortcutString.bytes != NULL) { call_virtual(drawString, drawingInterface, typeface, contents->items[itemIndex].shortcutString, VECTOR2f(menuBounds.xMax - itemPadding.x, menuBounds.yMax + contents->items[itemIndex].offsetY - itemHeight / 2), VECTOR2f(1.0f, 0.5f), 1.0f, textColor, vertexIO); } } } if (depth < MENU_DEPTH_MAX) { if (self->openItems[depth] == CONTEXT_MENU_OPEN_INDEX) { drawMenuContents(self, self->contextMenuContents, depth + 1, offset, drawingInterface, vertexIO); } else if (self->openItems[depth] < contents->itemCount) { drawMenuContents(self, &contents->items[self->openItems[depth]].contents, depth + 1, offset, drawingInterface, vertexIO); } } } void UIMenuBar_draw(UIMenuBar * self, Vector2f offset, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { Rect4f bounds = Rect4f_offset(call_virtual(getBounds, self), offset); Rect4f whiteEntry = getAppearanceAtlasEntry(self->appearance, UIToolkit_white).bounds; call_virtual(drawQuad, drawingInterface, bounds, whiteEntry, getAppearanceColor4f(self->appearance, UIMenuBar_outlineColor), vertexIO); call_virtual(drawQuad, drawingInterface, Rect4f_inset(bounds, VECTOR2f(1, 1)), whiteEntry, getAppearanceColor4f(self->appearance, UIMenuBar_backgroundColor), vertexIO); Color4f menuHighlightColor = getAppearanceColor4f(self->appearance, UIMenuBar_highlightColor); Color4f menuHighlightTextColor = getAppearanceColor4f(self->appearance, UIMenuBar_highlightTextColor); Color4f menuTextColor = getAppearanceColor4f(self->appearance, UIMenuBar_textColor); UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, drawingInterface); float menuPadding = getAppearanceFloat(self->appearance, UIMenuBar_menuPadding); for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { Color4f textColor; if (self->openItems[0] == menuIndex || (self->actionAnimationEndTime > vertexIO->referenceTime && menuIndex == self->actionAnimationMenuIndex)) { call_virtual(drawQuad, drawingInterface, RECT4f(bounds.xMin + self->menus[menuIndex].offsetX, bounds.xMin + self->menus[menuIndex].offsetX + self->menus[menuIndex].titleWidth, bounds.yMin, bounds.yMax), whiteEntry, menuHighlightColor, vertexIO); textColor = menuHighlightTextColor; } else { textColor = menuTextColor; } call_virtual(drawString, drawingInterface, typeface, self->menus[menuIndex].title, VECTOR2f(bounds.xMin + self->menus[menuIndex].offsetX + menuPadding, bounds.yMin + (bounds.yMax - bounds.yMin) / 2), VECTOR2f(0.0f, 0.5f), 1.0f, textColor, vertexIO); if (self->openItems[0] == menuIndex) { drawMenuContents(self, &self->menus[menuIndex].contents, 1, VECTOR2f(bounds.xMin, bounds.yMax), drawingInterface, vertexIO); } } self->dirty = self->actionAnimationEndTime > vertexIO->referenceTime; if (self->pendingMenuIdentifier != INT_MIN && self->pendingMenuItemIdentifier != INT_MIN && self->pendingMenuTimerID == SHELL_TIMER_INVALID) { self->pendingMenuTimerID = Shell_setTimer(0.0, false, pendingMenuTimerCallback, self); self->dirty = true; } } void UIMenuBar_listRenderables(UIMenuBar * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) { self->dirty = false; if (!self->visible) { return; } clipBounds = UIElement_intersectClipBounds(clipBounds, call_virtual(getAbsoluteClipBounds, self)); RenderableIO_addRenderable(renderableIO, self->renderable, drawOrderOffset + 1000, clipBounds); } bool UIMenuBar_isOpen(UIMenuBar * self) { return self->openItems[0] != UINT_MAX; } void UIMenuBar_close(UIMenuBar * self) { self->openItems[0] = UINT_MAX; self->pendingMenuIdentifier = self->pendingMenuItemIdentifier = INT_MIN; Shell_cancelTimer(self->pendingMenuTimerID); self->pendingMenuTimerID = SHELL_TIMER_INVALID; if (self->contextMenuContents != NULL) { disposeMenuContents(self->contextMenuContents); } } void UIMenuBar_playKeyShortcutAnimation(UIMenuBar * self, int menuIdentifier) { for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (self->menus[menuIndex].identifier == menuIdentifier) { self->actionAnimationEndTime = Shell_getCurrentTime() + getAppearanceDouble(self->appearance, UIToolkit_actionAnimationDuration); self->actionAnimationMenuIndex = menuIndex; break; } } } void UIMenuBar_addMenu(UIMenuBar * self, UIMenu * menu) { UIMenuBar_insertMenu(self, menu, self->menuCount); } void UIMenuBar_insertMenu(UIMenuBar * self, UIMenu * menu, unsigned int menuIndex) { if (menuIndex > self->menuCount) { menuIndex = self->menuCount; } self->menus = realloc(self->menus, (self->menuCount + 1) * sizeof(*self->menus)); for (unsigned int menuIndex2 = self->menuCount; menuIndex2 > menuIndex; menuIndex2--) { self->menus[menuIndex2] = self->menus[menuIndex2 - 1]; } self->menus[menuIndex].title = String_copy(menu->title); self->menus[menuIndex].identifier = menu->identifier; UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); self->menus[menuIndex].titleWidth = ceilf(call_virtual(measureString, typeface, menu->title) + getAppearanceFloat(self->appearance, UIMenuBar_menuPadding) * 2); self->menus[menuIndex].contents = copyMenuContents(self, menu->itemCount, menu->items); self->menuCount++; float offsetX = 0.0f; if (menuIndex > 0) { offsetX = self->menus[menuIndex - 1].offsetX + self->menus[menuIndex - 1].titleWidth; } float menuHeight = getAppearanceFloat(self->appearance, UIMenuBar_menuHeight); for (unsigned int menuIndex2 = menuIndex; menuIndex2 < self->menuCount; menuIndex2++) { self->menus[menuIndex2].offsetX = offsetX; assignSubmenuPositions(self, &self->menus[menuIndex2].contents, VECTOR2f(offsetX, -menuHeight)); offsetX += self->menus[menuIndex2].titleWidth; } } void UIMenuBar_removeMenu(UIMenuBar * self, int menuIdentifier) { float offsetX = 0.0f; float menuHeight = getAppearanceFloat(self->appearance, UIMenuBar_menuHeight); for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (self->menus[menuIndex].identifier == menuIdentifier) { String_free(self->menus[menuIndex].title); disposeMenuContents(&self->menus[menuIndex].contents); self->menuCount--; for (; menuIndex < self->menuCount; menuIndex++) { self->menus[menuIndex] = self->menus[menuIndex + 1]; self->menus[menuIndex].offsetX = offsetX; assignSubmenuPositions(self, &self->menus[menuIndex].contents, VECTOR2f(offsetX, -menuHeight)); offsetX += self->menus[menuIndex].titleWidth; } return; } offsetX += self->menus[menuIndex].titleWidth; } #ifdef DEBUG fprintf(stderr, "Warning: UIMenuBar_removeMenu() couldn't find menu with identifier %d\n", menuIdentifier); #endif } struct UIMenu_private * getMenu(UIMenuBar * self, int menuIdentifier) { for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (self->menus[menuIndex].identifier == menuIdentifier) { return &self->menus[menuIndex]; } } return NULL; } struct UIMenuItem_private * getMenuItemWithContents(UIMenuBar * self, struct UIMenuContents * contents, int menuItemIdentifier, struct UIMenuContents ** outParentContents) { for (unsigned int itemIndex = 0; itemIndex < contents->itemCount; itemIndex++) { if (contents->items[itemIndex].identifier == menuItemIdentifier) { if (outParentContents != NULL) { *outParentContents = contents; } return &contents->items[itemIndex]; } if (contents->items[itemIndex].contents.itemCount > 0) { struct UIMenuItem_private * item = getMenuItemWithContents(self, &contents->items[itemIndex].contents, menuItemIdentifier, outParentContents); if (item != NULL) { return item; } } } return NULL; } struct UIMenuItem_private * getMenuItem(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, struct UIMenuContents ** outParentContents) { struct UIMenu_private * menu = getMenu(self, menuIdentifier); if (menu != NULL) { return getMenuItemWithContents(self, &menu->contents, menuItemIdentifier, outParentContents); } return NULL; } void UIMenuBar_setMenuTitle(UIMenuBar * self, int menuIdentifier, String title) { float offsetX = 0.0f; UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float menuHeight = getAppearanceFloat(self->appearance, UIMenuBar_menuHeight); float menuPadding = getAppearanceFloat(self->appearance, UIMenuBar_menuPadding); for (unsigned int menuIndex = 0; menuIndex < self->menuCount; menuIndex++) { if (self->menus[menuIndex].identifier == menuIdentifier) { String_free(self->menus[menuIndex].title); self->menus[menuIndex].title = String_copy(title); self->menus[menuIndex].titleWidth = ceilf(call_virtual(measureString, typeface, self->menus[menuIndex].title) + menuPadding * 2); for (menuIndex++; menuIndex < self->menuCount; menuIndex++) { offsetX += self->menus[menuIndex - 1].titleWidth; self->menus[menuIndex].offsetX = offsetX; assignSubmenuPositions(self, &self->menus[menuIndex].contents, VECTOR2f(offsetX, -menuHeight)); } break; } offsetX += self->menus[menuIndex].titleWidth; } } static bool isMenuOpen(UIMenuBar * self, struct UIMenuContents * contents) { if (self->openItems[0] == UINT_MAX) { return false; } struct UIMenuContents * contentsAtDepth = &self->menus[self->openItems[0]].contents; if (contentsAtDepth == contents) { return true; } for (unsigned int depth = 1; depth < MENU_DEPTH_MAX && self->openItems[depth] != UINT_MAX; depth++) { if (self->openItems[depth] == CONTEXT_MENU_OPEN_INDEX) { contentsAtDepth = self->contextMenuContents; } else { contentsAtDepth = &contentsAtDepth->items[self->openItems[depth]].contents; } if (contentsAtDepth == contents) { return true; } } return false; } void UIMenuBar_setMenuContents(UIMenuBar * self, int menuIdentifier, unsigned int itemCount, UIMenuItem * items) { struct UIMenu_private * menu = getMenu(self, menuIdentifier); if (menu != NULL) { disposeMenuContents(&menu->contents); menu->contents = copyMenuContents(self, itemCount, items); assignSubmenuPositions(self, &menu->contents, VECTOR2f(menu->offsetX, -getAppearanceFloat(self->appearance, UIMenuBar_menuHeight))); if (isMenuOpen(self, &menu->contents)) { validateMenuContents(self, self->menus[self->openItems[0]].identifier, &menu->contents); } } } void UIMenuBar_setSubmenuContents(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, unsigned int itemCount, UIMenuItem * items) { struct UIMenuContents * contents; struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, &contents); if (item != NULL) { disposeMenuContents(&item->contents); item->contents = copyMenuContents(self, itemCount, items); assignSubmenuPositions(self, &item->contents, VECTOR2f(contents->topLeft.x + contents->size.x, contents->topLeft.y + item->offsetY)); if (isMenuOpen(self, &item->contents)) { validateMenuContents(self, self->menus[self->openItems[0]].identifier, &item->contents); } } } void UIMenuBar_setItemChecked(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, bool checked) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { item->checked = checked; } } bool UIMenuBar_getItemChecked(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { return item->checked; } return false; } void UIMenuBar_setItemEnabled(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, bool enabled) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { item->enabled = enabled; } } bool UIMenuBar_getItemEnabled(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { return item->enabled; } return false; } void UIMenuBar_setItemTitle(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, String title) { struct UIMenuContents * contents; struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, &contents); if (item != NULL) { String_free(item->title); item->title = String_copy(title); contents->layoutDirty = true; } } String UIMenuBar_getItemTitle(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { return item->title; } return STR_NULL; } void UIMenuBar_setItemShortcut(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, UIKeyShortcut * shortcut) { struct UIMenuContents * contents; struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, &contents); if (item != NULL) { free(item->shortcut); item->shortcut = UIKeyShortcut_copy(shortcut); String_free(item->shortcutString); item->shortcutString = createKeyShortcutString(item->shortcut); contents->layoutDirty = true; } } void UIMenuBar_setItemShortcutString(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, String shortcutString) { struct UIMenuContents * contents; struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, &contents); if (item != NULL) { String_free(item->shortcutString); item->shortcutString = String_copy(shortcutString); contents->layoutDirty = true; } } void UIMenuBar_setItemCustomIcon(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, UIAtlasEntry atlasEntry) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { item->customIcon = atlasEntry; } } void UIMenuBar_setItemTextColor(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, Color4f textColor) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { item->textColor = textColor; } } void UIMenuBar_setItemAnimatesMenuWhenInvoked(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier, bool animate) { struct UIMenuItem_private * item = getMenuItem(self, menuIdentifier, menuItemIdentifier, NULL); if (item != NULL) { item->animateWhenInvoked = animate; } } void UIMenuBar_addMenuItem(UIMenuBar * self, int menuIdentifier, UIMenuItem item) { UIMenuBar_insertMenuItem(self, menuIdentifier, UINT_MAX, item); } void UIMenuBar_insertMenuItem(UIMenuBar * self, int menuIdentifier, unsigned int itemIndex, UIMenuItem item) { struct UIMenu_private * menu = getMenu(self, menuIdentifier); if (menu == NULL) { #ifdef DEBUG fprintf(stderr, "Warning: UIMenuBar_insertMenuItem() couldn't find menu with identifier %d\n", menuIdentifier); #endif } else { UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float lineHeight = call_virtual(getLineHeight, typeface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); if (itemIndex > menu->contents.itemCount) { itemIndex = menu->contents.itemCount; } float newItemHeight; if (item.title.bytes == NULL) { newItemHeight = SEPARATOR_HEIGHT; } else { newItemHeight = lineHeight + itemPadding.y * 2; } float newItemOffsetY; if (itemIndex > 0) { if (itemIndex < menu->contents.itemCount) { newItemOffsetY = menu->contents.items[itemIndex].offsetY; } else { newItemOffsetY = -menu->contents.size.y; } } else { newItemOffsetY = 0.0f; } menu->contents.items = realloc(menu->contents.items, (menu->contents.itemCount + 1) * sizeof(*menu->contents.items)); for (unsigned int itemIndex2 = menu->contents.itemCount; itemIndex2 > itemIndex; itemIndex2--) { menu->contents.items[itemIndex2] = menu->contents.items[itemIndex2 - 1]; menu->contents.items[itemIndex2].offsetY -= newItemHeight; } menu->contents.items[itemIndex] = copyMenuItem(self, item); menu->contents.items[itemIndex].offsetY = newItemOffsetY; if (menu->contents.items[itemIndex].title.bytes != NULL && menu->contents.items[itemIndex].titleWidth > menu->contents.size.x) { menu->contents.size.x = menu->contents.items[itemIndex].titleWidth; } menu->contents.size.y += newItemHeight; menu->contents.layoutDirty = true; menu->contents.itemCount++; } } void UIMenuBar_removeMenuItem(UIMenuBar * self, int menuIdentifier, int menuItemIdentifier) { struct UIMenu_private * menu = getMenu(self, menuIdentifier); if (menu == NULL) { #ifdef DEBUG fprintf(stderr, "Warning: UIMenuBar_removeMenuItem() couldn't find menu with identifier %d\n", menuIdentifier); #endif } else { UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float lineHeight = call_virtual(getLineHeight, typeface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); for (unsigned int itemIndex = 0; itemIndex < menu->contents.itemCount; itemIndex++) { if (menu->contents.items[itemIndex].identifier == menuItemIdentifier) { float removedItemHeight; if (menu->contents.items[itemIndex].title.bytes == NULL) { removedItemHeight = SEPARATOR_HEIGHT; } else { removedItemHeight = lineHeight + itemPadding.y * 2; } disposeMenuItem(menu->contents.items[itemIndex]); menu->contents.itemCount--; for (; itemIndex < menu->contents.itemCount; itemIndex++) { menu->contents.items[itemIndex] = menu->contents.items[itemIndex + 1]; menu->contents.items[itemIndex].offsetY += removedItemHeight; } menu->contents.size.y -= removedItemHeight; return; } } #ifdef DEBUG fprintf(stderr, "Warning: UIMenuBar_removeMenuItem() couldn't find a menu item with identifier %d in menu %d\n", menuItemIdentifier, menuIdentifier); #endif } } void UIMenuBar_removeMenuItemAtIndex(UIMenuBar * self, int menuIdentifier, unsigned int itemIndex) { struct UIMenu_private * menu = getMenu(self, menuIdentifier); if (menu == NULL) { #ifdef DEBUG fprintf(stderr, "Warning: UIMenuBar_removeMenuItemAtIndex() couldn't find menu with identifier %d\n", menuIdentifier); #endif } else { float removedItemHeight; if (menu->contents.items[itemIndex].title.bytes == NULL) { removedItemHeight = SEPARATOR_HEIGHT; } else { UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float lineHeight = call_virtual(getLineHeight, typeface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); removedItemHeight = lineHeight + itemPadding.y * 2; } disposeMenuItem(menu->contents.items[itemIndex]); menu->contents.itemCount--; for (; itemIndex < menu->contents.itemCount; itemIndex++) { menu->contents.items[itemIndex] = menu->contents.items[itemIndex + 1]; menu->contents.items[itemIndex].offsetY += removedItemHeight; } menu->contents.size.y -= removedItemHeight; } } void UIMenuBar_addSubmenuItem(UIMenuBar * self, int menuIdentifier, int parentMenuItemIdentifier, UIMenuItem item) { UIMenuBar_insertSubmenuItem(self, menuIdentifier, parentMenuItemIdentifier, UINT_MAX, item); } void UIMenuBar_insertSubmenuItem(UIMenuBar * self, int menuIdentifier, int parentMenuItemIdentifier, unsigned int itemIndex, UIMenuItem item) { struct UIMenuItem_private * parentSubmenu = getMenuItem(self, menuIdentifier, parentMenuItemIdentifier, NULL); if (parentSubmenu == NULL) { #ifdef DEBUG fprintf(stderr, "Warning: UIMenuBar_insertSubmenuItem() couldn't find submenu with identifier %d in menu %d\n", parentMenuItemIdentifier, menuIdentifier); #endif } else { if (itemIndex > parentSubmenu->contents.itemCount) { itemIndex = parentSubmenu->contents.itemCount; } parentSubmenu->contents.items = realloc(parentSubmenu->contents.items, (parentSubmenu->contents.itemCount + 1) * sizeof(*parentSubmenu->contents.items)); for (unsigned int itemIndex2 = parentSubmenu->contents.itemCount; itemIndex2 > itemIndex; itemIndex2--) { parentSubmenu->contents.items[itemIndex2] = parentSubmenu->contents.items[itemIndex2 - 1]; } parentSubmenu->contents.items[itemIndex] = copyMenuItem(self, item); parentSubmenu->contents.items[itemIndex].offsetY = -parentSubmenu->contents.size.y; if (parentSubmenu->contents.items[itemIndex].title.bytes == NULL) { parentSubmenu->contents.size.y += SEPARATOR_HEIGHT; } else { if (parentSubmenu->contents.items[itemIndex].titleWidth > parentSubmenu->contents.size.x) { parentSubmenu->contents.size.x = parentSubmenu->contents.items[itemIndex].titleWidth; } UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); float lineHeight = call_virtual(getLineHeight, typeface); Vector2f itemPadding = getAppearanceVector2f(self->appearance, UIMenuBar_itemPadding); parentSubmenu->contents.size.y += lineHeight + itemPadding.y * 2; } parentSubmenu->contents.itemCount++; } }