From db7f835c320632ec4dea3b8baf5e21b62e2b75e1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 27 Mar 2021 14:18:06 +0200 Subject: DocumentWidget: Advanced text selection Double click to select by word, triple click by paragraph. IssueID #134 --- src/ui/documentwidget.c | 83 +++++++++++++++++++++++++++++++++++-------------- src/ui/text.c | 3 +- src/ui/util.c | 54 ++++++++++++++++++++++++++++++++ src/ui/util.h | 8 +++++ 4 files changed, 124 insertions(+), 24 deletions(-) (limited to 'src/ui') diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index b3508b8d..6bb16a93 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -198,6 +198,7 @@ struct Impl_DocumentWidget { iString * certSubject; int redirectCount; iRangecc selectMark; + iRangecc initialSelectMark; /* for word/line selection */ iRangecc foundMark; int pageMargin; iPtrArray visibleLinks; @@ -2325,6 +2326,15 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or return 0; } +static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { + setFocus_Widget(NULL); /* TODO: Focus this document? */ + invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); + resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); + d->selectMark = sourceLoc_DocumentWidget_(d, pos); + refresh_Widget(as_Widget(d)); +} + static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); if (isMetricsChange_UserEvent(ev)) { @@ -2593,19 +2603,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e return iTrue; } /* The left mouse button. */ - if (/*d->flags & selecting_DocumentWidgetFlag &&*/ ev->type == SDL_MOUSEBUTTONDOWN && - ev->button.button == SDL_BUTTON_LEFT) { - if (ev->button.clicks == 2) { - printf("double click\n"); - } - else if (ev->button.clicks == 3) { - printf("triple click\n"); - } - fflush(stdout); - } switch (processEvent_Click(&d->click, ev)) { case started_ClickResult: + if (d->grabbedPlayer) { + return iTrue; + } iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); + iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2); + iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3); + /* Double/triple clicks marks the selection immediately. */ + if (d->click.count >= 2) { + beginMarkingSelection_DocumentWidget_(d, d->click.startPos); + extendRange_Rangecc( + &d->selectMark, + range_String(source_GmDocument(d->doc)), + bothStartAndEnd_RangeExtension | + (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); + d->initialSelectMark = d->selectMark; + refresh_Widget(w); + } + else { + d->initialSelectMark = iNullRange; + } return iTrue; case drag_ClickResult: { if (d->grabbedPlayer) { @@ -2620,12 +2639,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } /* Begin selecting a range of text. */ if (~d->flags & selecting_DocumentWidgetFlag) { - setFocus_Widget(NULL); /* TODO: Focus this document? */ - invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); - resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ - iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); - d->selectMark = sourceLoc_DocumentWidget_(d, d->click.startPos); - refresh_Widget(w); + beginMarkingSelection_DocumentWidget_(d, d->click.startPos); } iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); if (!d->selectMark.start) { @@ -2634,6 +2648,23 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e else if (loc.end) { d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); } + iAssert((!d->selectMark.start && !d->selectMark.end) || + ( d->selectMark.start && d->selectMark.end)); + /* Extend the selection when double/triple clicking. */ + if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { + extendRange_Rangecc( + &d->selectMark, + range_String(source_GmDocument(d->doc)), + d->click.count == 2 ? word_RangeExtension : line_RangeExtension); + if (!isEmpty_Range(&d->initialSelectMark)) { + if (d->selectMark.end > d->selectMark.start) { + d->selectMark.start = d->initialSelectMark.start; + } + else if (d->selectMark.end < d->selectMark.start) { + d->selectMark.start = d->initialSelectMark.end; + } + } + } // printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)), // d->selectMark.end - cstr_String(source_GmDocument(d->doc))); // fflush(stdout); @@ -2727,7 +2758,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e 2); } } - if (d->selectMark.start) { + if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag | + selectWords_DocumentWidgetFlag))) { d->selectMark = iNullRange; refresh_Widget(w); } @@ -2776,16 +2808,21 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol /* Selection may be done in either direction. */ iSwap(const char *, mark.start, mark.end); } - if ((!*isInside && (contains_Range(&run->text, mark.start) || mark.start == run->text.end)) || - *isInside) { + if (*isInside || (contains_Range(&run->text, mark.start) || + contains_Range(&mark, run->text.start))) { int x = 0; if (!*isInside) { - x = advanceRange_Text(run->font, (iRangecc){ run->text.start, mark.start }).x; + x = advanceRange_Text(run->font, + (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) + .x; } int w = width_Rect(run->visBounds) - x; - if (contains_Range(&run->text, mark.end) || run->text.end == mark.end) { - w = advanceRange_Text(run->font, - !*isInside ? mark : (iRangecc){ run->text.start, mark.end }).x; + if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { + w = advanceRange_Text( + run->font, + !*isInside ? mark + : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }) + .x; *isInside = iFalse; } else { diff --git a/src/ui/text.c b/src/ui/text.c index 80d3634c..6aaf40f5 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -649,7 +649,7 @@ static iChar nextChar_(const char **chPos, const char *end) { return 0; } iChar ch; - int len = decodeBytes_MultibyteChar(*chPos, end - *chPos, &ch); + int len = decodeBytes_MultibyteChar(*chPos, end, &ch); if (len <= 0) { (*chPos)++; /* skip it */ return 0; @@ -862,6 +862,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { const enum iRunMode mode = args->mode; const char * lastWordEnd = args->text.start; iAssert(args->xposLimit == 0 || isMeasuring_(mode)); + iAssert(args->text.end >= args->text.start); if (args->continueFrom_out) { *args->continueFrom_out = args->text.end; } diff --git a/src/ui/util.c b/src/ui/util.c index cb9006f6..8074223b 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -164,6 +164,60 @@ iRangei union_Rangei(iRangei a, iRangei b) { return (iRangei){ iMin(a.start, b.start), iMax(a.end, b.end) }; } +static iBool isSelectionBreakingChar_(iChar c) { + return isSpace_Char(c) || (c == '@' || c == '-' || c == '/' || c == '\\' || c == ','); +} + +static const char *moveBackward_(const char *pos, iRangecc bounds, int mode) { + iChar ch; + while (pos > bounds.start) { + int len = decodePrecedingBytes_MultibyteChar(pos, bounds.start, &ch); + if (len > 0) { + if (mode & word_RangeExtension && isSelectionBreakingChar_(ch)) break; + if (mode & line_RangeExtension && ch == '\n') break; + pos -= len; + } + else break; + } + return pos; +} + +static const char *moveForward_(const char *pos, iRangecc bounds, int mode) { + iChar ch; + while (pos < bounds.end) { + int len = decodeBytes_MultibyteChar(pos, bounds.end, &ch); + if (len > 0) { + if (mode & word_RangeExtension && isSelectionBreakingChar_(ch)) break; + if (mode & line_RangeExtension && ch == '\n') break; + pos += len; + } + else break; + } + return pos; +} + +void extendRange_Rangecc(iRangecc *d, iRangecc bounds, int mode) { + if (!d->start) return; + if (d->end >= d->start) { + if (mode & bothStartAndEnd_RangeExtension) { + d->start = moveBackward_(d->start, bounds, mode); + d->end = moveForward_(d->end, bounds, mode); + } + else { + d->end = moveForward_(d->end, bounds, mode); + } + } + else { + if (mode & bothStartAndEnd_RangeExtension) { + d->start = moveForward_(d->start, bounds, mode); + d->end = moveBackward_(d->end, bounds, mode); + } + else { + d->end = moveBackward_(d->end, bounds, mode); + } + } +} + /*----------------------------------------------------------------------------------------------*/ iBool isFinished_Anim(const iAnim *d) { diff --git a/src/ui/util.h b/src/ui/util.h index da4d3a99..9e00e495 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -82,6 +82,14 @@ iLocalDef iBool isOverlapping_Rangei(iRangei a, iRangei b) { return !isEmpty_Rangei(intersect_Rangei(a, b)); } +enum iRangeExtension { + word_RangeExtension = iBit(1), + line_RangeExtension = iBit(2), + bothStartAndEnd_RangeExtension = iBit(3), +}; + +void extendRange_Rangecc (iRangecc *, iRangecc bounds, int mode); + /*-----------------------------------------------------------------------------------------------*/ iDeclareType(Anim) -- cgit v1.2.3