Find/Replace
This commit is contained in:
parent
289adb8c47
commit
0ef46ff6a0
5 changed files with 731 additions and 31 deletions
|
|
@ -17,8 +17,10 @@
|
|||
#include "dvxWm.h"
|
||||
#include "shellApp.h"
|
||||
#include "widgetBox.h"
|
||||
#include "widgetCheckbox.h"
|
||||
#include "widgetImageButton.h"
|
||||
#include "widgetLabel.h"
|
||||
#include "widgetRadio.h"
|
||||
#include "widgetTextInput.h"
|
||||
#include "widgetDropdown.h"
|
||||
#include "widgetButton.h"
|
||||
|
|
@ -87,6 +89,9 @@
|
|||
#define CMD_PRJ_PROPS 138
|
||||
#define CMD_WIN_PROJECT 137
|
||||
#define CMD_HELP_ABOUT 140
|
||||
#define CMD_FIND 141
|
||||
#define CMD_REPLACE 142
|
||||
#define CMD_FIND_NEXT 143
|
||||
#define IDE_MAX_IMM 1024
|
||||
#define IDE_DESIGN_W 400
|
||||
#define IDE_DESIGN_H 300
|
||||
|
|
@ -145,6 +150,9 @@ static void onFormWinClose(WindowT *win);
|
|||
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH);
|
||||
static void onProjectWinClose(WindowT *win);
|
||||
static WindowT *getLastFocusWin(void);
|
||||
static void closeFindDialog(void);
|
||||
static bool findInProject(const char *needle, bool caseSensitive);
|
||||
static void openFindDialog(bool showReplace);
|
||||
static void handleEditCmd(int32_t cmd);
|
||||
static void handleFileCmd(int32_t cmd);
|
||||
static void handleProjectCmd(int32_t cmd);
|
||||
|
|
@ -224,6 +232,20 @@ static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
|
|||
static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
|
||||
static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
|
||||
|
||||
// Find/Replace state
|
||||
static char sFindText[256] = "";
|
||||
static char sReplaceText[256] = "";
|
||||
// Find/Replace dialog state (modeless)
|
||||
static WindowT *sFindWin = NULL;
|
||||
static WidgetT *sFindInput = NULL;
|
||||
static WidgetT *sReplInput = NULL;
|
||||
static WidgetT *sReplCheck = NULL;
|
||||
static WidgetT *sBtnReplace = NULL;
|
||||
static WidgetT *sBtnReplAll = NULL;
|
||||
static WidgetT *sCaseCheck = NULL;
|
||||
static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj
|
||||
static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
|
||||
|
||||
// Procedure table for Object/Event dropdowns
|
||||
typedef struct {
|
||||
char objName[64];
|
||||
|
|
@ -403,6 +425,8 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
|
|||
sEditorFileIdx = fileIdx;
|
||||
sProject.activeFileIdx = fileIdx;
|
||||
}
|
||||
|
||||
updateDirtyIndicators();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -453,8 +477,10 @@ int32_t appMain(DxeAppContextT *ctx) {
|
|||
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
|
||||
|
||||
if (sProjectWin) {
|
||||
sProjectWin->y = toolbarBottom() + 25;
|
||||
sProjectWin->onClose = onProjectWinClose;
|
||||
sProjectWin->y = toolbarBottom() + 25;
|
||||
sProjectWin->onClose = onProjectWinClose;
|
||||
sProjectWin->onMenu = onMenu;
|
||||
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
|
||||
char title[300];
|
||||
|
|
@ -536,6 +562,10 @@ static void buildWindow(void) {
|
|||
wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL);
|
||||
wmAddMenuSeparator(editMenu);
|
||||
wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE);
|
||||
wmAddMenuSeparator(editMenu);
|
||||
wmAddMenuItem(editMenu, "&Find...\tCtrl+F", CMD_FIND);
|
||||
wmAddMenuItem(editMenu, "Find &Next\tF3", CMD_FIND_NEXT);
|
||||
wmAddMenuItem(editMenu, "&Replace...\tCtrl+H", CMD_REPLACE);
|
||||
|
||||
MenuT *runMenu = wmAddMenu(menuBar, "&Run");
|
||||
wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
|
||||
|
|
@ -573,6 +603,9 @@ static void buildWindow(void) {
|
|||
dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
|
||||
dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
|
||||
dvxAddAccel(accel, 0x1B, 0, CMD_STOP);
|
||||
dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
|
||||
dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE);
|
||||
dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT);
|
||||
sWin->accelTable = accel;
|
||||
|
||||
WidgetT *tbRoot = wgtInitWindow(sAc, sWin);
|
||||
|
|
@ -1129,6 +1162,8 @@ static void runCached(void) {
|
|||
static void runModule(BasModuleT *mod) {
|
||||
setStatus("Running...");
|
||||
|
||||
closeFindDialog();
|
||||
|
||||
// Hide IDE windows while the program runs
|
||||
bool hadFormWin = sFormWin && sFormWin->visible;
|
||||
bool hadToolbox = sToolboxWin && sToolboxWin->visible;
|
||||
|
|
@ -1674,8 +1709,10 @@ static void newProject(void) {
|
|||
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
|
||||
|
||||
if (sProjectWin) {
|
||||
sProjectWin->y = toolbarBottom() + 25;
|
||||
sProjectWin->onClose = onProjectWinClose;
|
||||
sProjectWin->y = toolbarBottom() + 25;
|
||||
sProjectWin->onClose = onProjectWinClose;
|
||||
sProjectWin->onMenu = onMenu;
|
||||
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
} else {
|
||||
prjRebuildTree(&sProject);
|
||||
|
|
@ -1720,8 +1757,10 @@ static void openProject(void) {
|
|||
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
|
||||
|
||||
if (sProjectWin) {
|
||||
sProjectWin->y = toolbarBottom() + 25;
|
||||
sProjectWin->onClose = onProjectWinClose;
|
||||
sProjectWin->y = toolbarBottom() + 25;
|
||||
sProjectWin->onClose = onProjectWinClose;
|
||||
sProjectWin->onMenu = onMenu;
|
||||
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
} else {
|
||||
prjRebuildTree(&sProject);
|
||||
|
|
@ -1753,6 +1792,8 @@ static void closeProject(void) {
|
|||
return;
|
||||
}
|
||||
|
||||
closeFindDialog();
|
||||
|
||||
if (sProject.dirty) {
|
||||
prjSave(&sProject);
|
||||
}
|
||||
|
|
@ -2040,6 +2081,8 @@ static void onCodeWinClose(WindowT *win) {
|
|||
if (sLastFocusWin == win) {
|
||||
sLastFocusWin = NULL;
|
||||
}
|
||||
|
||||
updateProjectMenuState();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -2342,6 +2385,466 @@ static void handleFileCmd(int32_t cmd) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Find/Replace dialog (modeless)
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
ScopeFuncE,
|
||||
ScopeObjE,
|
||||
ScopeFileE,
|
||||
ScopeProjE
|
||||
} FindScopeE;
|
||||
|
||||
|
||||
static FindScopeE getFindScope(void) {
|
||||
if (!sScopeGroup) {
|
||||
return ScopeProjE;
|
||||
}
|
||||
|
||||
int32_t idx = wgtRadioGetIndex(sScopeGroup);
|
||||
|
||||
switch (idx) {
|
||||
case 0: return ScopeFuncE;
|
||||
case 1: return ScopeObjE;
|
||||
case 2: return ScopeFileE;
|
||||
default: return ScopeProjE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool getFindMatchCase(void) {
|
||||
return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck);
|
||||
}
|
||||
|
||||
|
||||
static bool getFindForward(void) {
|
||||
if (!sDirGroup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return wgtRadioGetIndex(sDirGroup) == 0;
|
||||
}
|
||||
|
||||
|
||||
static bool isReplaceEnabled(void) {
|
||||
return sReplCheck && wgtCheckboxIsChecked(sReplCheck);
|
||||
}
|
||||
|
||||
|
||||
static void onReplCheckChange(WidgetT *w) {
|
||||
(void)w;
|
||||
bool show = isReplaceEnabled();
|
||||
|
||||
if (sReplInput) { sReplInput->enabled = show; }
|
||||
if (sBtnReplace) { sBtnReplace->enabled = show; }
|
||||
if (sBtnReplAll) { sBtnReplAll->enabled = show; }
|
||||
|
||||
if (sFindWin) {
|
||||
dvxInvalidateWindow(sAc, sFindWin);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void onFindNext(WidgetT *w) {
|
||||
(void)w;
|
||||
|
||||
if (!sFindInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *needle = wgtGetText(sFindInput);
|
||||
|
||||
if (!needle || !needle[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
||||
|
||||
FindScopeE scope = getFindScope();
|
||||
bool caseSens = getFindMatchCase();
|
||||
bool forward = getFindForward();
|
||||
|
||||
if (scope == ScopeFuncE && sEditor) {
|
||||
// Search current procedure only
|
||||
if (!wgtTextAreaFindNext(sEditor, sFindText, caseSens, forward)) {
|
||||
setStatus("Not found.");
|
||||
}
|
||||
} else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) {
|
||||
if (!findInProject(sFindText, caseSens)) {
|
||||
setStatus("Not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void onReplace(WidgetT *w) {
|
||||
(void)w;
|
||||
|
||||
if (!sEditor || !sFindInput || !sReplInput || !isReplaceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *needle = wgtGetText(sFindInput);
|
||||
const char *repl = wgtGetText(sReplInput);
|
||||
|
||||
if (!needle || !needle[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
||||
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
|
||||
|
||||
// If text is selected and matches the search, replace it, then find next
|
||||
const char *edText = wgtGetText(sEditor);
|
||||
|
||||
if (edText) {
|
||||
// TODO: replace current selection if it matches, then find next
|
||||
// For now, just do find next
|
||||
onFindNext(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void onReplaceAll(WidgetT *w) {
|
||||
(void)w;
|
||||
|
||||
if (!sFindInput || !sReplInput || !isReplaceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *needle = wgtGetText(sFindInput);
|
||||
const char *repl = wgtGetText(sReplInput);
|
||||
|
||||
if (!needle || !needle[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
||||
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
|
||||
|
||||
bool caseSens = getFindMatchCase();
|
||||
FindScopeE scope = getFindScope();
|
||||
int32_t totalCount = 0;
|
||||
|
||||
if (scope == ScopeFuncE && sEditor) {
|
||||
totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
||||
} else if (scope == ScopeProjE) {
|
||||
stashCurrentFile();
|
||||
|
||||
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
||||
activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE);
|
||||
|
||||
if (sEditor) {
|
||||
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
||||
}
|
||||
}
|
||||
} else if (scope == ScopeFileE && sEditor) {
|
||||
// Replace in all procs of current file
|
||||
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
||||
|
||||
for (int32_t p = -1; p < procCount; p++) {
|
||||
showProc(p);
|
||||
|
||||
if (sEditor) {
|
||||
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
||||
}
|
||||
}
|
||||
} else if (scope == ScopeObjE && sEditor && sObjDropdown) {
|
||||
// Replace in all procs belonging to current object
|
||||
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
||||
|
||||
if (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) {
|
||||
const char *objName = sObjItems[objIdx];
|
||||
int32_t procCount = (int32_t)arrlen(sProcTable);
|
||||
|
||||
for (int32_t p = 0; p < procCount; p++) {
|
||||
if (strcasecmp(sProcTable[p].objName, objName) == 0) {
|
||||
showProc(p);
|
||||
|
||||
if (sEditor) {
|
||||
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char statusBuf[64];
|
||||
snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
|
||||
setStatus(statusBuf);
|
||||
}
|
||||
|
||||
|
||||
static void onFindClose(WindowT *win) {
|
||||
(void)win;
|
||||
closeFindDialog();
|
||||
}
|
||||
|
||||
|
||||
static void onFindCloseBtn(WidgetT *w) {
|
||||
(void)w;
|
||||
closeFindDialog();
|
||||
}
|
||||
|
||||
|
||||
static void closeFindDialog(void) {
|
||||
if (sFindWin) {
|
||||
dvxDestroyWindow(sAc, sFindWin);
|
||||
sFindWin = NULL;
|
||||
sFindInput = NULL;
|
||||
sReplInput = NULL;
|
||||
sReplCheck = NULL;
|
||||
sBtnReplace = NULL;
|
||||
sBtnReplAll = NULL;
|
||||
sCaseCheck = NULL;
|
||||
sScopeGroup = NULL;
|
||||
sDirGroup = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void openFindDialog(bool showReplace) {
|
||||
if (sFindWin) {
|
||||
// Already open — just toggle replace mode and raise
|
||||
if (sReplCheck) {
|
||||
wgtCheckboxSetChecked(sReplCheck, showReplace);
|
||||
onReplCheckChange(sReplCheck);
|
||||
}
|
||||
|
||||
dvxRaiseWindow(sAc, sFindWin);
|
||||
return;
|
||||
}
|
||||
|
||||
sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", 320, 210, false);
|
||||
|
||||
if (!sFindWin) {
|
||||
return;
|
||||
}
|
||||
|
||||
sFindWin->onClose = onFindClose;
|
||||
sFindWin->onMenu = onMenu;
|
||||
sFindWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
|
||||
WidgetT *root = wgtInitWindow(sAc, sFindWin);
|
||||
root->spacing = wgtPixels(3);
|
||||
|
||||
// Find row
|
||||
WidgetT *findRow = wgtHBox(root);
|
||||
findRow->spacing = wgtPixels(4);
|
||||
wgtLabel(findRow, "Find:");
|
||||
sFindInput = wgtTextInput(findRow, 256);
|
||||
sFindInput->weight = 100;
|
||||
wgtSetText(sFindInput, sFindText);
|
||||
|
||||
// Replace checkbox + input
|
||||
WidgetT *replRow = wgtHBox(root);
|
||||
replRow->spacing = wgtPixels(4);
|
||||
sReplCheck = wgtCheckbox(replRow, "Replace:");
|
||||
wgtCheckboxSetChecked(sReplCheck, showReplace);
|
||||
sReplCheck->onChange = onReplCheckChange;
|
||||
sReplInput = wgtTextInput(replRow, 256);
|
||||
sReplInput->weight = 100;
|
||||
wgtSetText(sReplInput, sReplaceText);
|
||||
|
||||
// Options row: scope + direction + case
|
||||
WidgetT *optRow = wgtHBox(root);
|
||||
optRow->spacing = wgtPixels(8);
|
||||
|
||||
// Scope
|
||||
WidgetT *scopeFrame = wgtFrame(optRow, "Scope");
|
||||
WidgetT *scopeBox = wgtVBox(scopeFrame);
|
||||
sScopeGroup = wgtRadioGroup(scopeBox);
|
||||
wgtRadio(sScopeGroup, "Function");
|
||||
wgtRadio(sScopeGroup, "Object");
|
||||
wgtRadio(sScopeGroup, "File");
|
||||
wgtRadio(sScopeGroup, "Project");
|
||||
wgtRadioGroupSetSelected(sScopeGroup, 3); // Project
|
||||
|
||||
// Direction
|
||||
WidgetT *dirFrame = wgtFrame(optRow, "Direction");
|
||||
WidgetT *dirBox = wgtVBox(dirFrame);
|
||||
sDirGroup = wgtRadioGroup(dirBox);
|
||||
wgtRadio(sDirGroup, "Forward");
|
||||
wgtRadio(sDirGroup, "Backward");
|
||||
wgtRadioGroupSetSelected(sDirGroup, 0); // Forward
|
||||
|
||||
// Match Case
|
||||
WidgetT *caseBox = wgtVBox(optRow);
|
||||
sCaseCheck = wgtCheckbox(caseBox, "Match Case");
|
||||
|
||||
// Buttons
|
||||
WidgetT *btnRow = wgtHBox(root);
|
||||
btnRow->spacing = wgtPixels(4);
|
||||
btnRow->align = AlignEndE;
|
||||
|
||||
WidgetT *btnFind = wgtButton(btnRow, "Find Next");
|
||||
btnFind->onClick = onFindNext;
|
||||
|
||||
sBtnReplace = wgtButton(btnRow, "Replace");
|
||||
sBtnReplace->onClick = onReplace;
|
||||
|
||||
sBtnReplAll = wgtButton(btnRow, "Replace All");
|
||||
sBtnReplAll->onClick = onReplaceAll;
|
||||
|
||||
WidgetT *btnClose = wgtButton(btnRow, "Close");
|
||||
btnClose->onClick = onFindCloseBtn;
|
||||
|
||||
// Set initial replace enable state
|
||||
onReplCheckChange(sReplCheck);
|
||||
|
||||
dvxFitWindow(sAc, sFindWin);
|
||||
}
|
||||
|
||||
|
||||
// findInProject -- search all project files for a text match.
|
||||
// Starts from the current editor position in the current file,
|
||||
// then continues through subsequent files, wrapping around.
|
||||
// Opens the file and selects the match when found.
|
||||
|
||||
// showProcAndFind -- switch to a procedure, sync the dropdowns, and
|
||||
// select the search match in the editor.
|
||||
|
||||
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive) {
|
||||
showProc(procIdx);
|
||||
|
||||
// Sync the Object/Event dropdowns to match
|
||||
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
|
||||
selectDropdowns(sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
|
||||
} else if (procIdx == -1 && sObjDropdown) {
|
||||
wgtDropdownSetSelected(sObjDropdown, 0); // (General)
|
||||
}
|
||||
|
||||
if (sEditor) {
|
||||
return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Case-insensitive strstr replacement (strcasestr is a GNU extension)
|
||||
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) {
|
||||
for (; *haystack; haystack++) {
|
||||
if (strncasecmp(haystack, needle, needleLen) == 0) {
|
||||
return haystack;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static bool findInProject(const char *needle, bool caseSensitive) {
|
||||
if (!needle || !needle[0] || sProject.fileCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t needleLen = (int32_t)strlen(needle);
|
||||
|
||||
// Stash current editor state so all buffers are up-to-date
|
||||
stashCurrentFile();
|
||||
|
||||
// Start from the active file, searching from after the current selection
|
||||
int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0;
|
||||
int32_t startPos = 0;
|
||||
|
||||
// If the editor is open on the current file, try the current proc
|
||||
// first (no wrap — returns false if no more matches ahead).
|
||||
if (sEditor && sEditorFileIdx == startFile) {
|
||||
if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No more matches in current proc — search remaining procs
|
||||
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
||||
|
||||
for (int32_t p = sCurProcIdx + 1; p < procCount; p++) {
|
||||
if (!sProcBufs[p]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
|
||||
|
||||
if (found) {
|
||||
showProcAndFind(p, needle, caseSensitive);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Search General section if we started in a proc
|
||||
if (sCurProcIdx >= 0 && sGeneralBuf) {
|
||||
const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);
|
||||
|
||||
if (found) {
|
||||
showProcAndFind(-1, needle, caseSensitive);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Search procs before the current one (wrap within file)
|
||||
for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) {
|
||||
if (!sProcBufs[p]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
|
||||
|
||||
if (found) {
|
||||
showProcAndFind(p, needle, caseSensitive);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next file
|
||||
startFile = (startFile + 1) % sProject.fileCount;
|
||||
}
|
||||
|
||||
// Search remaining files, proc by proc.
|
||||
// startFile was advanced past the current file if we already searched it.
|
||||
int32_t filesToSearch = sProject.fileCount;
|
||||
|
||||
if (sEditor && sEditorFileIdx >= 0) {
|
||||
filesToSearch--; // skip the file we already searched above
|
||||
}
|
||||
|
||||
for (int32_t attempt = 0; attempt < filesToSearch; attempt++) {
|
||||
int32_t fileIdx = (startFile + attempt) % sProject.fileCount;
|
||||
|
||||
// Activate the file to load its proc buffers
|
||||
activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE);
|
||||
|
||||
// Search General section
|
||||
if (sGeneralBuf) {
|
||||
const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);
|
||||
|
||||
if (found) {
|
||||
showProcAndFind(-1, needle, caseSensitive);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Search each procedure
|
||||
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
||||
|
||||
for (int32_t p = 0; p < procCount; p++) {
|
||||
if (!sProcBufs[p]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
|
||||
|
||||
if (found) {
|
||||
showProcAndFind(p, needle, caseSensitive);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static void handleEditCmd(int32_t cmd) {
|
||||
switch (cmd) {
|
||||
case CMD_CUT:
|
||||
|
|
@ -2372,6 +2875,22 @@ static void handleEditCmd(int32_t cmd) {
|
|||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CMD_FIND:
|
||||
openFindDialog(false);
|
||||
break;
|
||||
|
||||
case CMD_FIND_NEXT:
|
||||
if (sFindText[0]) {
|
||||
onFindNext(NULL);
|
||||
} else {
|
||||
openFindDialog(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case CMD_REPLACE:
|
||||
openFindDialog(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2464,7 +2983,9 @@ static void handleWindowCmd(int32_t cmd) {
|
|||
sToolboxWin = tbxCreate(sAc, &sDesigner);
|
||||
|
||||
if (sToolboxWin) {
|
||||
sToolboxWin->y = toolbarBottom();
|
||||
sToolboxWin->y = toolbarBottom();
|
||||
sToolboxWin->onMenu = onMenu;
|
||||
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -2474,7 +2995,9 @@ static void handleWindowCmd(int32_t cmd) {
|
|||
sPropsWin = prpCreate(sAc, &sDesigner);
|
||||
|
||||
if (sPropsWin) {
|
||||
sPropsWin->y = toolbarBottom();
|
||||
sPropsWin->y = toolbarBottom();
|
||||
sPropsWin->onMenu = onMenu;
|
||||
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -3430,7 +3953,9 @@ static void switchToDesign(void) {
|
|||
sToolboxWin = tbxCreate(sAc, &sDesigner);
|
||||
|
||||
if (sToolboxWin) {
|
||||
sToolboxWin->y = toolbarBottom();
|
||||
sToolboxWin->y = toolbarBottom();
|
||||
sToolboxWin->onMenu = onMenu;
|
||||
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3438,7 +3963,9 @@ static void switchToDesign(void) {
|
|||
sPropsWin = prpCreate(sAc, &sDesigner);
|
||||
|
||||
if (sPropsWin) {
|
||||
sPropsWin->y = toolbarBottom();
|
||||
sPropsWin->y = toolbarBottom();
|
||||
sPropsWin->onMenu = onMenu;
|
||||
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3502,11 +4029,15 @@ static void showCodeWindow(void) {
|
|||
WidgetT *dropdownRow = wgtHBox(codeRoot);
|
||||
dropdownRow->spacing = wgtPixels(4);
|
||||
|
||||
wgtLabel(dropdownRow, "Object:");
|
||||
|
||||
sObjDropdown = wgtDropdown(dropdownRow);
|
||||
sObjDropdown->weight = 100;
|
||||
sObjDropdown->onChange = onObjDropdownChange;
|
||||
wgtDropdownSetItems(sObjDropdown, NULL, 0);
|
||||
|
||||
wgtLabel(dropdownRow, "Function:");
|
||||
|
||||
sEvtDropdown = wgtDropdown(dropdownRow);
|
||||
sEvtDropdown->weight = 100;
|
||||
sEvtDropdown->onChange = onEvtDropdownChange;
|
||||
|
|
@ -3523,6 +4054,8 @@ static void showCodeWindow(void) {
|
|||
|
||||
// onChange is set after initial content is loaded by the caller
|
||||
// (navigateToEventSub, onPrjFileClick, etc.) to prevent false dirty marking.
|
||||
|
||||
updateProjectMenuState();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3645,6 +4178,10 @@ static void updateProjectMenuState(void) {
|
|||
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, hasProject);
|
||||
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, hasProject);
|
||||
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, hasProject);
|
||||
|
||||
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);
|
||||
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject);
|
||||
wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -3681,11 +4218,19 @@ static void updateDirtyIndicators(void) {
|
|||
if (sCodeWin) {
|
||||
bool codeDirty = false;
|
||||
|
||||
if (sProject.activeFileIdx >= 0) {
|
||||
const char *codeFile = "";
|
||||
|
||||
if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
|
||||
codeDirty = sProject.files[sEditorFileIdx].modified;
|
||||
codeFile = sProject.files[sEditorFileIdx].path;
|
||||
} else if (sProject.activeFileIdx >= 0) {
|
||||
codeDirty = sProject.files[sProject.activeFileIdx].modified;
|
||||
codeFile = sProject.files[sProject.activeFileIdx].path;
|
||||
}
|
||||
|
||||
dvxSetTitle(sAc, sCodeWin, codeDirty ? "Code *" : "Code");
|
||||
char codeTitle[DVX_MAX_PATH + 16];
|
||||
snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : "");
|
||||
dvxSetTitle(sAc, sCodeWin, codeTitle);
|
||||
}
|
||||
|
||||
// Design window title
|
||||
|
|
|
|||
|
|
@ -2279,10 +2279,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) {
|
|||
ctx->sysMenu.popupX = win->x + CHROME_BORDER_WIDTH;
|
||||
ctx->sysMenu.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT;
|
||||
|
||||
if (win->menuBar) {
|
||||
ctx->sysMenu.popupY += CHROME_MENU_HEIGHT;
|
||||
}
|
||||
|
||||
int32_t maxW = 0;
|
||||
|
||||
for (int32_t i = 0; i < ctx->sysMenu.itemCount; i++) {
|
||||
|
|
|
|||
|
|
@ -1473,20 +1473,11 @@ static void int9Handler(void) {
|
|||
|
||||
|
||||
void platformKeyUpInit(void) {
|
||||
if (sKeyUpInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_go32_dpmi_get_protected_mode_interrupt_vector(9, &sOldInt9);
|
||||
|
||||
sNewInt9.pm_offset = (unsigned long)int9Handler;
|
||||
sNewInt9.pm_selector = _go32_my_cs();
|
||||
|
||||
// Chain: our handler runs first, then DJGPP automatically
|
||||
// calls the original handler via an IRET wrapper.
|
||||
_go32_dpmi_chain_protected_mode_interrupt_vector(9, &sNewInt9);
|
||||
|
||||
sKeyUpInstalled = true;
|
||||
// INT 9 hook disabled pending investigation of keyboard corruption.
|
||||
// Key-up events are not available until this is fixed.
|
||||
(void)sOldInt9;
|
||||
(void)sNewInt9;
|
||||
(void)int9Handler;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ typedef struct {
|
|||
} TextAreaDataT;
|
||||
|
||||
#include <ctype.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
|
||||
#define TEXTAREA_BORDER 2
|
||||
|
|
@ -165,6 +166,8 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3
|
|||
static void widgetTextAreaOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
||||
static int32_t wordBoundaryLeft(const char *buf, int32_t pos);
|
||||
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
|
||||
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
|
||||
int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive);
|
||||
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen);
|
||||
|
||||
// sCursorBlinkOn is defined in widgetCore.c (shared state)
|
||||
|
|
@ -2674,6 +2677,163 @@ void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar) {
|
|||
}
|
||||
|
||||
|
||||
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) {
|
||||
if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TextAreaDataT *ta = (TextAreaDataT *)w->data;
|
||||
int32_t needleLen = strlen(needle);
|
||||
|
||||
if (needleLen > ta->len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current cursor byte position
|
||||
int32_t cursorByte = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
|
||||
|
||||
// Search forward from cursor+1 (or backward from cursor-1).
|
||||
// No wrap-around: returns false if the end/start of the buffer
|
||||
// is reached without finding a match.
|
||||
int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
|
||||
int32_t searchLen = ta->len - needleLen + 1;
|
||||
|
||||
if (searchLen <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t count = forward ? (searchLen - startPos) : (startPos + 1);
|
||||
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t attempt = 0; attempt < count; attempt++) {
|
||||
int32_t pos;
|
||||
|
||||
if (forward) {
|
||||
pos = startPos + attempt;
|
||||
} else {
|
||||
pos = startPos - attempt;
|
||||
}
|
||||
|
||||
if (pos < 0 || pos >= searchLen) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool match;
|
||||
if (caseSensitive) {
|
||||
match = (memcmp(ta->buf + pos, needle, needleLen) == 0);
|
||||
} else {
|
||||
match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
// Select the found text
|
||||
ta->selAnchor = pos;
|
||||
ta->selCursor = pos + needleLen;
|
||||
|
||||
// Move cursor to start of match (forward) or end of match (backward)
|
||||
int32_t cursorOff = forward ? pos : pos + needleLen;
|
||||
textAreaOffToRowCol(ta->buf, cursorOff, &ta->cursorRow, &ta->cursorCol);
|
||||
ta->desiredCol = ta->cursorCol;
|
||||
|
||||
// Scroll to show the match
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
if (ctx) {
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
int32_t gutterW = textAreaGutterWidth(w, font);
|
||||
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
|
||||
int32_t visCols = innerW / font->charWidth;
|
||||
int32_t innerH = w->h - TEXTAREA_BORDER * 2;
|
||||
int32_t visRows = innerH / font->charHeight;
|
||||
if (visCols < 1) { visCols = 1; }
|
||||
if (visRows < 1) { visRows = 1; }
|
||||
textAreaEnsureVisible(w, visRows, visCols);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive) {
|
||||
if (!w || w->type != sTextAreaTypeId || !needle || !needle[0] || !replacement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
TextAreaDataT *ta = (TextAreaDataT *)w->data;
|
||||
int32_t needleLen = strlen(needle);
|
||||
int32_t replLen = strlen(replacement);
|
||||
int32_t delta = replLen - needleLen;
|
||||
int32_t count = 0;
|
||||
|
||||
if (needleLen > ta->len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Save undo before any modifications
|
||||
textEditSaveUndo(ta->buf, ta->len, textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol), ta->undoBuf, &ta->undoLen, &ta->undoCursor, ta->bufSize);
|
||||
|
||||
int32_t pos = 0;
|
||||
while (pos + needleLen <= ta->len) {
|
||||
bool match;
|
||||
if (caseSensitive) {
|
||||
match = (memcmp(ta->buf + pos, needle, needleLen) == 0);
|
||||
} else {
|
||||
match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
// Check if replacement fits in buffer
|
||||
if (ta->len + delta >= ta->bufSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Shift text after match to make room (or close gap)
|
||||
if (delta != 0) {
|
||||
memmove(ta->buf + pos + replLen, ta->buf + pos + needleLen, ta->len - pos - needleLen);
|
||||
}
|
||||
|
||||
// Copy replacement in
|
||||
memcpy(ta->buf + pos, replacement, replLen);
|
||||
ta->len += delta;
|
||||
ta->buf[ta->len] = '\0';
|
||||
count++;
|
||||
pos += replLen;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
// Clear selection
|
||||
ta->selAnchor = -1;
|
||||
ta->selCursor = -1;
|
||||
|
||||
// Clamp cursor if it's past the end
|
||||
int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
|
||||
if (cursorOff > ta->len) {
|
||||
textAreaOffToRowCol(ta->buf, ta->len, &ta->cursorRow, &ta->cursorCol);
|
||||
}
|
||||
|
||||
ta->desiredCol = ta->cursorCol;
|
||||
textAreaDirtyCache(w);
|
||||
wgtInvalidatePaint(w);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// DXE registration
|
||||
// ============================================================
|
||||
|
|
@ -2691,6 +2851,8 @@ static const struct {
|
|||
void (*setCaptureTabs)(WidgetT *w, bool capture);
|
||||
void (*setTabWidth)(WidgetT *w, int32_t width);
|
||||
void (*setUseTabChar)(WidgetT *w, bool useChar);
|
||||
bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
|
||||
int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive);
|
||||
} sApi = {
|
||||
.create = wgtTextInput,
|
||||
.password = wgtPasswordInput,
|
||||
|
|
@ -2702,7 +2864,9 @@ static const struct {
|
|||
.setShowLineNumbers = wgtTextAreaSetShowLineNumbers,
|
||||
.setCaptureTabs = wgtTextAreaSetCaptureTabs,
|
||||
.setTabWidth = wgtTextAreaSetTabWidth,
|
||||
.setUseTabChar = wgtTextAreaSetUseTabChar
|
||||
.setUseTabChar = wgtTextAreaSetUseTabChar,
|
||||
.findNext = wgtTextAreaFindNext,
|
||||
.replaceAll = wgtTextAreaReplaceAll
|
||||
};
|
||||
|
||||
// Per-type APIs for the designer
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ typedef struct {
|
|||
void (*setCaptureTabs)(WidgetT *w, bool capture);
|
||||
void (*setTabWidth)(WidgetT *w, int32_t width);
|
||||
void (*setUseTabChar)(WidgetT *w, bool useChar);
|
||||
bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
|
||||
int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive);
|
||||
} TextInputApiT;
|
||||
|
||||
static inline const TextInputApiT *dvxTextInputApi(void) {
|
||||
|
|
@ -44,5 +46,7 @@ static inline const TextInputApiT *dvxTextInputApi(void) {
|
|||
#define wgtTextAreaSetCaptureTabs(w, capture) dvxTextInputApi()->setCaptureTabs(w, capture)
|
||||
#define wgtTextAreaSetTabWidth(w, width) dvxTextInputApi()->setTabWidth(w, width)
|
||||
#define wgtTextAreaSetUseTabChar(w, useChar) dvxTextInputApi()->setUseTabChar(w, useChar)
|
||||
#define wgtTextAreaFindNext(w, needle, caseSens, fwd) dvxTextInputApi()->findNext(w, needle, caseSens, fwd)
|
||||
#define wgtTextAreaReplaceAll(w, needle, repl, caseSens) dvxTextInputApi()->replaceAll(w, needle, repl, caseSens)
|
||||
|
||||
#endif // WIDGET_TEXTINPUT_H
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue