diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-03-27 14:18:06 +0200 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-03-27 14:18:41 +0200 |
commit | db7f835c320632ec4dea3b8baf5e21b62e2b75e1 (patch) | |
tree | bc0d497c8152b55b04c62fccb98d394ff8cd57fa | |
parent | 2a4f5d0f67fcd1412968ef967ed3009469a46b90 (diff) |
DocumentWidget: Advanced text selection
Double click to select by word, triple click by paragraph.
IssueID #134
-rw-r--r-- | src/gmdocument.c | 4 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 83 | ||||
-rw-r--r-- | src/ui/text.c | 3 | ||||
-rw-r--r-- | src/ui/util.c | 54 | ||||
-rw-r--r-- | src/ui/util.h | 8 |
5 files changed, 126 insertions, 26 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c index 30f5169a..f1471f0f 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c | |||
@@ -239,7 +239,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li | |||
239 | if (link->flags & gemini_GmLinkFlag && ~link->flags & remote_GmLinkFlag) { | 239 | if (link->flags & gemini_GmLinkFlag && ~link->flags & remote_GmLinkFlag) { |
240 | iChar icon = 0; | 240 | iChar icon = 0; |
241 | int len = 0; | 241 | int len = 0; |
242 | if ((len = decodeBytes_MultibyteChar(desc.start, size_Range(&desc), &icon)) > 0) { | 242 | if ((len = decodeBytes_MultibyteChar(desc.start, desc.end, &icon)) > 0) { |
243 | if (desc.start + len < desc.end && | 243 | if (desc.start + len < desc.end && |
244 | (isPictograph_Char(icon) || isEmoji_Char(icon) || icon == 0x2022 /* bullet */) && | 244 | (isPictograph_Char(icon) || isEmoji_Char(icon) || icon == 0x2022 /* bullet */) && |
245 | !isFitzpatrickType_Char(icon)) { | 245 | !isFitzpatrickType_Char(icon)) { |
@@ -1675,7 +1675,7 @@ iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) { | |||
1675 | loc.end = loc.start; | 1675 | loc.end = loc.start; |
1676 | iChar ch; | 1676 | iChar ch; |
1677 | if (d->text.end != loc.start) { | 1677 | if (d->text.end != loc.start) { |
1678 | int chLen = decodeBytes_MultibyteChar(loc.start, d->text.end - loc.start, &ch); | 1678 | int chLen = decodeBytes_MultibyteChar(loc.start, d->text.end, &ch); |
1679 | if (chLen > 0) { | 1679 | if (chLen > 0) { |
1680 | /* End after the character. */ | 1680 | /* End after the character. */ |
1681 | loc.end += chLen; | 1681 | loc.end += chLen; |
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 { | |||
198 | iString * certSubject; | 198 | iString * certSubject; |
199 | int redirectCount; | 199 | int redirectCount; |
200 | iRangecc selectMark; | 200 | iRangecc selectMark; |
201 | iRangecc initialSelectMark; /* for word/line selection */ | ||
201 | iRangecc foundMark; | 202 | iRangecc foundMark; |
202 | int pageMargin; | 203 | int pageMargin; |
203 | iPtrArray visibleLinks; | 204 | iPtrArray visibleLinks; |
@@ -2325,6 +2326,15 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or | |||
2325 | return 0; | 2326 | return 0; |
2326 | } | 2327 | } |
2327 | 2328 | ||
2329 | static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { | ||
2330 | setFocus_Widget(NULL); /* TODO: Focus this document? */ | ||
2331 | invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); | ||
2332 | resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ | ||
2333 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); | ||
2334 | d->selectMark = sourceLoc_DocumentWidget_(d, pos); | ||
2335 | refresh_Widget(as_Widget(d)); | ||
2336 | } | ||
2337 | |||
2328 | static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { | 2338 | static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { |
2329 | iWidget *w = as_Widget(d); | 2339 | iWidget *w = as_Widget(d); |
2330 | if (isMetricsChange_UserEvent(ev)) { | 2340 | if (isMetricsChange_UserEvent(ev)) { |
@@ -2593,19 +2603,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
2593 | return iTrue; | 2603 | return iTrue; |
2594 | } | 2604 | } |
2595 | /* The left mouse button. */ | 2605 | /* The left mouse button. */ |
2596 | if (/*d->flags & selecting_DocumentWidgetFlag &&*/ ev->type == SDL_MOUSEBUTTONDOWN && | ||
2597 | ev->button.button == SDL_BUTTON_LEFT) { | ||
2598 | if (ev->button.clicks == 2) { | ||
2599 | printf("double click\n"); | ||
2600 | } | ||
2601 | else if (ev->button.clicks == 3) { | ||
2602 | printf("triple click\n"); | ||
2603 | } | ||
2604 | fflush(stdout); | ||
2605 | } | ||
2606 | switch (processEvent_Click(&d->click, ev)) { | 2606 | switch (processEvent_Click(&d->click, ev)) { |
2607 | case started_ClickResult: | 2607 | case started_ClickResult: |
2608 | if (d->grabbedPlayer) { | ||
2609 | return iTrue; | ||
2610 | } | ||
2608 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); | 2611 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); |
2612 | iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2); | ||
2613 | iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3); | ||
2614 | /* Double/triple clicks marks the selection immediately. */ | ||
2615 | if (d->click.count >= 2) { | ||
2616 | beginMarkingSelection_DocumentWidget_(d, d->click.startPos); | ||
2617 | extendRange_Rangecc( | ||
2618 | &d->selectMark, | ||
2619 | range_String(source_GmDocument(d->doc)), | ||
2620 | bothStartAndEnd_RangeExtension | | ||
2621 | (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); | ||
2622 | d->initialSelectMark = d->selectMark; | ||
2623 | refresh_Widget(w); | ||
2624 | } | ||
2625 | else { | ||
2626 | d->initialSelectMark = iNullRange; | ||
2627 | } | ||
2609 | return iTrue; | 2628 | return iTrue; |
2610 | case drag_ClickResult: { | 2629 | case drag_ClickResult: { |
2611 | if (d->grabbedPlayer) { | 2630 | if (d->grabbedPlayer) { |
@@ -2620,12 +2639,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
2620 | } | 2639 | } |
2621 | /* Begin selecting a range of text. */ | 2640 | /* Begin selecting a range of text. */ |
2622 | if (~d->flags & selecting_DocumentWidgetFlag) { | 2641 | if (~d->flags & selecting_DocumentWidgetFlag) { |
2623 | setFocus_Widget(NULL); /* TODO: Focus this document? */ | 2642 | beginMarkingSelection_DocumentWidget_(d, d->click.startPos); |
2624 | invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); | ||
2625 | resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ | ||
2626 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); | ||
2627 | d->selectMark = sourceLoc_DocumentWidget_(d, d->click.startPos); | ||
2628 | refresh_Widget(w); | ||
2629 | } | 2643 | } |
2630 | iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); | 2644 | iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); |
2631 | if (!d->selectMark.start) { | 2645 | if (!d->selectMark.start) { |
@@ -2634,6 +2648,23 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
2634 | else if (loc.end) { | 2648 | else if (loc.end) { |
2635 | d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); | 2649 | d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); |
2636 | } | 2650 | } |
2651 | iAssert((!d->selectMark.start && !d->selectMark.end) || | ||
2652 | ( d->selectMark.start && d->selectMark.end)); | ||
2653 | /* Extend the selection when double/triple clicking. */ | ||
2654 | if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { | ||
2655 | extendRange_Rangecc( | ||
2656 | &d->selectMark, | ||
2657 | range_String(source_GmDocument(d->doc)), | ||
2658 | d->click.count == 2 ? word_RangeExtension : line_RangeExtension); | ||
2659 | if (!isEmpty_Range(&d->initialSelectMark)) { | ||
2660 | if (d->selectMark.end > d->selectMark.start) { | ||
2661 | d->selectMark.start = d->initialSelectMark.start; | ||
2662 | } | ||
2663 | else if (d->selectMark.end < d->selectMark.start) { | ||
2664 | d->selectMark.start = d->initialSelectMark.end; | ||
2665 | } | ||
2666 | } | ||
2667 | } | ||
2637 | // printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)), | 2668 | // printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)), |
2638 | // d->selectMark.end - cstr_String(source_GmDocument(d->doc))); | 2669 | // d->selectMark.end - cstr_String(source_GmDocument(d->doc))); |
2639 | // fflush(stdout); | 2670 | // fflush(stdout); |
@@ -2727,7 +2758,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
2727 | 2); | 2758 | 2); |
2728 | } | 2759 | } |
2729 | } | 2760 | } |
2730 | if (d->selectMark.start) { | 2761 | if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag | |
2762 | selectWords_DocumentWidgetFlag))) { | ||
2731 | d->selectMark = iNullRange; | 2763 | d->selectMark = iNullRange; |
2732 | refresh_Widget(w); | 2764 | refresh_Widget(w); |
2733 | } | 2765 | } |
@@ -2776,16 +2808,21 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol | |||
2776 | /* Selection may be done in either direction. */ | 2808 | /* Selection may be done in either direction. */ |
2777 | iSwap(const char *, mark.start, mark.end); | 2809 | iSwap(const char *, mark.start, mark.end); |
2778 | } | 2810 | } |
2779 | if ((!*isInside && (contains_Range(&run->text, mark.start) || mark.start == run->text.end)) || | 2811 | if (*isInside || (contains_Range(&run->text, mark.start) || |
2780 | *isInside) { | 2812 | contains_Range(&mark, run->text.start))) { |
2781 | int x = 0; | 2813 | int x = 0; |
2782 | if (!*isInside) { | 2814 | if (!*isInside) { |
2783 | x = advanceRange_Text(run->font, (iRangecc){ run->text.start, mark.start }).x; | 2815 | x = advanceRange_Text(run->font, |
2816 | (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) | ||
2817 | .x; | ||
2784 | } | 2818 | } |
2785 | int w = width_Rect(run->visBounds) - x; | 2819 | int w = width_Rect(run->visBounds) - x; |
2786 | if (contains_Range(&run->text, mark.end) || run->text.end == mark.end) { | 2820 | if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { |
2787 | w = advanceRange_Text(run->font, | 2821 | w = advanceRange_Text( |
2788 | !*isInside ? mark : (iRangecc){ run->text.start, mark.end }).x; | 2822 | run->font, |
2823 | !*isInside ? mark | ||
2824 | : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }) | ||
2825 | .x; | ||
2789 | *isInside = iFalse; | 2826 | *isInside = iFalse; |
2790 | } | 2827 | } |
2791 | else { | 2828 | 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) { | |||
649 | return 0; | 649 | return 0; |
650 | } | 650 | } |
651 | iChar ch; | 651 | iChar ch; |
652 | int len = decodeBytes_MultibyteChar(*chPos, end - *chPos, &ch); | 652 | int len = decodeBytes_MultibyteChar(*chPos, end, &ch); |
653 | if (len <= 0) { | 653 | if (len <= 0) { |
654 | (*chPos)++; /* skip it */ | 654 | (*chPos)++; /* skip it */ |
655 | return 0; | 655 | return 0; |
@@ -862,6 +862,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
862 | const enum iRunMode mode = args->mode; | 862 | const enum iRunMode mode = args->mode; |
863 | const char * lastWordEnd = args->text.start; | 863 | const char * lastWordEnd = args->text.start; |
864 | iAssert(args->xposLimit == 0 || isMeasuring_(mode)); | 864 | iAssert(args->xposLimit == 0 || isMeasuring_(mode)); |
865 | iAssert(args->text.end >= args->text.start); | ||
865 | if (args->continueFrom_out) { | 866 | if (args->continueFrom_out) { |
866 | *args->continueFrom_out = args->text.end; | 867 | *args->continueFrom_out = args->text.end; |
867 | } | 868 | } |
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) { | |||
164 | return (iRangei){ iMin(a.start, b.start), iMax(a.end, b.end) }; | 164 | return (iRangei){ iMin(a.start, b.start), iMax(a.end, b.end) }; |
165 | } | 165 | } |
166 | 166 | ||
167 | static iBool isSelectionBreakingChar_(iChar c) { | ||
168 | return isSpace_Char(c) || (c == '@' || c == '-' || c == '/' || c == '\\' || c == ','); | ||
169 | } | ||
170 | |||
171 | static const char *moveBackward_(const char *pos, iRangecc bounds, int mode) { | ||
172 | iChar ch; | ||
173 | while (pos > bounds.start) { | ||
174 | int len = decodePrecedingBytes_MultibyteChar(pos, bounds.start, &ch); | ||
175 | if (len > 0) { | ||
176 | if (mode & word_RangeExtension && isSelectionBreakingChar_(ch)) break; | ||
177 | if (mode & line_RangeExtension && ch == '\n') break; | ||
178 | pos -= len; | ||
179 | } | ||
180 | else break; | ||
181 | } | ||
182 | return pos; | ||
183 | } | ||
184 | |||
185 | static const char *moveForward_(const char *pos, iRangecc bounds, int mode) { | ||
186 | iChar ch; | ||
187 | while (pos < bounds.end) { | ||
188 | int len = decodeBytes_MultibyteChar(pos, bounds.end, &ch); | ||
189 | if (len > 0) { | ||
190 | if (mode & word_RangeExtension && isSelectionBreakingChar_(ch)) break; | ||
191 | if (mode & line_RangeExtension && ch == '\n') break; | ||
192 | pos += len; | ||
193 | } | ||
194 | else break; | ||
195 | } | ||
196 | return pos; | ||
197 | } | ||
198 | |||
199 | void extendRange_Rangecc(iRangecc *d, iRangecc bounds, int mode) { | ||
200 | if (!d->start) return; | ||
201 | if (d->end >= d->start) { | ||
202 | if (mode & bothStartAndEnd_RangeExtension) { | ||
203 | d->start = moveBackward_(d->start, bounds, mode); | ||
204 | d->end = moveForward_(d->end, bounds, mode); | ||
205 | } | ||
206 | else { | ||
207 | d->end = moveForward_(d->end, bounds, mode); | ||
208 | } | ||
209 | } | ||
210 | else { | ||
211 | if (mode & bothStartAndEnd_RangeExtension) { | ||
212 | d->start = moveForward_(d->start, bounds, mode); | ||
213 | d->end = moveBackward_(d->end, bounds, mode); | ||
214 | } | ||
215 | else { | ||
216 | d->end = moveBackward_(d->end, bounds, mode); | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | |||
167 | /*----------------------------------------------------------------------------------------------*/ | 221 | /*----------------------------------------------------------------------------------------------*/ |
168 | 222 | ||
169 | iBool isFinished_Anim(const iAnim *d) { | 223 | 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) { | |||
82 | return !isEmpty_Rangei(intersect_Rangei(a, b)); | 82 | return !isEmpty_Rangei(intersect_Rangei(a, b)); |
83 | } | 83 | } |
84 | 84 | ||
85 | enum iRangeExtension { | ||
86 | word_RangeExtension = iBit(1), | ||
87 | line_RangeExtension = iBit(2), | ||
88 | bothStartAndEnd_RangeExtension = iBit(3), | ||
89 | }; | ||
90 | |||
91 | void extendRange_Rangecc (iRangecc *, iRangecc bounds, int mode); | ||
92 | |||
85 | /*-----------------------------------------------------------------------------------------------*/ | 93 | /*-----------------------------------------------------------------------------------------------*/ |
86 | 94 | ||
87 | iDeclareType(Anim) | 95 | iDeclareType(Anim) |