// 24 april 2015 #include "uipriv_windows.h" // TODO migrate to ptrArray static uiMenu **menus = NULL; static uintmax_t len = 0; static uintmax_t cap = 0; static BOOL menusFinalized = FALSE; static WORD curID = 100; // start somewhere safe static BOOL hasQuit = FALSE; static BOOL hasPreferences = FALSE; static BOOL hasAbout = FALSE; struct uiMenu { uiTyped t; WCHAR *name; uiMenuItem **items; uintmax_t len; uintmax_t cap; }; struct uiMenuItem { uiTyped t; WCHAR *name; int type; WORD id; void (*onClicked)(uiMenuItem *, uiWindow *, void *); void *onClickedData; BOOL disabled; // template for new instances; kept in sync with everything else BOOL checked; HMENU *hmenus; uintmax_t len; uintmax_t cap; }; enum { typeRegular, typeCheckbox, typeQuit, typePreferences, typeAbout, typeSeparator, }; #define grow 32 static void sync(uiMenuItem *item) { uintmax_t i; MENUITEMINFOW mi; ZeroMemory(&mi, sizeof (MENUITEMINFOW)); mi.cbSize = sizeof (MENUITEMINFOW); mi.fMask = MIIM_STATE; if (item->disabled) mi.fState |= MFS_DISABLED; if (item->checked) mi.fState |= MFS_CHECKED; for (i = 0; i < item->len; i++) if (SetMenuItemInfo(item->hmenus[i], item->id, FALSE, &mi) == 0) logLastError("error synchronizing menu items in windows/menu.c sync()"); } static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data) { // do nothing } static void onQuitClicked(uiMenuItem *item, uiWindow *w, void *data) { if (shouldQuit()) uiQuit(); } void uiMenuItemEnable(uiMenuItem *i) { i->disabled = FALSE; sync(i); } void uiMenuItemDisable(uiMenuItem *i) { i->disabled = TRUE; sync(i); } void uiMenuItemOnClicked(uiMenuItem *i, void (*f)(uiMenuItem *, uiWindow *, void *), void *data) { if (i->type == typeQuit) complain("attempt to call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead"); i->onClicked = f; i->onClickedData = data; } int uiMenuItemChecked(uiMenuItem *i) { return i->checked != FALSE; } void uiMenuItemSetChecked(uiMenuItem *i, int checked) { // use explicit values i->checked = FALSE; if (checked) i->checked = TRUE; sync(i); } static uiMenuItem *newItem(uiMenu *m, int type, const char *name) { uiMenuItem *item; if (menusFinalized) complain("attempt to create a new menu item after menus have been finalized"); if (m->len >= m->cap) { m->cap += grow; m->items = (uiMenuItem **) uiRealloc(m->items, m->cap * sizeof (uiMenuItem *), "uiMenuitem *[]"); } item = uiNew(uiMenuItem); uiTyped(item)->Type = uiMenuItemType(); m->items[m->len] = item; m->len++; item->type = type; switch (item->type) { case typeQuit: item->name = toUTF16("Quit"); break; case typePreferences: item->name = toUTF16("Preferences..."); break; case typeAbout: item->name = toUTF16("About"); break; case typeSeparator: break; default: item->name = toUTF16(name); break; } if (item->type != typeSeparator) { item->id = curID; curID++; } // TODO copy this from the unix one item->onClicked = defaultOnClicked; if (item->type == typeQuit) item->onClicked = onQuitClicked; return item; } uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name) { return newItem(m, typeRegular, name); } uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name) { return newItem(m, typeCheckbox, name); } uiMenuItem *uiMenuAppendQuitItem(uiMenu *m) { if (hasQuit) complain("attempt to add multiple Quit menu items"); hasQuit = TRUE; newItem(m, typeSeparator, NULL); return newItem(m, typeQuit, NULL); } uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m) { if (hasPreferences) complain("attempt to add multiple Preferences menu items"); hasPreferences = TRUE; newItem(m, typeSeparator, NULL); return newItem(m, typePreferences, NULL); } uiMenuItem *uiMenuAppendAboutItem(uiMenu *m) { if (hasAbout) complain("attempt to add multiple About menu items"); hasAbout = TRUE; newItem(m, typeSeparator, NULL); return newItem(m, typeAbout, NULL); } void uiMenuAppendSeparator(uiMenu *m) { newItem(m, typeSeparator, NULL); } uiMenu *uiNewMenu(const char *name) { uiMenu *m; if (menusFinalized) complain("attempt to create a new menu after menus have been finalized"); if (len >= cap) { cap += grow; menus = (uiMenu **) uiRealloc(menus, cap * sizeof (uiMenu *), "uiMenu *[]"); } m = uiNew(uiMenu); uiTyped(m)->Type = uiMenuType(); menus[len] = m; len++; m->name = toUTF16(name); return m; } static void appendMenuItem(HMENU menu, uiMenuItem *item) { UINT uFlags; uFlags = MF_SEPARATOR; if (item->type != typeSeparator) { uFlags = MF_STRING; if (item->disabled) uFlags |= MF_DISABLED | MF_GRAYED; if (item->checked) uFlags |= MF_CHECKED; } if (AppendMenuW(menu, uFlags, item->id, item->name) == 0) logLastError("error appending menu item in appendMenuItem()"); if (item->len >= item->cap) { item->cap += grow; item->hmenus = (HMENU *) uiRealloc(item->hmenus, item->cap * sizeof (HMENU), "HMENU[]"); } item->hmenus[item->len] = menu; item->len++; } static HMENU makeMenu(uiMenu *m) { HMENU menu; uintmax_t i; menu = CreatePopupMenu(); if (menu == NULL) logLastError("error creating menu in makeMenu()"); for (i = 0; i < m->len; i++) appendMenuItem(menu, m->items[i]); return menu; } HMENU makeMenubar(void) { HMENU menubar; HMENU menu; uintmax_t i; menusFinalized = TRUE; menubar = CreateMenu(); if (menubar == NULL) logLastError("error creating menubar in makeMenubar()"); for (i = 0; i < len; i++) { menu = makeMenu(menus[i]); if (AppendMenuW(menubar, MF_POPUP | MF_STRING, (UINT_PTR) menu, menus[i]->name) == 0) logLastError("error appending menu to menubar in makeMenubar()"); } return menubar; } void runMenuEvent(WORD id, uiWindow *w) { uiMenu *m; uiMenuItem *item; uintmax_t i, j; // this isn't optimal, but it works, and it should be just fine for most cases for (i = 0; i < len; i++) { m = menus[i]; for (j = 0; j < m->len; j++) { item = m->items[j]; if (item->id == id) goto found; } } // no match complain("unknown menu ID %hu in runMenuEvent()", id); found: // first toggle checkboxes, if any if (item->type == typeCheckbox) uiMenuItemSetChecked(item, !uiMenuItemChecked(item)); // then run the event (*(item->onClicked))(item, w, item->onClickedData); } static void freeMenu(uiMenu *m, HMENU submenu) { uintmax_t i; uiMenuItem *item; uintmax_t j; for (i = 0; i < m->len; i++) { item = m->items[i]; for (j = 0; j < item->len; j++) if (item->hmenus[j] == submenu) break; if (j >= item->len) complain("submenu handle %p not found in freeMenu()", submenu); for (; j < item->len - 1; j++) item->hmenus[j] = item->hmenus[j + 1]; item->hmenus[j] = NULL; item->len--; } } void freeMenubar(HMENU menubar) { uintmax_t i; MENUITEMINFOW mi; for (i = 0; i < len; i++) { ZeroMemory(&mi, sizeof (MENUITEMINFOW)); mi.cbSize = sizeof (MENUITEMINFOW); mi.fMask = MIIM_SUBMENU; if (GetMenuItemInfoW(menubar, i, TRUE, &mi) == 0) logLastError("error getting menu to delete item references from in freeMenubar()"); freeMenu(menus[i], mi.hSubMenu); } // no need to worry about destroying any menus; destruction of the window they're in will do it for us } void uninitMenus(void) { uiMenu *m; uiMenuItem *item; uintmax_t i, j; for (i = 0; i < len; i++) { m = menus[i]; uiFree(m->name); for (j = 0; j < m->len; j++) { item = m->items[j]; if (item->len != 0) complain("menu item %p (%ws) still has uiWindows attached; did you forget to destroy some windows?", item, item->name); if (item->name != NULL) uiFree(item->name); if (item->hmenus != NULL) uiFree(item->hmenus); uiFree(item); } if (m->items != NULL) uiFree(m->items); uiFree(m); } if (menus != NULL) uiFree(menus); }