1640 lines
47 KiB
C
1640 lines
47 KiB
C
// dvxhelp.c -- DVX Help Viewer application
|
|
//
|
|
// A callback-only DXE app that displays .hlp help files with a TreeView
|
|
// TOC sidebar and scrollable content area. Supports headings, text,
|
|
// links, images, lists, horizontal rules, notes, and code blocks.
|
|
//
|
|
// The content area is rebuilt per-topic by reading records from the .hlp
|
|
// file and creating private widget types (HelpText, HelpHeading, HelpLink)
|
|
// registered via wgtRegisterClass at startup.
|
|
//
|
|
// Navigation history supports back/forward with a fixed-size circular stack.
|
|
|
|
#include "hlpformat.h"
|
|
#include "dvxApp.h"
|
|
#include "dvxDlg.h"
|
|
#include "dvxWgt.h"
|
|
#include "dvxWgtP.h"
|
|
#include "dvxWm.h"
|
|
#include "dvxDraw.h"
|
|
#include "dvxVideo.h"
|
|
#include "dvxPlat.h"
|
|
#include "shellApp.h"
|
|
#include "box/box.h"
|
|
#include "button/button.h"
|
|
#include "splitter/splitter.h"
|
|
#include "treeView/treeView.h"
|
|
#include "scrollPane/scrlPane.h"
|
|
#include "toolbar/toolbar.h"
|
|
#include "image/image.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "dvxMem.h"
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define HELP_WIN_W 560
|
|
#define HELP_WIN_H 400
|
|
#define HELP_CASCADE_OFFSET 16
|
|
#define HELP_SPLITTER_POS 160
|
|
#define HELP_TOC_MIN_W 100
|
|
#define HELP_CONTENT_PAD 6
|
|
#define HELP_PARA_SPACING 12
|
|
#define HELP_HEADING1_TOP 24
|
|
#define HELP_HEADING1_PAD 6
|
|
#define HELP_HEADING2_TOP 16
|
|
#define HELP_HEADING2_PAD 4
|
|
#define HELP_HEADING3_TOP 8
|
|
#define HELP_LINK_PAD_X 2
|
|
#define HELP_LINK_PAD_Y 1
|
|
#define HELP_NOTE_PAD 6
|
|
#define HELP_NOTE_PREFIX_W 4
|
|
#define HELP_CODE_PAD 4
|
|
|
|
#define MAX_HISTORY 64
|
|
#define MAX_SEARCH_BUF 128
|
|
|
|
#define CMD_BACK 100
|
|
#define CMD_FORWARD 101
|
|
#define CMD_INDEX 102
|
|
#define CMD_SEARCH 103
|
|
#define CMD_EXIT 104
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static DxeAppContextT *sCtx = NULL;
|
|
static AppContextT *sAc = NULL;
|
|
static WindowT *sWin = NULL;
|
|
static WidgetT *sTocTree = NULL;
|
|
static WidgetT *sContentScroll = NULL;
|
|
static WidgetT *sContentBox = NULL;
|
|
static WidgetT *sBtnBack = NULL;
|
|
static WidgetT *sBtnForward = NULL;
|
|
|
|
// .hlp file data
|
|
static FILE *sHlpFile = NULL;
|
|
static HlpHeaderT sHeader;
|
|
static HlpTopicDirT *sTopicDir = NULL;
|
|
static char *sStringTable = NULL;
|
|
static HlpTocEntryT *sTocEntries = NULL;
|
|
static HlpIndexEntryT *sIndexEntries = NULL;
|
|
|
|
// Navigation history
|
|
static int32_t sHistory[MAX_HISTORY];
|
|
static int32_t sHistoryPos = -1;
|
|
static int32_t sHistoryCount = 0;
|
|
static int32_t sCurrentTopic = -1;
|
|
|
|
// Viewport wrap width — the width text should wrap at, derived from the
|
|
// ScrollPane's inner area. Updated when the window is first laid out and
|
|
// on resize. Wrapping widgets use this instead of w->parent->w so that
|
|
// wide tables/code don't inflate the wrap width.
|
|
static int32_t sWrapWidth = 0;
|
|
|
|
// Custom widget type IDs
|
|
static int32_t sHelpTextTypeId = -1;
|
|
static int32_t sHelpHeadingTypeId = -1;
|
|
static int32_t sHelpLinkTypeId = -1;
|
|
static int32_t sHelpNoteTypeId = -1;
|
|
static int32_t sHelpCodeTypeId = -1;
|
|
static int32_t sHelpRuleTypeId = -1;
|
|
static int32_t sHelpListItemTypeId = -1;
|
|
|
|
// ============================================================
|
|
// Private widget data types
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
char *text; // original flowing text (no newlines)
|
|
char *wrapped; // cached wrapped text (with \n), NULL = needs wrap
|
|
int32_t lineCount; // line count of wrapped text
|
|
int32_t wrapWidth; // pixel width text was wrapped at (-1 = dirty)
|
|
} HelpTextDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
int32_t level;
|
|
} HelpHeadingDataT;
|
|
|
|
typedef struct {
|
|
char *displayText;
|
|
char *targetTopicId;
|
|
} HelpLinkDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
char *wrapped;
|
|
int32_t lineCount;
|
|
int32_t wrapWidth;
|
|
uint8_t noteType;
|
|
} HelpNoteDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
int32_t lineCount;
|
|
} HelpCodeDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
char *wrapped;
|
|
int32_t lineCount;
|
|
int32_t wrapWidth;
|
|
} HelpListItemDataT;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx);
|
|
static void buildContentWidgets(void);
|
|
static void closeHelpFile(void);
|
|
static int32_t countLines(const char *text);
|
|
static void displayRecord(const HlpRecordHdrT *hdr, const char *payload);
|
|
static int32_t findTopicByIdStr(const char *topicId);
|
|
static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpCodeDestroy(WidgetT *w);
|
|
static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpHeadingDestroy(WidgetT *w);
|
|
static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpLinkDestroy(WidgetT *w);
|
|
static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
|
static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void helpListItemCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpListItemDestroy(WidgetT *w);
|
|
static void helpListItemPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpNoteDestroy(WidgetT *w);
|
|
static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void helpTextDestroy(WidgetT *w);
|
|
static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void historyPush(int32_t topicIdx);
|
|
static const char *hlpString(uint32_t offset);
|
|
static void navigateBack(void);
|
|
static void navigateForward(void);
|
|
static void navigateToTopic(int32_t topicIdx);
|
|
static void onClose(WindowT *win);
|
|
static void onIndex(WidgetT *w);
|
|
static void onMenu(WindowT *win, int32_t menuId);
|
|
static void onNavBack(WidgetT *w);
|
|
static void onNavForward(WidgetT *w);
|
|
static void onSearch(WidgetT *w);
|
|
static void onTocChange(WidgetT *w);
|
|
static bool openHelpFile(const char *path);
|
|
static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg);
|
|
static void populateToc(void);
|
|
static void registerWidgetClasses(void);
|
|
static void updateNavButtons(void);
|
|
|
|
// ============================================================
|
|
// App descriptor
|
|
// ============================================================
|
|
|
|
AppDescriptorT appDescriptor = {
|
|
.name = "DVX Help",
|
|
.hasMainLoop = false,
|
|
.multiInstance = true,
|
|
.stackSize = SHELL_STACK_DEFAULT,
|
|
.priority = TS_PRIORITY_NORMAL
|
|
};
|
|
|
|
// ============================================================
|
|
// Utility helpers
|
|
// ============================================================
|
|
|
|
// Word-wrap flowing text to fit within colWidth characters.
|
|
// Returns a dvxMalloc'd string with \n at wrap points.
|
|
// Caller must dvxFree the result.
|
|
static char *wrapText(const char *text, int32_t colWidth) {
|
|
if (!text || !text[0] || colWidth < 10) {
|
|
return dvxStrdup(text ? text : "");
|
|
}
|
|
|
|
int32_t textLen = (int32_t)strlen(text);
|
|
char *result = (char *)dvxMalloc(textLen + textLen / colWidth + 16);
|
|
|
|
if (!result) {
|
|
return dvxStrdup(text);
|
|
}
|
|
|
|
int32_t outPos = 0;
|
|
int32_t col = 0;
|
|
const char *p = text;
|
|
|
|
while (*p) {
|
|
// Skip leading whitespace at start of line
|
|
if (col == 0) {
|
|
while (*p == ' ') {
|
|
p++;
|
|
}
|
|
}
|
|
|
|
if (!*p) {
|
|
break;
|
|
}
|
|
|
|
// Find end of word
|
|
const char *wordStart = p;
|
|
|
|
while (*p && *p != ' ' && *p != '\n') {
|
|
p++;
|
|
}
|
|
|
|
int32_t wordLen = (int32_t)(p - wordStart);
|
|
|
|
// Check if word fits on current line
|
|
if (col > 0 && col + 1 + wordLen > colWidth) {
|
|
result[outPos++] = '\n';
|
|
col = 0;
|
|
} else if (col > 0) {
|
|
result[outPos++] = ' ';
|
|
col++;
|
|
}
|
|
|
|
memcpy(result + outPos, wordStart, wordLen);
|
|
outPos += wordLen;
|
|
col += wordLen;
|
|
|
|
// Skip spaces between words
|
|
while (*p == ' ') {
|
|
p++;
|
|
}
|
|
|
|
// Explicit newline in source = paragraph break
|
|
if (*p == '\n') {
|
|
result[outPos++] = '\n';
|
|
col = 0;
|
|
p++;
|
|
}
|
|
}
|
|
|
|
result[outPos] = '\0';
|
|
return result;
|
|
}
|
|
|
|
|
|
// Ensure a HelpText-style widget's wrapped text is up to date for the given pixel width.
|
|
// Returns the wrapped text and updates lineCount. Uses cached result if width unchanged.
|
|
// Wrap text at the given pixel width. Caches the result — only re-wraps
|
|
// when pixelW changes. Returns the wrapped string.
|
|
static const char *doWrap(char **wrappedPtr, int32_t *wrapWidthPtr, int32_t *lineCountPtr, const char *text, int32_t pixelW, int32_t charW, int32_t padPixels) {
|
|
int32_t cols = (pixelW - padPixels) / charW;
|
|
|
|
if (cols < 10) {
|
|
cols = 10;
|
|
}
|
|
|
|
// Return cached result if width hasn't changed
|
|
if (*wrappedPtr && *wrapWidthPtr == pixelW) {
|
|
return *wrappedPtr;
|
|
}
|
|
|
|
dvxFree(*wrappedPtr);
|
|
*wrappedPtr = wrapText(text, cols);
|
|
*wrapWidthPtr = pixelW;
|
|
*lineCountPtr = countLines(*wrappedPtr);
|
|
return *wrappedPtr;
|
|
}
|
|
|
|
|
|
static int32_t maxLineWidth(const BitmapFontT *font, const char *text) {
|
|
int32_t maxW = 0;
|
|
int32_t lineLen = 0;
|
|
|
|
for (const char *p = text; ; p++) {
|
|
if (*p == '\n' || *p == '\0') {
|
|
int32_t w = lineLen * font->charWidth;
|
|
|
|
if (w > maxW) {
|
|
maxW = w;
|
|
}
|
|
|
|
lineLen = 0;
|
|
|
|
if (*p == '\0') {
|
|
break;
|
|
}
|
|
} else {
|
|
lineLen++;
|
|
}
|
|
}
|
|
|
|
return maxW;
|
|
}
|
|
|
|
|
|
|
|
|
|
static int32_t countLines(const char *text) {
|
|
int32_t count = 1;
|
|
|
|
for (const char *p = text; *p; p++) {
|
|
if (*p == '\n') {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
|
|
|
|
static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg) {
|
|
const char *line = text;
|
|
|
|
for (int32_t i = 0; i < lineCount; i++) {
|
|
const char *end = strchr(line, '\n');
|
|
int32_t len = end ? (int32_t)(end - line) : (int32_t)strlen(line);
|
|
|
|
if (len > 0) {
|
|
drawTextN(d, ops, font, x, y, line, len, fg, 0, false);
|
|
}
|
|
|
|
y += font->charHeight;
|
|
line = end ? end + 1 : line + len;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpText widget class
|
|
// ============================================================
|
|
|
|
|
|
// Update sWrapWidth from the ScrollPane's current size.
|
|
static void updateWrapWidth(void) {
|
|
// 38 = ScrollPane border (4) + VBox padding (12) + scrollbar (14) + 1 char buffer (8)
|
|
if (sContentScroll && sContentScroll->w > 38) {
|
|
sWrapWidth = sContentScroll->w - 38;
|
|
}
|
|
}
|
|
|
|
|
|
static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpTextDataT *td = (HelpTextDataT *)w->data;
|
|
updateWrapWidth();
|
|
|
|
if (sWrapWidth > 0) {
|
|
doWrap(&td->wrapped, &td->wrapWidth, &td->lineCount, td->text, sWrapWidth, font->charWidth, 0);
|
|
}
|
|
|
|
w->calcMinW = 0;
|
|
w->calcMinH = td->lineCount * font->charHeight;
|
|
}
|
|
|
|
|
|
static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpTextDataT *td = (HelpTextDataT *)w->data;
|
|
const char *text = td->wrapped ? td->wrapped : td->text;
|
|
paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, text, td->lineCount, colors->contentFg);
|
|
}
|
|
|
|
|
|
static void helpTextDestroy(WidgetT *w) {
|
|
HelpTextDataT *td = (HelpTextDataT *)w->data;
|
|
|
|
if (td) {
|
|
dvxFree(td->wrapped);
|
|
dvxFree(td->text);
|
|
dvxFree(td);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpHeading widget class
|
|
// ============================================================
|
|
|
|
static bool isFirstChild(WidgetT *w) {
|
|
return w->parent && w->parent->firstChild == w;
|
|
}
|
|
|
|
|
|
static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data;
|
|
int32_t textH = font->charHeight;
|
|
bool first = isFirstChild(w);
|
|
|
|
w->calcMinW = 0;
|
|
|
|
if (hd->level == 1) {
|
|
int32_t top = first ? 0 : HELP_HEADING1_TOP;
|
|
w->calcMinH = top + textH + HELP_HEADING1_PAD * 2;
|
|
} else if (hd->level == 2) {
|
|
int32_t top = first ? 0 : HELP_HEADING2_TOP;
|
|
w->calcMinH = top + textH + HELP_HEADING2_PAD + 2;
|
|
} else {
|
|
int32_t top = first ? 0 : HELP_HEADING3_TOP;
|
|
w->calcMinH = top + textH;
|
|
}
|
|
}
|
|
|
|
|
|
static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data;
|
|
int32_t textLen = (int32_t)strlen(hd->text);
|
|
bool first = isFirstChild(w);
|
|
|
|
if (hd->level == 1) {
|
|
int32_t top = first ? 0 : HELP_HEADING1_TOP;
|
|
int32_t barY = w->y + top;
|
|
int32_t barH = w->h - top;
|
|
rectFill(d, ops, w->x, barY, w->w, barH, colors->activeTitleBg);
|
|
int32_t textY = barY + HELP_HEADING1_PAD;
|
|
drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->activeTitleFg, 0, false);
|
|
} else if (hd->level == 2) {
|
|
int32_t top = first ? 0 : HELP_HEADING2_TOP;
|
|
int32_t textY = w->y + top;
|
|
drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false);
|
|
int32_t lineY = textY + font->charHeight + 1;
|
|
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow);
|
|
} else {
|
|
int32_t top = first ? 0 : HELP_HEADING3_TOP;
|
|
int32_t textY = w->y + top;
|
|
drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false);
|
|
}
|
|
}
|
|
|
|
|
|
static void helpHeadingDestroy(WidgetT *w) {
|
|
HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data;
|
|
|
|
if (hd) {
|
|
dvxFree(hd->text);
|
|
dvxFree(hd);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpLink widget class
|
|
// ============================================================
|
|
|
|
static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
w->calcMinW = 0;
|
|
w->calcMinH = font->charHeight + HELP_LINK_PAD_Y * 2;
|
|
}
|
|
|
|
|
|
static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpLinkDataT *ld = (HelpLinkDataT *)w->data;
|
|
int32_t textLen = (int32_t)strlen(ld->displayText);
|
|
int32_t x = w->x + HELP_CONTENT_PAD + HELP_LINK_PAD_X;
|
|
int32_t y = w->y + HELP_LINK_PAD_Y;
|
|
|
|
// Use active title color as link color
|
|
uint32_t linkColor = colors->activeTitleBg;
|
|
|
|
drawTextN(d, ops, font, x, y, ld->displayText, textLen, linkColor, 0, false);
|
|
|
|
// Underline
|
|
int32_t lineY = y + font->charHeight;
|
|
int32_t lineW = textLen * font->charWidth;
|
|
drawHLine(d, ops, x, lineY, lineW, linkColor);
|
|
}
|
|
|
|
|
|
static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|
(void)root;
|
|
(void)vx;
|
|
(void)vy;
|
|
|
|
HelpLinkDataT *ld = (HelpLinkDataT *)w->data;
|
|
|
|
if (!ld || !ld->targetTopicId) {
|
|
return;
|
|
}
|
|
|
|
int32_t topicIdx = findTopicByIdStr(ld->targetTopicId);
|
|
|
|
if (topicIdx >= 0) {
|
|
navigateToTopic(topicIdx);
|
|
}
|
|
}
|
|
|
|
|
|
static void helpLinkDestroy(WidgetT *w) {
|
|
HelpLinkDataT *ld = (HelpLinkDataT *)w->data;
|
|
|
|
if (ld) {
|
|
dvxFree(ld->displayText);
|
|
dvxFree(ld->targetTopicId);
|
|
dvxFree(ld);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpNote widget class
|
|
// ============================================================
|
|
|
|
static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpNoteDataT *nd = (HelpNoteDataT *)w->data;
|
|
updateWrapWidth();
|
|
|
|
if (sWrapWidth > 0) {
|
|
int32_t notePad = HELP_NOTE_PAD * 2 + HELP_NOTE_PREFIX_W;
|
|
doWrap(&nd->wrapped, &nd->wrapWidth, &nd->lineCount, nd->text, sWrapWidth, font->charWidth, notePad);
|
|
}
|
|
|
|
w->calcMinW = 0;
|
|
w->calcMinH = nd->lineCount * font->charHeight + HELP_NOTE_PAD * 2;
|
|
}
|
|
|
|
|
|
static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpNoteDataT *nd = (HelpNoteDataT *)w->data;
|
|
|
|
uint32_t barColor;
|
|
|
|
if (nd->noteType == HLP_NOTE_WARNING) {
|
|
barColor = colors->activeTitleBg;
|
|
} else if (nd->noteType == HLP_NOTE_TIP) {
|
|
barColor = colors->menuHighlightBg;
|
|
} else {
|
|
barColor = colors->windowShadow;
|
|
}
|
|
|
|
int32_t boxX = w->x + HELP_CONTENT_PAD;
|
|
int32_t boxW = w->w - HELP_CONTENT_PAD * 2;
|
|
|
|
rectFill(d, ops, boxX, w->y, boxW, w->h, colors->windowFace);
|
|
rectFill(d, ops, boxX, w->y, HELP_NOTE_PREFIX_W, w->h, barColor);
|
|
|
|
int32_t textX = boxX + HELP_NOTE_PREFIX_W + HELP_NOTE_PAD;
|
|
const char *text = nd->wrapped ? nd->wrapped : nd->text;
|
|
paintLines(d, ops, font, textX, w->y + HELP_NOTE_PAD, text, nd->lineCount, colors->contentFg);
|
|
}
|
|
|
|
|
|
static void helpNoteDestroy(WidgetT *w) {
|
|
HelpNoteDataT *nd = (HelpNoteDataT *)w->data;
|
|
|
|
if (nd) {
|
|
dvxFree(nd->wrapped);
|
|
dvxFree(nd->text);
|
|
dvxFree(nd);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpCode widget class
|
|
// ============================================================
|
|
|
|
static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpCodeDataT *cd = (HelpCodeDataT *)w->data;
|
|
w->calcMinW = maxLineWidth(font, cd->text) + HELP_CONTENT_PAD * 2 + HELP_CODE_PAD * 2;
|
|
w->calcMinH = cd->lineCount * font->charHeight + HELP_CODE_PAD * 2;
|
|
}
|
|
|
|
|
|
static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpCodeDataT *cd = (HelpCodeDataT *)w->data;
|
|
|
|
int32_t boxX = w->x + HELP_CONTENT_PAD;
|
|
int32_t boxW = w->w - HELP_CONTENT_PAD * 2;
|
|
|
|
// Sunken background
|
|
rectFill(d, ops, boxX, w->y, boxW, w->h, colors->contentBg);
|
|
drawHLine(d, ops, boxX, w->y, boxW, colors->windowShadow);
|
|
drawHLine(d, ops, boxX, w->y + w->h - 1, boxW, colors->windowHighlight);
|
|
|
|
paintLines(d, ops, font, boxX + HELP_CODE_PAD, w->y + HELP_CODE_PAD, cd->text, cd->lineCount, colors->contentFg);
|
|
}
|
|
|
|
|
|
static void helpCodeDestroy(WidgetT *w) {
|
|
HelpCodeDataT *cd = (HelpCodeDataT *)w->data;
|
|
|
|
if (cd) {
|
|
dvxFree(cd->text);
|
|
dvxFree(cd);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpRule widget class
|
|
// ============================================================
|
|
|
|
static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
(void)font;
|
|
w->calcMinW = 0;
|
|
w->calcMinH = 5;
|
|
}
|
|
|
|
|
|
static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
(void)font;
|
|
int32_t lineY = w->y + 2;
|
|
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow);
|
|
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY + 1, w->w - HELP_CONTENT_PAD * 2, colors->windowHighlight);
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpListItem widget class
|
|
// ============================================================
|
|
|
|
static void helpListItemCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)w->data;
|
|
updateWrapWidth();
|
|
|
|
if (sWrapWidth > 0) {
|
|
doWrap(&ld->wrapped, &ld->wrapWidth, &ld->lineCount, ld->text, sWrapWidth, font->charWidth, 0);
|
|
}
|
|
|
|
w->calcMinW = 0;
|
|
w->calcMinH = ld->lineCount * font->charHeight;
|
|
}
|
|
|
|
|
|
static void helpListItemPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)w->data;
|
|
const char *text = ld->wrapped ? ld->wrapped : ld->text;
|
|
paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, text, ld->lineCount, colors->contentFg);
|
|
}
|
|
|
|
|
|
static void helpListItemDestroy(WidgetT *w) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)w->data;
|
|
|
|
if (ld) {
|
|
dvxFree(ld->wrapped);
|
|
dvxFree(ld->text);
|
|
dvxFree(ld);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Widget class registration
|
|
// ============================================================
|
|
|
|
static const WidgetClassT sClassHelpText = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpTextPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpTextCalcMinSize,
|
|
[WGT_METHOD_DESTROY] = (void *)helpTextDestroy,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassHelpHeading = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpHeadingPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpHeadingCalcMinSize,
|
|
[WGT_METHOD_DESTROY] = (void *)helpHeadingDestroy,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassHelpLink = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpLinkPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpLinkCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)helpLinkOnMouse,
|
|
[WGT_METHOD_DESTROY] = (void *)helpLinkDestroy,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassHelpNote = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpNotePaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpNoteCalcMinSize,
|
|
[WGT_METHOD_DESTROY] = (void *)helpNoteDestroy,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassHelpCode = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpCodePaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpCodeCalcMinSize,
|
|
[WGT_METHOD_DESTROY] = (void *)helpCodeDestroy,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassHelpRule = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpRulePaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpRuleCalcMinSize,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassHelpListItem = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)helpListItemPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)helpListItemCalcMinSize,
|
|
[WGT_METHOD_DESTROY] = (void *)helpListItemDestroy,
|
|
}
|
|
};
|
|
|
|
|
|
static void registerWidgetClasses(void) {
|
|
sHelpTextTypeId = wgtRegisterClass(&sClassHelpText);
|
|
sHelpHeadingTypeId = wgtRegisterClass(&sClassHelpHeading);
|
|
sHelpLinkTypeId = wgtRegisterClass(&sClassHelpLink);
|
|
sHelpNoteTypeId = wgtRegisterClass(&sClassHelpNote);
|
|
sHelpCodeTypeId = wgtRegisterClass(&sClassHelpCode);
|
|
sHelpRuleTypeId = wgtRegisterClass(&sClassHelpRule);
|
|
sHelpListItemTypeId = wgtRegisterClass(&sClassHelpListItem);
|
|
}
|
|
|
|
// ============================================================
|
|
// String table lookup
|
|
// ============================================================
|
|
|
|
static const char *hlpString(uint32_t offset) {
|
|
if (!sStringTable || offset >= sHeader.stringTableSize) {
|
|
return "";
|
|
}
|
|
|
|
return sStringTable + offset;
|
|
}
|
|
|
|
// ============================================================
|
|
// Topic lookup by ID string
|
|
// ============================================================
|
|
|
|
static int32_t findTopicByIdStr(const char *topicId) {
|
|
if (!topicId || !sTopicDir) {
|
|
return -1;
|
|
}
|
|
|
|
// Linear search -- topic directory is typically small
|
|
for (uint32_t i = 0; i < sHeader.topicCount; i++) {
|
|
const char *id = hlpString(sTopicDir[i].topicIdStr);
|
|
|
|
if (strcmp(id, topicId) == 0) {
|
|
return (int32_t)i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// ============================================================
|
|
// History management
|
|
// ============================================================
|
|
|
|
static void historyPush(int32_t topicIdx) {
|
|
// Truncate forward history when navigating to a new topic
|
|
sHistoryPos++;
|
|
|
|
if (sHistoryPos >= MAX_HISTORY) {
|
|
// Shift history down to make room
|
|
memmove(&sHistory[0], &sHistory[1], (MAX_HISTORY - 1) * sizeof(int32_t));
|
|
sHistoryPos = MAX_HISTORY - 1;
|
|
}
|
|
|
|
sHistory[sHistoryPos] = topicIdx;
|
|
sHistoryCount = sHistoryPos + 1;
|
|
}
|
|
|
|
|
|
static void navigateBack(void) {
|
|
if (sHistoryPos <= 0) {
|
|
return;
|
|
}
|
|
|
|
sHistoryPos--;
|
|
int32_t topicIdx = sHistory[sHistoryPos];
|
|
sCurrentTopic = topicIdx;
|
|
buildContentWidgets();
|
|
updateNavButtons();
|
|
}
|
|
|
|
|
|
static void navigateForward(void) {
|
|
if (sHistoryPos >= sHistoryCount - 1) {
|
|
return;
|
|
}
|
|
|
|
sHistoryPos++;
|
|
int32_t topicIdx = sHistory[sHistoryPos];
|
|
sCurrentTopic = topicIdx;
|
|
buildContentWidgets();
|
|
updateNavButtons();
|
|
}
|
|
|
|
|
|
// userData stores topicIdx + 1 so that topic 0 doesn't collide with NULL
|
|
#define TOPIC_TO_USERDATA(idx) ((void *)(intptr_t)((idx) + 1))
|
|
#define USERDATA_TO_TOPIC(ud) ((int32_t)(intptr_t)(ud) - 1)
|
|
|
|
static WidgetT *findTreeItemByTopic(WidgetT *node, int32_t topicIdx) {
|
|
for (WidgetT *child = node->firstChild; child; child = child->nextSibling) {
|
|
if (child->userData && USERDATA_TO_TOPIC(child->userData) == topicIdx) {
|
|
return child;
|
|
}
|
|
|
|
WidgetT *found = findTreeItemByTopic(child, topicIdx);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static void navigateToTopic(int32_t topicIdx) {
|
|
if (topicIdx < 0 || (uint32_t)topicIdx >= sHeader.topicCount) {
|
|
return;
|
|
}
|
|
|
|
if (topicIdx == sCurrentTopic) {
|
|
dvxLog("[WRAP] navigateToTopic: SKIPPED (same topic %d)", (int)topicIdx);
|
|
return;
|
|
}
|
|
|
|
dvxLog("[WRAP] navigateToTopic: %d -> %d", (int)sCurrentTopic, (int)topicIdx);
|
|
historyPush(topicIdx);
|
|
sCurrentTopic = topicIdx;
|
|
buildContentWidgets();
|
|
updateNavButtons();
|
|
|
|
// Update window title
|
|
const char *title = hlpString(sTopicDir[topicIdx].titleStr);
|
|
char winTitle[300];
|
|
snprintf(winTitle, sizeof(winTitle), "%s - DVX Help", title);
|
|
dvxSetTitle(sAc, sWin, winTitle);
|
|
|
|
// Sync TOC tree selection
|
|
if (sTocTree) {
|
|
WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx);
|
|
dvxLog("[WRAP] treeSync: topicIdx=%d item=%p", (int)topicIdx, (void *)item);
|
|
|
|
if (item) {
|
|
// Suppress onChange to avoid re-entering navigateToTopic
|
|
void (*savedOnChange)(WidgetT *) = sTocTree->onChange;
|
|
sTocTree->onChange = NULL;
|
|
wgtTreeViewSetSelected(sTocTree, item);
|
|
sTocTree->onChange = savedOnChange;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void updateNavButtons(void) {
|
|
if (sBtnBack) {
|
|
wgtSetEnabled(sBtnBack, sHistoryPos > 0);
|
|
}
|
|
|
|
if (sBtnForward) {
|
|
wgtSetEnabled(sBtnForward, sHistoryPos < sHistoryCount - 1);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Content display
|
|
// ============================================================
|
|
|
|
static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
|
|
if (!sContentBox) {
|
|
return;
|
|
}
|
|
|
|
switch (hdr->type) {
|
|
case HLP_REC_TEXT: {
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpTextTypeId);
|
|
|
|
if (w) {
|
|
HelpTextDataT *td = (HelpTextDataT *)dvxCalloc(1, sizeof(HelpTextDataT));
|
|
td->text = dvxStrdup(payload);
|
|
td->wrapWidth = -1;
|
|
w->data = td;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_HEADING1:
|
|
case HLP_REC_HEADING2:
|
|
case HLP_REC_HEADING3: {
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpHeadingTypeId);
|
|
|
|
if (w) {
|
|
HelpHeadingDataT *hd = (HelpHeadingDataT *)dvxCalloc(1, sizeof(HelpHeadingDataT));
|
|
hd->text = dvxStrdup(payload);
|
|
hd->level = hdr->type - HLP_REC_HEADING1 + 1;
|
|
w->data = hd;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_LINK: {
|
|
// Payload: target topic ID \0 display text \0
|
|
const char *targetTopicId = payload;
|
|
const char *displayText = payload + strlen(payload) + 1;
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpLinkTypeId);
|
|
|
|
if (w) {
|
|
HelpLinkDataT *ld = (HelpLinkDataT *)dvxCalloc(1, sizeof(HelpLinkDataT));
|
|
ld->displayText = dvxStrdup(displayText);
|
|
ld->targetTopicId = dvxStrdup(targetTopicId);
|
|
w->data = ld;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_IMAGE: {
|
|
if (hdr->length >= sizeof(HlpImageRefT)) {
|
|
const HlpImageRefT *imgRef = (const HlpImageRefT *)payload;
|
|
uint32_t absOffset = sHeader.imagePoolOffset + imgRef->imageOffset;
|
|
|
|
// Save file position — buildContentWidgets reads records
|
|
// sequentially and we must not disturb its position.
|
|
long savedPos = ftell(sHlpFile);
|
|
|
|
uint8_t *bmpData = (uint8_t *)dvxMalloc(imgRef->imageSize);
|
|
|
|
if (bmpData) {
|
|
fseek(sHlpFile, absOffset, SEEK_SET);
|
|
size_t bytesRead = fread(bmpData, 1, imgRef->imageSize, sHlpFile);
|
|
|
|
if (bytesRead == imgRef->imageSize) {
|
|
int32_t imgW = 0;
|
|
int32_t imgH = 0;
|
|
int32_t imgP = 0;
|
|
bool hasAlpha = false;
|
|
uint32_t keyColor = 0;
|
|
uint8_t *pixels = dvxLoadImageAlpha(sAc, bmpData, (int32_t)imgRef->imageSize, &imgW, &imgH, &imgP, &hasAlpha, &keyColor);
|
|
|
|
if (pixels) {
|
|
WidgetT *parent = sContentBox;
|
|
|
|
if (hdr->flags == HLP_IMG_CENTER || hdr->flags == HLP_IMG_RIGHT) {
|
|
WidgetT *row = wgtHBox(sContentBox);
|
|
|
|
if (row) {
|
|
row->align = (hdr->flags == HLP_IMG_CENTER) ? AlignCenterE : AlignEndE;
|
|
parent = row;
|
|
}
|
|
}
|
|
|
|
WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP);
|
|
|
|
if (imgWgt && hasAlpha) {
|
|
wgtImageSetTransparent(imgWgt, true, keyColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
dvxFree(bmpData);
|
|
}
|
|
|
|
// Restore file position for sequential record reading
|
|
fseek(sHlpFile, savedPos, SEEK_SET);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_LIST_ITEM: {
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpListItemTypeId);
|
|
|
|
if (w) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)dvxCalloc(1, sizeof(HelpListItemDataT));
|
|
// Prepend bullet + space to the text
|
|
int32_t pLen = (int32_t)strlen(payload);
|
|
ld->text = (char *)dvxMalloc(pLen + 3);
|
|
|
|
if (ld->text) {
|
|
ld->text[0] = '\x07';
|
|
ld->text[1] = ' ';
|
|
memcpy(ld->text + 2, payload, pLen + 1);
|
|
}
|
|
|
|
ld->wrapWidth = -1;
|
|
w->data = ld;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_HRULE: {
|
|
widgetAlloc(sContentBox, sHelpRuleTypeId);
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_NOTE: {
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpNoteTypeId);
|
|
|
|
if (w) {
|
|
HelpNoteDataT *nd = (HelpNoteDataT *)dvxCalloc(1, sizeof(HelpNoteDataT));
|
|
nd->text = dvxStrdup(payload);
|
|
nd->wrapWidth = -1;
|
|
nd->noteType = hdr->flags;
|
|
w->data = nd;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_CODE: {
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId);
|
|
|
|
if (w) {
|
|
HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT));
|
|
cd->text = dvxStrdup(payload);
|
|
cd->lineCount = countLines(cd->text);
|
|
w->data = cd;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_TABLE: {
|
|
// Render tables as preformatted text
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId);
|
|
|
|
if (w) {
|
|
HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT));
|
|
cd->text = dvxStrdup(payload);
|
|
cd->lineCount = countLines(cd->text);
|
|
w->data = cd;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Re-wrap all text content widgets at the current content box width.
|
|
|
|
static void buildContentWidgets(void) {
|
|
if (!sContentBox || sCurrentTopic < 0) {
|
|
return;
|
|
}
|
|
|
|
// Destroy old content
|
|
widgetDestroyChildren(sContentBox);
|
|
|
|
// Read topic content from file
|
|
HlpTopicDirT *topic = &sTopicDir[sCurrentTopic];
|
|
|
|
fseek(sHlpFile, topic->contentOffset, SEEK_SET);
|
|
|
|
uint32_t bytesRead = 0;
|
|
|
|
while (bytesRead < topic->contentSize) {
|
|
HlpRecordHdrT recHdr;
|
|
|
|
if (fread(&recHdr, sizeof(recHdr), 1, sHlpFile) != 1) {
|
|
break;
|
|
}
|
|
|
|
bytesRead += sizeof(recHdr);
|
|
|
|
if (recHdr.type == HLP_REC_END) {
|
|
break;
|
|
}
|
|
|
|
if (recHdr.length == 0) {
|
|
displayRecord(&recHdr, "");
|
|
continue;
|
|
}
|
|
|
|
// Read payload
|
|
char *payload = (char *)dvxMalloc(recHdr.length + 1);
|
|
|
|
if (!payload) {
|
|
break;
|
|
}
|
|
|
|
if (fread(payload, 1, recHdr.length, sHlpFile) != recHdr.length) {
|
|
dvxFree(payload);
|
|
break;
|
|
}
|
|
|
|
payload[recHdr.length] = '\0';
|
|
bytesRead += recHdr.length;
|
|
|
|
displayRecord(&recHdr, payload);
|
|
dvxFree(payload);
|
|
}
|
|
|
|
// Compute viewport wrap width from the ScrollPane.
|
|
// ScrollPane border = 2*2 = 4, vertical scrollbar = 14, VBox padding = 2*6 = 12.
|
|
// Total overhead = 30. This is approximate but stable.
|
|
if (sContentScroll && sContentScroll->w > 30) {
|
|
updateWrapWidth();
|
|
}
|
|
|
|
// Reset scroll to top
|
|
if (sContentScroll) {
|
|
wgtScrollPaneScrollToTop(sContentScroll);
|
|
}
|
|
|
|
if (sContentScroll) {
|
|
wgtInvalidate(sContentScroll);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TOC population
|
|
// ============================================================
|
|
|
|
static void populateToc(void) {
|
|
if (!sTocTree || !sTocEntries) {
|
|
return;
|
|
}
|
|
|
|
// Track parent nodes at each depth level
|
|
#define MAX_TOC_DEPTH 16
|
|
WidgetT *parents[MAX_TOC_DEPTH];
|
|
|
|
for (int32_t i = 0; i < MAX_TOC_DEPTH; i++) {
|
|
parents[i] = sTocTree;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < sHeader.tocCount; i++) {
|
|
HlpTocEntryT *entry = &sTocEntries[i];
|
|
const char *title = hlpString(entry->titleStr);
|
|
int32_t depth = entry->depth;
|
|
|
|
if (depth >= MAX_TOC_DEPTH) {
|
|
depth = MAX_TOC_DEPTH - 1;
|
|
}
|
|
|
|
WidgetT *parent = parents[depth];
|
|
WidgetT *item = wgtTreeItem(parent, title);
|
|
|
|
if (item) {
|
|
// Store topic index in userData (+1 so topic 0 != NULL)
|
|
item->userData = TOPIC_TO_USERDATA(entry->topicIdx);
|
|
|
|
// Update parent for next deeper level
|
|
if (depth + 1 < MAX_TOC_DEPTH) {
|
|
parents[depth + 1] = item;
|
|
}
|
|
|
|
// Expand by default if flagged
|
|
if (entry->flags & HLP_TOC_EXPANDED) {
|
|
wgtTreeItemSetExpanded(item, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// File I/O
|
|
// ============================================================
|
|
|
|
static bool openHelpFile(const char *path) {
|
|
sHlpFile = fopen(path, "rb");
|
|
|
|
if (!sHlpFile) {
|
|
return false;
|
|
}
|
|
|
|
// Read header from end of file
|
|
fseek(sHlpFile, -(long)sizeof(HlpHeaderT), SEEK_END);
|
|
|
|
if (fread(&sHeader, sizeof(HlpHeaderT), 1, sHlpFile) != 1) {
|
|
fclose(sHlpFile);
|
|
sHlpFile = NULL;
|
|
return false;
|
|
}
|
|
|
|
// Validate magic
|
|
if (sHeader.magic != HLP_MAGIC) {
|
|
fclose(sHlpFile);
|
|
sHlpFile = NULL;
|
|
return false;
|
|
}
|
|
|
|
// Validate version
|
|
if (sHeader.version != HLP_VERSION) {
|
|
fclose(sHlpFile);
|
|
sHlpFile = NULL;
|
|
return false;
|
|
}
|
|
|
|
// Read topic directory
|
|
sTopicDir = (HlpTopicDirT *)dvxMalloc(sHeader.topicCount * sizeof(HlpTopicDirT));
|
|
|
|
if (!sTopicDir) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
|
|
fseek(sHlpFile, sHeader.topicDirOffset, SEEK_SET);
|
|
|
|
if (fread(sTopicDir, sizeof(HlpTopicDirT), sHeader.topicCount, sHlpFile) != sHeader.topicCount) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
|
|
// Read string table
|
|
sStringTable = (char *)dvxMalloc(sHeader.stringTableSize);
|
|
|
|
if (!sStringTable) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
|
|
fseek(sHlpFile, sHeader.stringTableOffset, SEEK_SET);
|
|
|
|
if (fread(sStringTable, 1, sHeader.stringTableSize, sHlpFile) != sHeader.stringTableSize) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
|
|
// Read TOC entries
|
|
if (sHeader.tocCount > 0) {
|
|
sTocEntries = (HlpTocEntryT *)dvxMalloc(sHeader.tocCount * sizeof(HlpTocEntryT));
|
|
|
|
if (!sTocEntries) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
|
|
fseek(sHlpFile, sHeader.tocOffset, SEEK_SET);
|
|
|
|
if (fread(sTocEntries, sizeof(HlpTocEntryT), sHeader.tocCount, sHlpFile) != sHeader.tocCount) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Read keyword index
|
|
if (sHeader.indexCount > 0) {
|
|
sIndexEntries = (HlpIndexEntryT *)dvxMalloc(sHeader.indexCount * sizeof(HlpIndexEntryT));
|
|
|
|
if (!sIndexEntries) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
|
|
fseek(sHlpFile, sHeader.indexOffset, SEEK_SET);
|
|
|
|
if (fread(sIndexEntries, sizeof(HlpIndexEntryT), sHeader.indexCount, sHlpFile) != sHeader.indexCount) {
|
|
closeHelpFile();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static void closeHelpFile(void) {
|
|
if (sHlpFile) {
|
|
fclose(sHlpFile);
|
|
sHlpFile = NULL;
|
|
}
|
|
|
|
dvxFree(sTopicDir);
|
|
sTopicDir = NULL;
|
|
|
|
dvxFree(sStringTable);
|
|
sStringTable = NULL;
|
|
|
|
dvxFree(sTocEntries);
|
|
sTocEntries = NULL;
|
|
|
|
dvxFree(sIndexEntries);
|
|
sIndexEntries = NULL;
|
|
}
|
|
|
|
// ============================================================
|
|
// Callbacks
|
|
// ============================================================
|
|
|
|
static void onClose(WindowT *win) {
|
|
closeHelpFile();
|
|
dvxDestroyWindow(sAc, win);
|
|
sWin = NULL;
|
|
sTocTree = NULL;
|
|
sContentScroll = NULL;
|
|
sContentBox = NULL;
|
|
sBtnBack = NULL;
|
|
sBtnForward = NULL;
|
|
}
|
|
|
|
|
|
static void onIndex(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sIndexEntries || sHeader.indexCount == 0) {
|
|
dvxMessageBox(sAc, "DVX Help", "No index available.", MB_OK | MB_ICONINFO);
|
|
return;
|
|
}
|
|
|
|
// Build list of keyword strings
|
|
const char **items = (const char **)dvxMalloc(sHeader.indexCount * sizeof(const char *));
|
|
|
|
if (!items) {
|
|
return;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < sHeader.indexCount; i++) {
|
|
items[i] = hlpString(sIndexEntries[i].keywordStr);
|
|
}
|
|
|
|
int32_t selected = -1;
|
|
bool ok = dvxChoiceDialog(sAc, "Index", "Select a topic:", items, (int32_t)sHeader.indexCount, 0, &selected);
|
|
|
|
if (ok && selected >= 0 && (uint32_t)selected < sHeader.indexCount) {
|
|
int32_t topicIdx = (int32_t)sIndexEntries[selected].topicIdx;
|
|
navigateToTopic(topicIdx);
|
|
}
|
|
|
|
dvxFree(items);
|
|
}
|
|
|
|
|
|
static void onMenu(WindowT *win, int32_t menuId) {
|
|
(void)win;
|
|
|
|
switch (menuId) {
|
|
case CMD_BACK:
|
|
navigateBack();
|
|
break;
|
|
|
|
case CMD_FORWARD:
|
|
navigateForward();
|
|
break;
|
|
|
|
case CMD_INDEX:
|
|
onIndex(NULL);
|
|
break;
|
|
|
|
case CMD_SEARCH:
|
|
onSearch(NULL);
|
|
break;
|
|
|
|
case CMD_EXIT:
|
|
if (sWin) {
|
|
onClose(sWin);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void onNavBack(WidgetT *w) {
|
|
(void)w;
|
|
navigateBack();
|
|
}
|
|
|
|
|
|
static void onNavForward(WidgetT *w) {
|
|
(void)w;
|
|
navigateForward();
|
|
}
|
|
|
|
|
|
static void onSearch(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sIndexEntries || sHeader.indexCount == 0) {
|
|
dvxMessageBox(sAc, "DVX Help", "No search index available.", MB_OK | MB_ICONINFO);
|
|
return;
|
|
}
|
|
|
|
char searchBuf[MAX_SEARCH_BUF] = "";
|
|
|
|
if (!dvxInputBox(sAc, "Search Help", "Search for:", NULL, searchBuf, sizeof(searchBuf))) {
|
|
return;
|
|
}
|
|
|
|
if (searchBuf[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
// Linear scan of keyword index for case-insensitive substring match
|
|
int32_t firstMatch = -1;
|
|
|
|
for (uint32_t i = 0; i < sHeader.indexCount; i++) {
|
|
const char *keyword = hlpString(sIndexEntries[i].keywordStr);
|
|
// Case-insensitive substring search
|
|
const char *kp = keyword;
|
|
const char *sp = searchBuf;
|
|
bool found = false;
|
|
|
|
while (*kp && !found) {
|
|
const char *k = kp;
|
|
const char *s = sp;
|
|
bool match = true;
|
|
|
|
while (*s && *k) {
|
|
char kc = *k;
|
|
char sc = *s;
|
|
|
|
if (kc >= 'A' && kc <= 'Z') {
|
|
kc += 32;
|
|
}
|
|
|
|
if (sc >= 'A' && sc <= 'Z') {
|
|
sc += 32;
|
|
}
|
|
|
|
if (kc != sc) {
|
|
match = false;
|
|
break;
|
|
}
|
|
|
|
k++;
|
|
s++;
|
|
}
|
|
|
|
if (match && *s == '\0') {
|
|
found = true;
|
|
}
|
|
|
|
kp++;
|
|
}
|
|
|
|
if (found) {
|
|
firstMatch = (int32_t)sIndexEntries[i].topicIdx;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstMatch >= 0) {
|
|
navigateToTopic(firstMatch);
|
|
} else {
|
|
dvxMessageBox(sAc, "Search Help", "No matching topics found.", MB_OK | MB_ICONINFO);
|
|
}
|
|
}
|
|
|
|
|
|
static void onTocChange(WidgetT *w) {
|
|
WidgetT *selected = wgtTreeViewGetSelected(w);
|
|
|
|
if (!selected || !selected->userData) {
|
|
return;
|
|
}
|
|
|
|
int32_t topicIdx = USERDATA_TO_TOPIC(selected->userData);
|
|
|
|
if (topicIdx < 0 || topicIdx >= (int32_t)sHeader.topicCount) {
|
|
return;
|
|
}
|
|
|
|
navigateToTopic(topicIdx);
|
|
}
|
|
|
|
// ============================================================
|
|
// Entry point
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx) {
|
|
sCtx = ctx;
|
|
sAc = ctx->shellCtx;
|
|
|
|
// Register private widget classes
|
|
registerWidgetClasses();
|
|
|
|
// Determine help file path: use args if provided, else default
|
|
char hlpPath[DVX_MAX_PATH];
|
|
|
|
if (ctx->args[0]) {
|
|
// args may be "path/to/file.hlp" or "path/to/file.hlp topicId"
|
|
snprintf(hlpPath, sizeof(hlpPath), "%s", ctx->args);
|
|
|
|
// Truncate at first space to separate path from topic ID
|
|
char *space = strchr(hlpPath, ' ');
|
|
|
|
if (space) {
|
|
*space = '\0';
|
|
}
|
|
} else {
|
|
snprintf(hlpPath, sizeof(hlpPath), "%s%c%s", ctx->appDir, DVX_PATH_SEP, "dvxhelp.hlp");
|
|
}
|
|
|
|
if (!openHelpFile(hlpPath)) {
|
|
dvxSetBusy(sAc, false);
|
|
dvxMessageBox(sAc, "DVX Help", "Could not open help file.", MB_OK | MB_ICONERROR);
|
|
return -1;
|
|
}
|
|
|
|
// Create window
|
|
int32_t screenW = sAc->display.width;
|
|
int32_t screenH = sAc->display.height;
|
|
int32_t winW = HELP_WIN_W;
|
|
int32_t winH = HELP_WIN_H;
|
|
int32_t winX = (screenW - winW) / 2 + HELP_CASCADE_OFFSET;
|
|
int32_t winY = (screenH - winH) / 3 + HELP_CASCADE_OFFSET;
|
|
|
|
sWin = dvxCreateWindow(sAc, "DVX Help", winX, winY, winW, winH, true);
|
|
|
|
if (!sWin) {
|
|
closeHelpFile();
|
|
return -1;
|
|
}
|
|
|
|
sWin->onClose = onClose;
|
|
sWin->onMenu = onMenu;
|
|
|
|
// Menu bar
|
|
MenuBarT *menuBar = wmAddMenuBar(sWin);
|
|
|
|
MenuT *navMenu = wmAddMenu(menuBar, "&Navigate");
|
|
wmAddMenuItem(navMenu, "&Back", CMD_BACK);
|
|
wmAddMenuItem(navMenu, "&Forward", CMD_FORWARD);
|
|
wmAddMenuSeparator(navMenu);
|
|
wmAddMenuItem(navMenu, "&Index...", CMD_INDEX);
|
|
wmAddMenuItem(navMenu, "&Search...", CMD_SEARCH);
|
|
wmAddMenuSeparator(navMenu);
|
|
wmAddMenuItem(navMenu, "E&xit", CMD_EXIT);
|
|
|
|
// Widget tree
|
|
WidgetT *root = wgtInitWindow(sAc, sWin);
|
|
|
|
// Toolbar
|
|
WidgetT *toolbar = wgtToolbar(root);
|
|
|
|
sBtnBack = wgtButton(toolbar, "< Back");
|
|
sBtnBack->onClick = onNavBack;
|
|
wgtSetEnabled(sBtnBack, false);
|
|
|
|
sBtnForward = wgtButton(toolbar, "Fwd >");
|
|
sBtnForward->onClick = onNavForward;
|
|
wgtSetEnabled(sBtnForward, false);
|
|
|
|
WidgetT *btnIndex = wgtButton(toolbar, "Index");
|
|
btnIndex->onClick = onIndex;
|
|
|
|
WidgetT *btnSearch = wgtButton(toolbar, "Search");
|
|
btnSearch->onClick = onSearch;
|
|
|
|
// Splitter with TOC tree and content scroll pane
|
|
WidgetT *splitter = wgtSplitter(root, true);
|
|
splitter->weight = 100;
|
|
|
|
sTocTree = wgtTreeView(splitter);
|
|
sTocTree->weight = 0;
|
|
sTocTree->minW = wgtPixels(HELP_TOC_MIN_W);
|
|
sTocTree->onChange = onTocChange;
|
|
|
|
sContentScroll = wgtScrollPane(splitter);
|
|
sContentScroll->weight = 100;
|
|
|
|
sContentBox = wgtVBox(sContentScroll);
|
|
sContentBox->spacing = wgtPixels(HELP_PARA_SPACING);
|
|
sContentBox->padding = wgtPixels(HELP_CONTENT_PAD);
|
|
|
|
wgtSplitterSetPos(splitter, HELP_SPLITTER_POS);
|
|
|
|
// Populate TOC tree
|
|
populateToc();
|
|
|
|
// Navigate to requested topic (from args) or default
|
|
int32_t startTopic = -1;
|
|
|
|
if (ctx->args[0]) {
|
|
// Check for topic ID after the file path
|
|
const char *space = strchr(ctx->args, ' ');
|
|
|
|
if (space && space[1]) {
|
|
startTopic = findTopicByIdStr(space + 1);
|
|
}
|
|
}
|
|
|
|
if (startTopic < 0) {
|
|
startTopic = findTopicByIdStr(hlpString(sHeader.defaultTopicStr));
|
|
}
|
|
|
|
if (startTopic < 0 && sHeader.topicCount > 0) {
|
|
startTopic = 0;
|
|
}
|
|
|
|
if (startTopic >= 0) {
|
|
navigateToTopic(startTopic);
|
|
}
|
|
|
|
return 0;
|
|
}
|