diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 2d77b58..e4a7a6e 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1382,6 +1382,79 @@ static void pollKeyboard(AppContextT *ctx) { ascii = 0; } + // Read shift state (INT 16h, AH=12h — enhanced shift flags) + memset(&r, 0, sizeof(r)); + r.x.ax = 0x1200; + __dpmi_int(0x16, &r); + int32_t shiftFlags = r.x.ax & 0xFF; + bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift + + // Alt+Tab / Shift+Alt+Tab — cycle windows + // Alt+Tab: scancode=0xA5, ascii=0x00 + if (ascii == 0 && scancode == 0xA5) { + if (ctx->stack.count > 1) { + if (shiftHeld) { + // Shift+Alt+Tab — focus the bottom window, raise it + wmRaiseWindow(&ctx->stack, &ctx->dirty, 0); + wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); + } else { + // Alt+Tab — send top window to bottom, focus new top + WindowT *top = ctx->stack.windows[ctx->stack.count - 1]; + dirtyListAdd(&ctx->dirty, top->x, top->y, top->w, top->h); + + // Shift all windows up + for (int32_t i = ctx->stack.count - 1; i > 0; i--) { + ctx->stack.windows[i] = ctx->stack.windows[i - 1]; + } + + ctx->stack.windows[0] = top; + top->focused = false; + + // Focus the new top window + wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); + dirtyListAdd(&ctx->dirty, ctx->stack.windows[ctx->stack.count - 1]->x, + ctx->stack.windows[ctx->stack.count - 1]->y, + ctx->stack.windows[ctx->stack.count - 1]->w, + ctx->stack.windows[ctx->stack.count - 1]->h); + } + } + + continue; + } + + // Alt+F4 — close focused window + if (ascii == 0 && scancode == 0x6B) { + if (ctx->stack.focusedIdx >= 0) { + WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; + + if (win->onClose) { + win->onClose(win); + } + } + + continue; + } + + // F10 — activate menu bar + if (ascii == 0 && scancode == 0x44) { + if (ctx->stack.focusedIdx >= 0) { + WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; + + if (win->menuBar && win->menuBar->menuCount > 0) { + if (ctx->popup.active) { + // F10 again closes the menu + dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, + ctx->popup.popupW, ctx->popup.popupH); + ctx->popup.active = false; + } else { + dispatchAccelKey(ctx, win->menuBar->menus[0].accelKey); + } + } + } + + continue; + } + // Keyboard move/resize mode — intercept all keys if (ctx->kbMoveResize.mode != KbModeNoneE) { WindowT *kbWin = findWindowById(ctx, ctx->kbMoveResize.windowId); @@ -1790,6 +1863,93 @@ static void pollKeyboard(AppContextT *ctx) { continue; } + // Tab / Shift-Tab — cycle focus between widgets + // Tab: scancode=0x0F, ascii=0x09 + // Shift-Tab: scancode=0x0F, ascii=0x00 + if (scancode == 0x0F && (ascii == 0x09 || ascii == 0)) { + if (ctx->stack.focusedIdx >= 0) { + WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; + + if (win->widgetRoot) { + // Find currently focused widget + WidgetT *current = NULL; + WidgetT *fstack[64]; + int32_t ftop = 0; + fstack[ftop++] = win->widgetRoot; + + while (ftop > 0) { + WidgetT *w = fstack[--ftop]; + + if (w->focused && widgetIsFocusable(w->type)) { + // Don't tab out of the terminal — it swallows Tab + if (w->type == WidgetAnsiTermE) { + current = NULL; + break; + } + + current = w; + break; + } + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (c->visible && ftop < 64) { + fstack[ftop++] = c; + } + } + } + + // Terminal swallowed Tab — send to widget system instead + if (current == NULL) { + // Check if a terminal is focused + ftop = 0; + fstack[ftop++] = win->widgetRoot; + bool termFocused = false; + + while (ftop > 0) { + WidgetT *w = fstack[--ftop]; + + if (w->focused && w->type == WidgetAnsiTermE) { + termFocused = true; + break; + } + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (c->visible && ftop < 64) { + fstack[ftop++] = c; + } + } + } + + if (termFocused) { + // Terminal has focus — send Tab to it + if (win->onKey) { + win->onKey(win, ascii ? ascii : (scancode | 0x100), 0); + } + + continue; + } + } + + WidgetT *next; + + if (ascii == 0x09) { + next = widgetFindNextFocusable(win->widgetRoot, current); + } else { + next = widgetFindPrevFocusable(win->widgetRoot, current); + } + + if (next) { + sOpenPopup = NULL; + widgetClearFocus(win->widgetRoot); + next->focused = true; + wgtInvalidate(win->widgetRoot); + } + } + } + + continue; + } + // Send to focused window if (ctx->stack.focusedIdx >= 0) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 8fff078..ba6dace 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -270,6 +270,72 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) { } +// ============================================================ +// widgetFindPrevFocusable +// ============================================================ +// +// Depth-first walk collecting all visible+enabled focusable widgets, +// then returns the one before 'before'. Wraps around if needed. + +WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) { + WidgetT *list[128]; + int32_t count = 0; + + // Collect all focusable widgets via depth-first traversal + WidgetT *stack[64]; + int32_t top = 0; + stack[top++] = root; + + while (top > 0) { + WidgetT *w = stack[--top]; + + if (!w->visible || !w->enabled) { + continue; + } + + if (widgetIsFocusable(w->type) && count < 128) { + list[count++] = w; + } + + // Push children in reverse order so first child is processed first + WidgetT *children[64]; + int32_t childCount = 0; + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (childCount < 64) { + children[childCount++] = c; + } + } + + for (int32_t i = childCount - 1; i >= 0; i--) { + if (top < 64) { + stack[top++] = children[i]; + } + } + } + + if (count == 0) { + return NULL; + } + + // Find 'before' in the list + int32_t idx = -1; + + for (int32_t i = 0; i < count; i++) { + if (list[i] == before) { + idx = i; + break; + } + } + + if (idx <= 0) { + return list[count - 1]; // Wrap to last + } + + return list[idx - 1]; +} + + // ============================================================ // widgetFrameBorderWidth // ============================================================ diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 6d83347..4949ce2 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -63,6 +63,7 @@ void widgetDestroyChildren(WidgetT *w); void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); WidgetT *widgetFindByAccel(WidgetT *root, char key); WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); +WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); int32_t widgetFrameBorderWidth(const WidgetT *w); WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); bool widgetIsFocusable(WidgetTypeE type);