1406 lines
40 KiB
C
1406 lines
40 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 "dvxDialog.h"
|
|
#include "dvxWidget.h"
|
|
#include "dvxWidgetPlugin.h"
|
|
#include "dvxWm.h"
|
|
#include "dvxDraw.h"
|
|
#include "dvxVideo.h"
|
|
#include "dvxPlatform.h"
|
|
#include "shellApp.h"
|
|
#include "widgetBox.h"
|
|
#include "widgetButton.h"
|
|
#include "widgetSplitter.h"
|
|
#include "widgetTreeView.h"
|
|
#include "widgetScrollPane.h"
|
|
#include "widgetToolbar.h"
|
|
#include "widgetImage.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_HEADING1_PAD 4
|
|
#define HELP_HEADING2_PAD 2
|
|
#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 HELP_LIST_INDENT 12
|
|
#define HELP_LIST_BULLET_W 8
|
|
|
|
#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
|
|
|
|
// ============================================================
|
|
// 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;
|
|
|
|
// 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;
|
|
int32_t lineCount;
|
|
} HelpTextDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
int32_t level;
|
|
} HelpHeadingDataT;
|
|
|
|
typedef struct {
|
|
char *displayText;
|
|
char *targetTopicId;
|
|
} HelpLinkDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
int32_t lineCount;
|
|
uint8_t noteType;
|
|
} HelpNoteDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
int32_t lineCount;
|
|
} HelpCodeDataT;
|
|
|
|
typedef struct {
|
|
char *text;
|
|
} 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 int32_t maxLineWidth(const BitmapFontT *font, const char *text);
|
|
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
|
|
// ============================================================
|
|
|
|
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 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 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
|
|
// ============================================================
|
|
|
|
static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpTextDataT *td = (HelpTextDataT *)w->data;
|
|
w->calcMinW = maxLineWidth(font, td->text) + HELP_CONTENT_PAD * 2;
|
|
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;
|
|
paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, td->text, td->lineCount, colors->contentFg);
|
|
}
|
|
|
|
|
|
static void helpTextDestroy(WidgetT *w) {
|
|
HelpTextDataT *td = (HelpTextDataT *)w->data;
|
|
|
|
if (td) {
|
|
dvxFree(td->text);
|
|
dvxFree(td);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HelpHeading widget class
|
|
// ============================================================
|
|
|
|
static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data;
|
|
int32_t textW = textWidth(font, hd->text);
|
|
int32_t textH = font->charHeight;
|
|
|
|
w->calcMinW = textW + HELP_CONTENT_PAD * 2;
|
|
|
|
if (hd->level == 1) {
|
|
// Full bar with padding above and below
|
|
w->calcMinH = textH + HELP_HEADING1_PAD * 2 + 2;
|
|
} else if (hd->level == 2) {
|
|
// Text with underline
|
|
w->calcMinH = textH + HELP_HEADING2_PAD + 2;
|
|
} else {
|
|
// Plain bold text
|
|
w->calcMinH = textH + 2;
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
if (hd->level == 1) {
|
|
// Filled bar with title bar colors
|
|
rectFill(d, ops, w->x, w->y, w->w, w->h, colors->activeTitleBg);
|
|
int32_t textY = w->y + 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) {
|
|
// Text with underline
|
|
int32_t textY = w->y;
|
|
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 {
|
|
// Plain text
|
|
drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, 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) {
|
|
HelpLinkDataT *ld = (HelpLinkDataT *)w->data;
|
|
w->calcMinW = textWidth(font, ld->displayText) + HELP_CONTENT_PAD * 2 + HELP_LINK_PAD_X * 2;
|
|
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;
|
|
w->calcMinW = maxLineWidth(font, nd->text) + HELP_CONTENT_PAD * 2 + HELP_NOTE_PAD * 2 + HELP_NOTE_PREFIX_W;
|
|
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;
|
|
|
|
// Left accent bar color varies by note type; background is always windowFace
|
|
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;
|
|
paintLines(d, ops, font, textX, w->y + HELP_NOTE_PAD, nd->text, nd->lineCount, colors->contentFg);
|
|
}
|
|
|
|
|
|
static void helpNoteDestroy(WidgetT *w) {
|
|
HelpNoteDataT *nd = (HelpNoteDataT *)w->data;
|
|
|
|
if (nd) {
|
|
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;
|
|
w->calcMinW = textWidth(font, ld->text) + HELP_CONTENT_PAD * 2 + HELP_LIST_INDENT;
|
|
w->calcMinH = font->charHeight;
|
|
}
|
|
|
|
|
|
static void helpListItemPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)w->data;
|
|
int32_t textLen = (int32_t)strlen(ld->text);
|
|
int32_t bulletX = w->x + HELP_CONTENT_PAD + HELP_LIST_INDENT - HELP_LIST_BULLET_W;
|
|
int32_t textX = w->x + HELP_CONTENT_PAD + HELP_LIST_INDENT;
|
|
int32_t textY = w->y;
|
|
|
|
// Bullet character
|
|
drawTextN(d, ops, font, bulletX, textY, "\x07", 1, colors->contentFg, 0, false);
|
|
|
|
if (textLen > 0) {
|
|
drawTextN(d, ops, font, textX, textY, ld->text, textLen, colors->contentFg, 0, false);
|
|
}
|
|
}
|
|
|
|
|
|
static void helpListItemDestroy(WidgetT *w) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)w->data;
|
|
|
|
if (ld) {
|
|
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();
|
|
}
|
|
|
|
|
|
static void navigateToTopic(int32_t topicIdx) {
|
|
if (topicIdx < 0 || (uint32_t)topicIdx >= sHeader.topicCount) {
|
|
return;
|
|
}
|
|
|
|
if (topicIdx == sCurrentTopic) {
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
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->lineCount = countLines(td->text);
|
|
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: display text \0 target topic ID \0
|
|
const char *displayText = payload;
|
|
const char *targetTopicId = 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;
|
|
|
|
// Read BMP data from file
|
|
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;
|
|
uint8_t *pixels = dvxLoadImageFromMemory(sAc, bmpData, (int32_t)imgRef->imageSize, &imgW, &imgH, &imgP);
|
|
|
|
if (pixels) {
|
|
wgtImage(sContentBox, pixels, imgW, imgH, imgP);
|
|
// wgtImage takes ownership of the pixel data
|
|
}
|
|
}
|
|
|
|
dvxFree(bmpData);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case HLP_REC_LIST_ITEM: {
|
|
WidgetT *w = widgetAlloc(sContentBox, sHelpListItemTypeId);
|
|
|
|
if (w) {
|
|
HelpListItemDataT *ld = (HelpListItemDataT *)dvxCalloc(1, sizeof(HelpListItemDataT));
|
|
ld->text = dvxStrdup(payload);
|
|
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->lineCount = countLines(nd->text);
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
// Scroll to top by scrolling the first child into view
|
|
if (sContentScroll && sContentBox->firstChild) {
|
|
wgtScrollPaneScrollToChild(sContentScroll, sContentBox->firstChild);
|
|
}
|
|
|
|
wgtInvalidate(sContentBox);
|
|
}
|
|
|
|
// ============================================================
|
|
// 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 for selection callback
|
|
item->userData = (void *)(intptr_t)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;
|
|
}
|
|
}
|
|
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
int32_t topicIdx = (int32_t)(intptr_t)selected->userData;
|
|
|
|
if (topicIdx == 0xFFFF) {
|
|
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);
|
|
|
|
// 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(2);
|
|
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;
|
|
}
|