Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
TextInteractor.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <cctype>
7#include <set>
8
13#include "Stroika/Frameworks/Led/Command.h"
14#include "Stroika/Frameworks/Led/Config.h"
15#include "Stroika/Frameworks/Led/IdleManager.h"
16#include "Stroika/Frameworks/Led/Marker.h"
17#include "Stroika/Frameworks/Led/TextInteractor.h"
18#include "Stroika/Frameworks/Led/TextStore.h"
19
20using namespace Stroika::Foundation;
22
23using namespace Stroika::Frameworks;
24using namespace Stroika::Frameworks::Led;
25
26#if qStroika_Frameworks_Led_SupportGDI
27using SavedTextRep = InteractiveReplaceCommand::SavedTextRep;
28
29namespace {
30 class FlavorSavorTextRep : public SavedTextRep {
31 private:
32 using inherited = SavedTextRep;
33
34 public:
35 FlavorSavorTextRep (TextInteractor* interactor, size_t regionStart, size_t regionEnd, size_t selStart, size_t selEnd)
36 : inherited (selStart, selEnd)
37 , fSavedText ()
38 , fTextLength (regionEnd - regionStart)
39 {
40#if !qFailToLookupFunctionNameWhenCompilingFunctionLocalClassMethodCompilerBug
41 RequireNotNull (interactor);
42#endif
43 interactor->GetExternalizer ()->ExternalizeBestFlavor (fSavedText, regionStart, regionEnd);
44 }
45 virtual size_t GetLength () const override
46 {
47 return fTextLength;
48 }
49 virtual void InsertSelf (TextInteractor* interactor, size_t at, size_t nBytesToOverwrite) override
50 {
51#if !qFailToLookupFunctionNameWhenCompilingFunctionLocalClassMethodCompilerBug
52 RequireNotNull (interactor);
53#endif
54 interactor->GetInternalizer ()->InternalizeBestFlavor (fSavedText, at, at + nBytesToOverwrite);
55 }
56
57 private:
58 size_t fTextLength;
60 };
61}
62
63namespace {
64 // Only these chars count as whitespce for smart cut/n/paste
65 inline bool IsSmartSpace (Led_tChar c)
66 {
67 return (c == ' ' or c == '\t');
68 }
69 inline bool IsShouldBeSepWithWhitespaceWordChar (Led_tChar c)
70 {
71 // iswalnum vectors to GetStringTypeW (CT_CTYPE1) and then checks for C1_ALPHA, which
72 // matches all alphabetic, numeric, AND Idiogram characters. I THINK the right thing to use
73 // is to directly call GetStringTypeW (CT_CTYPE2, and check for C2_SEGMENTSEPARATOR, or something
74 // like that (will have to read up on UNICODE more, and/or experiemnet). Anyhow - none of that will happen
75 // for this release, and this should be good enuf to prevent smart-copy-paste from happing with
76 // idiogram characters
77 return Character (c).IsAlphaNumeric () and c < 127;
78 }
79}
80
81namespace {
82 class MyCallback : public TextInteractor::DialogSupport::SpellCheckDialogCallback {
83 private:
84 using inherited = TextInteractor::DialogSupport::SpellCheckDialogCallback;
85
86 public:
87 MyCallback (TextInteractor& ti)
88 : fTI{ti}
89 , fIgnoredWords{}
90 {
91 }
92
93 public:
94 DISABLE_COMPILER_MSC_WARNING_START (6262)
95 virtual MisspellingInfo* GetNextMisspelling () override
96 {
97 SpellCheckEngine* sce = fTI.GetSpellCheckEngine ();
98 if (sce != nullptr) {
99 Led_tChar charBuf[10 * 1024]; // buffer size doesn't matter much - but just has to be larger than the largest undef word we ever want to find...
100 bool firstTry = true;
101 size_t startRegion = fTI.GetSelectionEnd ();
102 SecondTry: {
103 // regardless of the startRegion - back up the search to the start of the interesected word. The only
104 // exception is if the size of our charBuf isn't big enough to go past the startRegion position (cuz
105 // then we'd be going backwards, and risk never moving forwards, in case of a large word)
106 size_t wordStart = 0;
107 size_t wordEnd = 0;
108 bool wordReal = false;
109 fTI.GetTextStore ().FindWordBreaks (startRegion, &wordStart, &wordEnd, &wordReal, sce->PeekAtTextBreaksUsed ());
110 if (wordReal and wordStart + Memory::NEltsOf (charBuf) > startRegion) {
111 startRegion = wordStart;
112 }
113 }
114 size_t endRegion = min (startRegion + Memory::NEltsOf (charBuf), fTI.GetEnd ());
115 fTI.CopyOut (startRegion, endRegion - startRegion, charBuf);
116 const Led_tChar* cursor = nullptr;
117 const Led_tChar* wordStart = nullptr;
118 const Led_tChar* wordEnd = nullptr;
119 if (sce->ScanForUndefinedWord (charBuf, charBuf + (endRegion - startRegion), &cursor, &wordStart, &wordEnd)) {
120 /*
121 * If the undefined word ends at the end of the buffer region we looked at - it might be an artifact of our
122 * chunking. Retry the word (being careful about the special case where the 'word' is as big as our buffer).
123 */
124 if (wordStart != charBuf and wordEnd == charBuf + (endRegion - startRegion)) {
125 startRegion += (wordStart - charBuf); // advance to the start of this word and try again
126 goto SecondTry;
127 }
128
129 Led_tString undefinedWord = Led_tString{wordStart, wordEnd};
130 if (fIgnoredWords.find (undefinedWord) != fIgnoredWords.end ()) {
131 // push startRegion a bit forward over this word, and try again, but don't
132 // set 'second try' flag cuz we haven't wrapped at the end of the document...
133 startRegion += (wordEnd - charBuf);
134 goto SecondTry;
135 }
136 MisspellingInfo* mi = new MisspellingInfo ();
137 mi->fUndefinedWord = undefinedWord;
138 mi->fSuggestions = sce->GenerateSuggestions (mi->fUndefinedWord);
139 size_t selStart = startRegion + wordStart - charBuf;
140 size_t selEnd = selStart + (wordEnd - wordStart);
141 fTI.SetSelection (selStart, selEnd);
142 fTI.ScrollToSelection ();
143 return mi;
144 }
145 else if (endRegion < fTI.GetEnd ()) {
146 // no undefined words in that region, so try the next chunk. Stopping at word boundaries taken
147 // care of with logic already above (for first case)
148 startRegion = endRegion;
149 goto SecondTry;
150 }
151 else if (firstTry) {
152 // Wrap around...
153 startRegion = 0;
154 firstTry = false;
155 goto SecondTry;
156 }
157 }
158 return nullptr;
159 }
160 DISABLE_COMPILER_MSC_WARNING_END (6262)
161 virtual void DoIgnore () override
162 {
163 fTI.SetSelection (fTI.GetSelectionEnd (), fTI.GetSelectionEnd ());
164 fTI.ScrollToSelection ();
165 }
166 virtual void DoIgnoreAll () override
167 {
168 {
169 size_t origSelStart = fTI.GetSelectionStart ();
170 size_t origSelEnd = fTI.GetSelectionEnd ();
171 Memory::StackBuffer<Led_tChar> text{Memory::eUninitialized, origSelEnd - origSelStart + 1};
172 fTI.CopyOut (origSelStart, origSelEnd - origSelStart, text.data ());
173 Led_tString ignoredWord = Led_tString{text.data (), origSelEnd - origSelStart};
174 fIgnoredWords.insert (ignoredWord);
175 }
176 DoIgnore ();
177 }
178 virtual void DoChange (const Led_tString& changeTo) override
179 {
180 size_t origSelStart = fTI.GetSelectionStart ();
181 size_t origSelEnd = fTI.GetSelectionEnd ();
182 TextInteractor::SearchParameters sp;
183 {
184 Memory::StackBuffer<Led_tChar> text{Memory::eUninitialized, origSelEnd - origSelStart + 1};
185 fTI.CopyOut (origSelStart, origSelEnd - origSelStart, text.data ());
186 sp.fMatchString = Led_tString{text.data (), origSelEnd - origSelStart};
187 }
188 fTI.SetSelection (origSelStart, origSelStart); // cuz OnDoReplaceCommand () looks from selectionEND
189 fTI.OnDoReplaceCommand (sp, changeTo);
190 }
191 virtual void DoChangeAll (const Led_tString& changeTo) override
192 {
193 size_t origSelStart = fTI.GetSelectionStart ();
194 size_t origSelEnd = fTI.GetSelectionEnd ();
195 TextInteractor::SearchParameters sp;
196 {
197 Memory::StackBuffer<Led_tChar> text{Memory::eUninitialized, origSelEnd - origSelStart + 1};
198 fTI.CopyOut (origSelStart, origSelEnd - origSelStart, text.data ());
199 sp.fMatchString = Led_tString{text.data (), origSelEnd - origSelStart};
200 }
201 fTI.OnDoReplaceAllCommand (sp, changeTo);
202 }
203 virtual bool AddToDictionaryEnabled () const override
204 {
205 SpellCheckEngine* sce = fTI.GetSpellCheckEngine ();
206 if (sce != nullptr) {
207 SpellCheckEngine::UDInterface* udi = sce->GetUDInterface ();
208 if (udi != nullptr) {
209 return udi->AddWordToUserDictionarySupported ();
210 }
211 }
212 return false;
213 }
214 virtual void AddToDictionary (const Led_tString& newWord) override
215 {
216 SpellCheckEngine* sce = fTI.GetSpellCheckEngine ();
217 if (sce != nullptr) {
218 SpellCheckEngine::UDInterface* udi = sce->GetUDInterface ();
219 if (udi != nullptr) {
220 udi->AddWordToUserDictionary (newWord);
221 }
222 }
223
224 fTI.SetSelection (fTI.GetSelectionEnd (), fTI.GetSelectionEnd ());
225 fTI.ScrollToSelection ();
226 }
227 virtual void LookupOnWeb (const Led_tString& word) override
228 {
229 const char kURLBase[] = "http://dictionary.reference.com/search?q=";
230 Led_URLManager::Get ().Open (kURLBase + Led_tString2ANSIString (word));
231 }
232 virtual bool OptionsDialogEnabled () const override
233 {
234 // cuz no implementation of OptionsDialog () callback...
235 return false;
236 }
237 virtual void OptionsDialog () override
238 {
239 // NYI. When enabled - change result of OptionsDialogEnabled ()
240 }
241
242 private:
243 TextInteractor& fTI;
244 set<Led_tString> fIgnoredWords; // note we intentionally dont keep this around so that the
245 // ignored words list stays around (lives) as long as the dialog...
246 };
247}
248
249/*
250 ********************************************************************************
251 ************************* TextInteractor::DialogSupport ************************
252 ********************************************************************************
253 */
254void TextInteractor::DialogSupport::DisplayFindDialog (Led_tString* /*findText*/, const vector<Led_tString>& /*recentFindSuggestions*/,
255 bool* /*wrapSearch*/, bool* /*wholeWordSearch*/, bool* /*caseSensative*/, bool* /*pressedOK*/)
256{
257 /*
258 * You may want to use code something like this in your OVERRIDE:
259 *
260 Led_StdDialogHelper_FindDialog findDialog (::AfxGetResourceHandle (), nullptr);
261
262 findDialog.fFindText = *findText;
263 findDialog.fRecentFindTextStrings = recentFindSuggestions;
264 findDialog.fWrapSearch = *wrapSearch;
265 findDialog.fWholeWordSearch = *wholeWordSearch;
266 findDialog.fCaseSensativeSearch = *caseSensative;
267
268 findDialog.DoModal ();
269
270 *findText = findDialog.fFindText;
271 *wrapSearch = findDialog.fWrapSearch;
272 *wholeWordSearch = findDialog.fWholeWordSearch;
273 *caseSensative = findDialog.fCaseSensativeSearch;
274 *pressOK = findDialog.fPressedOK;
275 */
276 Assert (false); // to use this - you must OVERRIDE this routine, and provide your own implementation, perhaps akin to the above.
277}
278
279TextInteractor::DialogSupport::ReplaceButtonPressed
280TextInteractor::DialogSupport::DisplayReplaceDialog (Led_tString* /*findText*/, const vector<Led_tString>& /*recentFindSuggestions*/,
281 Led_tString* /*replaceText*/, bool* /*wrapSearch*/, bool* /*wholeWordSearch*/, bool* /*caseSensative*/)
282{
283 Assert (false); // to use this - you must OVERRIDE this routine, and provide your own implementation, perhaps akin to the above.
284 return eReplaceButton_Cancel;
285}
286
287void TextInteractor::DialogSupport::DisplaySpellCheckDialog (SpellCheckDialogCallback& /*callback*/)
288{
289 Assert (false); // to use this - you must OVERRIDE this routine, and provide your own implementation, perhaps akin to the above.
290}
291
292/*
293 ********************************************************************************
294 ******************** TextInteractor::UndoableContextHelper *********************
295 ********************************************************************************
296 */
297TextInteractor::UndoableContextHelper::UndoableContextHelper (TextInteractor& ti, const SDKString& cmdName, bool allowSmartCNPExpansion)
298 : fSimplePlainTextInsertOptimization (false)
299 , fTextInteractor (ti)
300 , fCmdName (cmdName)
301 , fSelStart (ti.GetSelectionStart ())
302 , fSelEnd (ti.GetSelectionEnd ())
303 , fBefore (nullptr)
304 , fCommandComplete (false)
305{
306 if (allowSmartCNPExpansion) {
307 ti.OptionallyExpandSelectionForSmartCutAndPasteModeDeletes (&fSelStart, &fSelEnd);
308 }
309 if (ti.GetCommandHandler () != nullptr) {
310 ti.PreInteractiveUndoHelper (&fBefore, fSelStart, fSelEnd, ti.GetSelectionStart (), ti.GetSelectionEnd ());
311 }
312}
313
314TextInteractor::UndoableContextHelper::UndoableContextHelper (TextInteractor& ti, const SDKString& cmdName, size_t regionAndSelStart,
315 size_t regionAndSelEnd, bool allowSmartCNPExpansion)
316 : fSimplePlainTextInsertOptimization (false)
317 , fTextInteractor (ti)
318 , fCmdName (cmdName)
319 , fSelStart (regionAndSelStart)
320 , fSelEnd (regionAndSelEnd)
321 , fBefore (nullptr)
322 , fCommandComplete (false)
323{
324 if (allowSmartCNPExpansion) {
325 ti.OptionallyExpandSelectionForSmartCutAndPasteModeDeletes (&fSelStart, &fSelEnd);
326 }
327 if (ti.GetCommandHandler () != nullptr) {
328 ti.PreInteractiveUndoHelper (&fBefore, fSelStart, fSelEnd, regionAndSelStart, regionAndSelEnd);
329 }
330}
331
332TextInteractor::UndoableContextHelper::UndoableContextHelper (TextInteractor& ti, const SDKString& cmdName, size_t regionStart,
333 size_t regionEnd, size_t selStart, size_t selEnd, bool allowSmartCNPExpansion)
334 : fSimplePlainTextInsertOptimization (false)
335 , fTextInteractor (ti)
336 , fCmdName (cmdName)
337 , fSelStart (regionStart)
338 , fSelEnd (regionEnd)
339 , fBefore (nullptr)
340 , fCommandComplete (false)
341{
342 if (allowSmartCNPExpansion) {
343 ti.OptionallyExpandSelectionForSmartCutAndPasteModeDeletes (&fSelStart, &fSelEnd);
344 }
345 if (ti.GetCommandHandler () != nullptr) {
346 ti.PreInteractiveUndoHelper (&fBefore, fSelStart, fSelEnd, selStart, selEnd);
347 }
348}
349
350TextInteractor::UndoableContextHelper::~UndoableContextHelper ()
351{
352 if (not fCommandComplete) {
353 delete fBefore;
354 }
355}
356
357/*
358@METHOD: TextInteractor::UndoableContextHelper::CommandComplete
359@ACCESS: public
360@DESCRIPTION: <p>There are two overloaded versions of this function. Both require knowledge of
361 the END of the updated (inserted) region. The one with no arguments assumes the selectionEnd
362 is the end of the inserted region. The second overload allows you to explicitly specify the
363 end of the inserted region.
364 </p>
365 <p>Call this function when a command has been completed (See the @'TextInteractor::UndoableContextHelper' docs
366 for how to structure the CTOR/CommandComplete calls). Failure to call this after constructing a UndoableContextHelper
367 object results in the command not being recorded (as is appropriate if there is an exception thrown and the command
368 is not performed).
369 </p>
370*/
371void TextInteractor::UndoableContextHelper::CommandComplete ()
372{
373 fTextInteractor.ScrollToSelection ();
374 if (fTextInteractor.GetCommandHandler () != nullptr) {
375 if (GetSimplePlainTextInsertOptimization ()) {
376 fTextInteractor.PostInteractiveSimpleCharInsertUndoHelper (&fBefore, fSelStart, fTextInteractor.GetSelectionEnd (), fCmdName);
377 }
378 else {
379 fTextInteractor.PostInteractiveUndoHelper (&fBefore, fSelStart, fTextInteractor.GetSelectionEnd (), fCmdName);
380 }
381 }
382 fCommandComplete = true;
383}
384
385void TextInteractor::UndoableContextHelper::CommandComplete (size_t endOfInsert)
386{
387 fTextInteractor.ScrollToSelection ();
388 if (fTextInteractor.GetCommandHandler () != nullptr) {
389 if (GetSimplePlainTextInsertOptimization ()) {
390 fTextInteractor.PostInteractiveSimpleCharInsertUndoHelper (&fBefore, fSelStart, endOfInsert, fCmdName);
391 }
392 else {
393 fTextInteractor.PostInteractiveUndoHelper (&fBefore, fSelStart, endOfInsert, fCmdName);
394 }
395 }
396 fCommandComplete = true;
397}
398
399/*
400 ********************************************************************************
401 ************************* TextInteractor::PreReplaceInfo ***********************
402 ********************************************************************************
403 */
404TextInteractor::PreReplaceInfo::PreReplaceInfo ()
405 : fTextInteractor (nullptr)
406 , fUpdateMode (TextInteractor::eNoUpdate)
407 , fFrom (0)
408 , fTo (0)
409 , fWithWhatCharCount (0)
410 , fBoundingUpdateMarker ()
411 , fBoundingUpdateHeight (0)
412 , fStableTypingRegionHeight (0)
413{
414}
415
416TextInteractor::PreReplaceInfo::~PreReplaceInfo ()
417{
418 if (fTextInteractor != nullptr) {
419 fTextInteractor->GetTextStore ().RemoveMarker (&fBoundingUpdateMarker);
420 fTextInteractor = nullptr;
421 }
422}
423
424TextInteractor::UpdateMode TextInteractor::PreReplaceInfo::GetUpdateMode () const
425{
426 return fUpdateMode;
427}
428
429size_t TextInteractor::PreReplaceInfo::GetFrom () const
430{
431 return fFrom;
432}
433
434size_t TextInteractor::PreReplaceInfo::GetTo () const
435{
436 return fTo;
437}
438
439/*
440 ********************************************************************************
441 ******************************** TextInteractor ********************************
442 ********************************************************************************
443 */
444
445TextInteractor::CommandNames TextInteractor::sCommandNames = TextInteractor::MakeDefaultCommandNames ();
446TextInteractor::DialogSupport* TextInteractor::sDialogSupport = nullptr;
447TextInteractor::SearchParameters TextInteractor::sSearchParameters;
448TextInteractor::ReplaceParameters TextInteractor::sReplaceParameters;
449
450TextInteractor::TextInteractor ()
451 : fCommandHandler (nullptr)
452 , fSpellCheckEngine (nullptr)
453 , fSuppressCommandBreaksContext (false)
454 , fDefaultUpdateMode (eDelayedUpdate)
455 , fSmartCutAndPasteMode (true)
456 , fClickCount (0)
457 , fLastMouseDownAt (Led_Point (0, 0))
458 , fWholeWindowInvalid (false)
459 , fUseSecondaryHilight (false)
460 , fUseBitmapScrollingOptimization (true)
461 , fSuppressTypedControlCharacters (true)
462 , fInteractiveUpdadeMode (eIndeterminateInteractiveUpdateMode)
463 , fTmpPreReplaceInfo ()
464 , fDoingUpdateModeReplaceOn (nullptr)
465 , fCaretShown (false)
466 , fLeftSideOfSelectionInteresting (false)
467 , fCaretShownAfterPos (true)
468 , fInternalizer ()
469 , fExternalizer ()
470 ,
471 //fScrollBarType (),
472 fScrollBarParamsValid (false)
473{
474 fScrollBarType[h] = eScrollBarNever;
475 fScrollBarType[v] = eScrollBarNever;
476}
477
478TextInteractor::~TextInteractor ()
479{
480 Assert (fCommandHandler == nullptr); // must be set to nullptr before we are destroyed...
481 // just sanity check - no real reason...
482 Assert (fSpellCheckEngine == nullptr); // DITTO
483}
484
485TextInteractor::CommandNames TextInteractor::MakeDefaultCommandNames ()
486{
487 TextInteractor::CommandNames cmdNames;
488 cmdNames.fTypingCommandName = Led_SDK_TCHAROF ("Typing");
489 cmdNames.fCutCommandName = Led_SDK_TCHAROF ("Cut");
490 cmdNames.fClearCommandName = Led_SDK_TCHAROF ("Clear");
491 cmdNames.fPasteCommandName = Led_SDK_TCHAROF ("Paste");
492 cmdNames.fUndoFormatString = Led_SDK_TCHAROF ("Undo %s");
493 cmdNames.fRedoFormatString = Led_SDK_TCHAROF ("ReDo %s");
494#if qStroika_Foundation_Common_Platform_Windows
495 cmdNames.fUndoFormatString += Led_SDK_TCHAROF ("\tCtrl+Z");
496 cmdNames.fRedoFormatString += Led_SDK_TCHAROF ("\tCtrl+Y");
497#endif
498 cmdNames.fReplaceCommandName = Led_SDK_TCHAROF ("Replace");
499 cmdNames.fReplaceAllCommandName = Led_SDK_TCHAROF ("Replace All");
500 cmdNames.fReplaceAllInSelectionCommandName = Led_SDK_TCHAROF ("Replace All In Selection");
501 return cmdNames;
502}
503
504/*
505@METHOD: TextInteractor::OnUpdateCommand
506@ACCESS: public
507@DESCRIPTION: <p>
508 </p>
509*/
510bool TextInteractor::OnUpdateCommand (CommandUpdater* enabler)
511{
512 RequireNotNull (enabler);
513 switch (enabler->GetCmdID ()) {
514 case kSelectAll_CmdID: {
515 enabler->SetEnabled (true);
516 return true;
517 }
518 case kCut_CmdID: {
519 OnUpdateCutCopyClearCommand (enabler);
520 return true;
521 }
522 case kCopy_CmdID: {
523 OnUpdateCutCopyClearCommand (enabler);
524 return true;
525 }
526 case kPaste_CmdID: {
527 OnUpdatePasteCommand (enabler);
528 return true;
529 }
530 case kClear_CmdID: {
531 OnUpdateCutCopyClearCommand (enabler);
532 return true;
533 }
534 case kUndo_CmdID: {
535 OnUpdateUndoRedoCommand (enabler);
536 return true;
537 }
538 case kRedo_CmdID: {
539 OnUpdateUndoRedoCommand (enabler);
540 return true;
541 }
542 case kFind_CmdID: {
543 OnUpdateFindCommands (enabler);
544 return true;
545 }
546 case kFindAgain_CmdID: {
547 OnUpdateFindCommands (enabler);
548 return true;
549 }
550 case kEnterFindString_CmdID: {
551 OnUpdateFindCommands (enabler);
552 return true;
553 }
554 case kReplace_CmdID: {
555 OnUpdateFindCommands (enabler);
556 return true;
557 }
558 case kReplaceAgain_CmdID: {
559 OnUpdateFindCommands (enabler);
560 return true;
561 }
562 case kSpellCheck_CmdID: {
563 OnUpdateSpellCheckCommand (enabler);
564 return true;
565 }
566 case kSelectWord_CmdID:
567 case kSelectTextRow_CmdID:
568 case kSelectParagraph_CmdID: {
569 OnUpdateSelectTextCommand (enabler);
570 return true;
571 }
572 }
573 return false;
574}
575
576/*
577@METHOD: TextInteractor::OnPerformCommand
578@ACCESS: public
579@DESCRIPTION: <p>
580 </p>
581*/
582bool TextInteractor::OnPerformCommand (CommandNumber commandNumber)
583{
584 switch (commandNumber) {
585 case kSelectAll_CmdID: {
586 OnSelectAllCommand ();
587 return true;
588 }
589 case kCut_CmdID: {
590 OnCutCommand ();
591 return true;
592 }
593 case kCopy_CmdID: {
594 OnCopyCommand ();
595 return true;
596 }
597 case kPaste_CmdID: {
598 OnPasteCommand ();
599 return true;
600 }
601 case kClear_CmdID: {
602 OnClearCommand ();
603 return true;
604 }
605 case kUndo_CmdID: {
606 OnUndoCommand ();
607 return true;
608 }
609 case kRedo_CmdID: {
610 OnRedoCommand ();
611 return true;
612 }
613 case kFind_CmdID: {
614 OnFindCommand ();
615 return true;
616 }
617 case kFindAgain_CmdID: {
618 OnFindAgainCommand ();
619 return true;
620 }
621 case kEnterFindString_CmdID: {
622 OnEnterFindString ();
623 return true;
624 }
625 case kReplace_CmdID: {
626 OnReplaceCommand ();
627 return true;
628 }
629 case kReplaceAgain_CmdID: {
630 OnReplaceAgainCommand ();
631 return true;
632 }
633 case kSpellCheck_CmdID: {
634 OnSpellCheckCommand ();
635 return true;
636 }
637 case kSelectWord_CmdID:
638 case kSelectTextRow_CmdID:
639 case kSelectParagraph_CmdID: {
640 OnPerformSelectTextCommand (commandNumber);
641 return true;
642 }
643 }
644 return false;
645}
646
647void TextInteractor::OnUpdateCutCopyClearCommand (CommandUpdater* enabler)
648{
649 RequireNotNull (enabler);
650 size_t start;
651 size_t end;
652 static_cast<TextImager*> (this)->GetSelection (&start, &end);
653 enabler->SetEnabled (start != end);
654}
655
656void TextInteractor::OnUpdatePasteCommand (CommandUpdater* enabler)
657{
658 RequireNotNull (enabler);
659 //tmphack
660 enabler->SetEnabled (true);
661}
662
663void TextInteractor::OnUpdateUndoRedoCommand (CommandUpdater* enabler)
664{
665 RequireNotNull (enabler);
666 if (GetCommandHandler () == nullptr) {
667 enabler->SetEnabled (false);
668 }
669 else {
670 if (enabler->GetCmdID () == kUndo_CmdID) {
671 enabler->SetEnabled (GetCommandHandler ()->CanUndo ());
672
673 SDKString menuItemText =
674 Characters::CString::Format (GetCommandNames ().fUndoFormatString.c_str (), GetCommandHandler ()->GetUndoCmdName ());
675 enabler->SetText (menuItemText.c_str ());
676 }
677 else if (enabler->GetCmdID () == kRedo_CmdID) {
678 enabler->SetEnabled (GetCommandHandler ()->CanRedo ());
679
680 SDKString menuItemText =
681 Characters::CString::Format (GetCommandNames ().fRedoFormatString.c_str (), GetCommandHandler ()->GetRedoCmdName ());
682 enabler->SetText (menuItemText.c_str ());
683 }
684 }
685}
686
687void TextInteractor::OnUpdateSelectTextCommand (CommandUpdater* enabler)
688{
689 RequireNotNull (enabler);
690 enabler->SetEnabled (true);
691}
692
693void TextInteractor::OnPerformSelectTextCommand (CommandNumber commandNumber)
694{
695 // Note: these are intentionally made not undoable, since they don't modify data
696 size_t oldSelStart = GetSelectionStart ();
697 size_t oldSelEnd = GetSelectionEnd ();
698 size_t newSelStart = oldSelStart;
699 size_t newSelEnd = oldSelEnd;
700 switch (commandNumber) {
701 case kSelectWord_CmdID: {
702 size_t wordStart = 0;
703 size_t wordEnd = 0;
704 bool wordReal = false;
705 GetTextStore ().FindWordBreaks (oldSelStart, &wordStart, &wordEnd, &wordReal);
706 if (wordReal) {
707 Assert (wordStart <= oldSelStart);
708 newSelStart = wordStart;
709 }
710 else {
711 // See if we were just after a word
712 GetTextStore ().FindWordBreaks (FindPreviousCharacter (oldSelStart), &wordStart, &wordEnd, &wordReal);
713 if (wordReal) {
714 Assert (wordStart <= oldSelStart);
715 newSelStart = wordStart;
716 }
717 }
718
719 GetTextStore ().FindWordBreaks (oldSelEnd, &wordStart, &wordEnd, &wordReal);
720 if (wordReal) {
721 newSelEnd = wordEnd;
722 }
723 } break;
724 case kSelectTextRow_CmdID: {
725 newSelStart = GetStartOfRowContainingPosition (oldSelStart);
726 // check if oldSelEnd is ALREADY the end of a row - and if so - don't change it,
727 // but otherwise, do so.
728 if (oldSelStart == oldSelEnd or oldSelEnd != GetStartOfRowContainingPosition (oldSelEnd)) {
729 newSelEnd = GetEndOfRowContainingPosition (oldSelEnd);
730 }
731 } break;
732 case kSelectParagraph_CmdID: {
733 newSelStart = GetTextStore ().GetStartOfLineContainingPosition (oldSelStart);
734
735 // check if oldSelEnd is ALREADY the end of a paragraph - and if so - don't change it,
736 // but otherwise, do so.
737 if (oldSelStart == oldSelEnd or oldSelEnd != GetTextStore ().GetStartOfLineContainingPosition (oldSelEnd)) {
738 newSelEnd = GetTextStore ().GetEndOfLineContainingPosition (oldSelEnd);
739 // grab one past endofline - so we select the NEWLINE at the end...
740 if (newSelEnd < GetEnd ()) {
741 Led_tChar c;
742 GetTextStore ().CopyOut (newSelEnd, 1, &c);
743 if (c == '\n') {
744 newSelEnd = FindNextCharacter (newSelEnd);
745 }
746 }
747 }
748 } break;
749 }
750 SetSelection (newSelStart, newSelEnd);
751}
752
753namespace {
754 vector<Led_tString> MergeRecentFindStrings (const Led_tString& s, const vector<Led_tString>& oldRecents);
755}
756
757void TextInteractor::OnFindCommand ()
758{
759 SearchParameters parameters = GetSearchParameters ();
760 bool pressedOK = false;
761 GetDialogSupport ().DisplayFindDialog (&parameters.fMatchString, parameters.fRecentFindStrings, &parameters.fWrapSearch,
762 &parameters.fWholeWordSearch, &parameters.fCaseSensativeSearch, &pressedOK);
763 parameters.fRecentFindStrings = MergeRecentFindStrings (parameters.fMatchString, parameters.fRecentFindStrings);
764 SetSearchParameters (parameters);
765 if (pressedOK) {
766 OnFindAgainCommand ();
767 }
768}
769
770void TextInteractor::OnReplaceCommand ()
771{
772 SearchParameters parameters = GetSearchParameters ();
773 ReplaceParameters rParameters = GetReplaceParameters ();
774 DialogSupport::ReplaceButtonPressed pressed =
775 GetDialogSupport ().DisplayReplaceDialog (&parameters.fMatchString, parameters.fRecentFindStrings, &rParameters.fReplaceWith,
776 &parameters.fWrapSearch, &parameters.fWholeWordSearch, &parameters.fCaseSensativeSearch);
777 parameters.fRecentFindStrings = MergeRecentFindStrings (parameters.fMatchString, parameters.fRecentFindStrings);
778 SetSearchParameters (parameters);
779 SetReplaceParameters (rParameters);
780 switch (pressed) {
781 case TextInteractor::DialogSupport::eReplaceButton_Find:
782 OnFindAgainCommand ();
783 break;
784 case TextInteractor::DialogSupport::eReplaceButton_Replace:
785 OnDoReplaceCommand (parameters, rParameters.fReplaceWith);
786 break;
787 case TextInteractor::DialogSupport::eReplaceButton_ReplaceAll:
788 OnDoReplaceAllCommand (parameters, rParameters.fReplaceWith);
789 break;
790 case TextInteractor::DialogSupport::eReplaceButton_ReplaceAllInSelection:
791 OnDoReplaceAllInSelectionCommand (parameters, rParameters.fReplaceWith);
792 break;
793 }
794}
795
796void TextInteractor::OnReplaceAgainCommand ()
797{
798 SearchParameters parameters = GetSearchParameters ();
799 ReplaceParameters rParameters = GetReplaceParameters ();
800 OnDoReplaceCommand (parameters, rParameters.fReplaceWith);
801}
802
803void TextInteractor::OnDoReplaceCommand (const SearchParameters& searchFor, const Led_tString& replaceWith)
804{
805 BreakInGroupedCommands ();
806 // search for last text entered into find dialog (could have been a find again).
807 size_t origSelStart = GetSelectionStart ();
808 size_t origSelEnd = GetSelectionEnd ();
809 size_t whereTo = GetTextStore ().Find (searchFor, origSelEnd);
810 if ((whereTo == kBadIndex) or (whereTo == origSelStart and whereTo + searchFor.fMatchString.length () == origSelEnd)) {
811 Led_BeepNotify ();
812 }
813 else {
814 InteractiveModeUpdater iuMode (*this);
815 size_t replaceStart = whereTo;
816 size_t replaceEnd = whereTo + searchFor.fMatchString.length ();
817 UndoableContextHelper undoContext (*this, GetCommandNames ().fReplaceCommandName, replaceStart, replaceEnd, GetSelectionStart (),
818 GetSelectionEnd (), false);
819 {
820 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), replaceWith.c_str (), replaceWith.length ());
821 SetSelection (whereTo, whereTo + replaceWith.length ());
822 }
823 undoContext.CommandComplete ();
824 ScrollToSelection ();
825 }
826 BreakInGroupedCommands ();
827}
828
829void TextInteractor::OnDoReplaceAllCommand (const SearchParameters& searchFor, const Led_tString& replaceWith)
830{
831 InteractiveModeUpdater iuMode (*this);
832 BreakInGroupedCommands ();
833 size_t startAt = 0;
834 while (true) {
835 size_t whereTo = GetTextStore ().Find (searchFor, startAt, GetTextStore ().GetEnd ());
836 if (whereTo == kBadIndex) {
837 break;
838 }
839 else {
840 size_t replaceStart = whereTo;
841 size_t replaceEnd = whereTo + searchFor.fMatchString.length ();
842 UndoableContextHelper undoContext (*this, GetCommandNames ().fReplaceAllCommandName, replaceStart, replaceEnd,
843 GetSelectionStart (), GetSelectionEnd (), false);
844 {
845 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), replaceWith.c_str (), replaceWith.length ());
846 SetSelection (whereTo, whereTo + replaceWith.length ());
847 startAt = whereTo + replaceWith.length ();
848 }
849 undoContext.CommandComplete ();
850 }
851 }
852 ScrollToSelection ();
853 BreakInGroupedCommands ();
854}
855
856void TextInteractor::OnDoReplaceAllInSelectionCommand (const SearchParameters& searchFor, const Led_tString& replaceWith)
857{
858 InteractiveModeUpdater iuMode (*this);
859 BreakInGroupedCommands ();
860 size_t startAt = GetSelectionStart ();
861 TempMarker selectionRegion (GetTextStore (), startAt, GetSelectionEnd ());
862 while (true) {
863 Assert (startAt <= selectionRegion.GetEnd ());
864 size_t whereTo = GetTextStore ().Find (searchFor, startAt, selectionRegion.GetEnd ());
865 if (whereTo == kBadIndex) {
866 break;
867 }
868 else {
869 size_t replaceStart = whereTo;
870 size_t replaceEnd = whereTo + searchFor.fMatchString.length ();
871 UndoableContextHelper undoContext (*this, GetCommandNames ().fReplaceAllInSelectionCommandName, replaceStart, replaceEnd,
872 GetSelectionStart (), GetSelectionEnd (), false);
873 {
874 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), replaceWith.c_str (), replaceWith.length ());
875 SetSelection (whereTo, whereTo + replaceWith.length ());
876 startAt = whereTo + replaceWith.length ();
877 }
878 undoContext.CommandComplete ();
879 }
880 }
881 ScrollToSelection ();
882 BreakInGroupedCommands ();
883}
884
885void TextInteractor::OnFindAgainCommand ()
886{
887 TextStore::SearchParameters parameters = GetSearchParameters ();
888
889 // search for last text entered into find dialog (could have been a find again).
890 size_t origSelStart = GetSelectionStart ();
891 size_t origSelEnd = GetSelectionEnd ();
892 size_t whereTo = GetTextStore ().Find (parameters, origSelEnd);
893 if ((whereTo == kBadIndex) or (whereTo == origSelStart and whereTo + parameters.fMatchString.length () == origSelEnd)) {
894 Led_BeepNotify ();
895 }
896 else {
897 SetSelection (whereTo, whereTo + parameters.fMatchString.length ());
898 ScrollToSelection ();
899 }
900}
901
902void TextInteractor::OnEnterFindString ()
903{
904 SearchParameters parameters = GetSearchParameters ();
905
906 size_t selStart = GetSelectionStart ();
907 size_t selEnd = GetSelectionEnd ();
908 size_t selLength = selEnd - selStart;
909
910 Memory::StackBuffer<Led_tChar> buf{Memory::eUninitialized, selLength};
911 CopyOut (selStart, selLength, buf.data ());
912 parameters.fMatchString = Led_tString{buf.data (), selLength};
913 parameters.fRecentFindStrings = MergeRecentFindStrings (parameters.fMatchString, parameters.fRecentFindStrings);
914 SetSearchParameters (parameters);
915}
916
917void TextInteractor::OnUpdateFindCommands (CommandUpdater* enabler)
918{
919 RequireNotNull (enabler);
920 if (enabler->GetCmdID () == kFind_CmdID) {
921 enabler->SetEnabled (true);
922 }
923 else if (enabler->GetCmdID () == kFindAgain_CmdID) {
924 enabler->SetEnabled (GetSearchParameters ().fMatchString.length () != 0);
925 }
926 else if (enabler->GetCmdID () == kEnterFindString_CmdID) {
927 enabler->SetEnabled (GetSelectionStart () != GetSelectionEnd ());
928 }
929 else if (enabler->GetCmdID () == kReplace_CmdID) {
930 enabler->SetEnabled (true);
931 }
932 else if (enabler->GetCmdID () == kReplaceAgain_CmdID) {
933 enabler->SetEnabled (GetSearchParameters ().fMatchString.length () != 0);
934 }
935}
936
937TextInteractor::SearchParameters TextInteractor::GetSearchParameters () const
938{
939 return sSearchParameters;
940}
941
942void TextInteractor::SetSearchParameters (const SearchParameters& sp)
943{
944 sSearchParameters = sp;
945}
946
947TextInteractor::ReplaceParameters TextInteractor::GetReplaceParameters () const
948{
949 return sReplaceParameters;
950}
951
952void TextInteractor::SetReplaceParameters (const ReplaceParameters& rp)
953{
954 sReplaceParameters = rp;
955}
956
957vector<Led_tString> TextInteractor::MergeRecentFindStrings (const Led_tString& s, const vector<Led_tString>& oldRecents)
958{
959 const unsigned int kMaxEntries = 20;
960 vector<Led_tString> result = oldRecents;
961 // See if the given string appears in the list. If so - erase it (so it only appears once). Prepend the new string
962 // (most recent) to the top of the list. Truncate any entries beyond some magic #.
963 vector<Led_tString>::iterator i = std::find (result.begin (), result.end (), s);
964 if (i != result.end ()) {
965 result.erase (i);
966 }
967 result.insert (result.begin (), s);
968 if (result.size () > kMaxEntries) {
969 result.erase (result.begin () + kMaxEntries, result.end ());
970 }
971 return result;
972}
973
974void TextInteractor::OnSpellCheckCommand ()
975{
976 if (GetSpellCheckEngine () == nullptr) {
977 Led_BeepNotify ();
978 }
979 else {
980 MyCallback cb (*this);
981 GetDialogSupport ().DisplaySpellCheckDialog (cb);
982 }
983}
984
985void TextInteractor::OnUpdateSpellCheckCommand (CommandUpdater* enabler)
986{
987 RequireNotNull (enabler);
988 enabler->SetEnabled (GetSpellCheckEngine () != nullptr);
989}
990
991/*
992@METHOD: TextInteractor::SetDefaultUpdateMode
993@ACCESS: protected
994@DESCRIPTION: <p>TextInteractor's have an associated default UpdateMode. This is the update mode which is
995 used by methods which used the eDefaultUpdateMode argument (most default UpdateMode args in Led are this value).</p>
996 <p>This should <em>not</em> be set directly. Instead, instantiate a @'TextInteractor::TemporarilySetUpdateMode' object on
997 the stack.</p>
998*/
999void TextInteractor::SetDefaultUpdateMode (UpdateMode defaultUpdateMode)
1000{
1001 if (defaultUpdateMode != eDefaultUpdate) { // setting update mode to 'eDefaultUpdate' is synonomous with a NO-OP
1002 fDefaultUpdateMode = defaultUpdateMode;
1003 }
1004}
1005
1006/*
1007@METHOD: TextInteractor::LooksLikeSmartPastableText
1008@ACCESS: protected
1009@DESCRIPTION: <p>This is called internally when text is pasted or dropped in a Led text buffer.
1010 Right now - it looks at the selection range to see if the text surrounding the selection boundaries
1011 has space characters and sets this information into @'TextInteractor::SmartCNPInfo'.
1012 </p>
1013 <p>This code is still far from perfect, but now (SPR#1286) appears to work pretty decently.
1014 It may still see significant revision at some point as part of SPR #1040.
1015 </p>
1016*/
1017bool TextInteractor::LooksLikeSmartPastableText ([[maybe_unused]] const Led_tChar* text, size_t /*nTextTChars*/, SmartCNPInfo* smartCNPInfo) const
1018{
1019 RequireNotNull (text);
1020 RequireNotNull (smartCNPInfo);
1021 if (GetSmartCutAndPasteMode ()) {
1022 size_t selStart = GetSelectionStart ();
1023 size_t selEnd = GetSelectionEnd ();
1024
1025 // If both current and prev chars where non-space, insert a space between.
1026 if (selStart == 0 or selStart >= GetEnd ()) {
1027 smartCNPInfo->fWordBreakAtSelStart = true;
1028 }
1029 else {
1030 size_t prev = FindPreviousCharacter (selStart);
1031 Led_tChar prevC;
1032 CopyOut (prev, 1, &prevC);
1033 Led_tChar c;
1034 CopyOut (selStart, 1, &c);
1035 smartCNPInfo->fWordBreakAtSelStart = (IsShouldBeSepWithWhitespaceWordChar (c) != IsShouldBeSepWithWhitespaceWordChar (prevC));
1036 }
1037
1038 if (selEnd == 0 or selEnd >= GetEnd ()) {
1039 smartCNPInfo->fWordBreakAtSelEnd = true;
1040 }
1041 else {
1042 size_t prev = FindPreviousCharacter (selEnd);
1043 Led_tChar prevC;
1044 CopyOut (prev, 1, &prevC);
1045 Led_tChar c;
1046 CopyOut (selEnd, 1, &c);
1047 smartCNPInfo->fWordBreakAtSelEnd = (IsShouldBeSepWithWhitespaceWordChar (c) != IsShouldBeSepWithWhitespaceWordChar (prevC));
1048 }
1049 }
1050 return true;
1051}
1052
1053/*
1054@METHOD: TextInteractor::OptionallyAddExtraSpaceForSmartCutAndPasteModeAdds
1055@ACCESS: protected
1056@DESCRIPTION: <p>See @'TextInteractor::GetSmartCutAndPasteMode'.</p>
1057 <p>The way in which which we handle smart cut-and-paste is twofold. We have sometimes add extra spaces
1058 (which is done in this routine), and sometimes we must delete extra spaces
1059 (which is done in @'TextInteractor::OptionallyExpandSelectionForSmartCutAndPasteModeDeletes').</p>
1060 <p>We must be careful todo our space insertion in such a way that it is friendly to the UNDO system (doesn't
1061 appear as a separate undo event etc).</p>
1062 <p>Note - this routine is called after the update has taken place. And it looks at the text
1063 at the LHS of where the insertion was done, and at the RHS of where it finihsed. The RHS of where
1064 it finished is always assumed to be the result of a call to @'TextImager::GetSelectionEnd'. And the start
1065 is given by the 'selStart' argument.</p>
1066*/
1067void TextInteractor::OptionallyAddExtraSpaceForSmartCutAndPasteModeAdds (size_t selStart, const SmartCNPInfo& smartCNPInfo)
1068{
1069 size_t selEnd = GetSelectionEnd ();
1070
1071 Require (0 <= selStart);
1072 Require (selStart <= selEnd);
1073 Require (selEnd <= GetTextStore ().GetEnd ());
1074
1075 if (GetSmartCutAndPasteMode ()) {
1076 if (selEnd > 0 and selEnd < GetTextStore ().GetEnd ()) {
1077 size_t prev = FindPreviousCharacter (selEnd);
1078 Led_tChar prevC;
1079 CopyOut (prev, 1, &prevC);
1080 Led_tChar c;
1081 CopyOut (selEnd, 1, &c);
1082 if (smartCNPInfo.fWordBreakAtSelEnd and IsShouldBeSepWithWhitespaceWordChar (c) and IsShouldBeSepWithWhitespaceWordChar (prevC)) {
1083 InteractiveReplace_ (selEnd, selEnd, LED_TCHAR_OF (" "), 1, false);
1084 // Cannot pass 'updateSelection' flag in the above InteractiveReplace_ () call because
1085 // that code only works under restrictive circumstances (see require calls in that code).
1086 // Still - we MUST advance the end of the selection to take into account the space added. Not
1087 // just for cosmetic reasons, but because the current (as of Led 2.3b8) - and probably all future versions-
1088 // Undo code uses the selection end at the end of insertions to see how much text needs to be
1089 // saved for undoing purposes.
1090 // LGP990401
1091 ++selEnd;
1092 SetSelection (selEnd, selEnd);
1093 }
1094 }
1095 // If both current and prev chars where non-space, insert a space between.
1096 if (selStart > 0 and selStart < GetTextStore ().GetEnd ()) {
1097 size_t prev = FindPreviousCharacter (selStart);
1098 Led_tChar prevC;
1099 CopyOut (prev, 1, &prevC);
1100 Led_tChar c;
1101 CopyOut (selStart, 1, &c);
1102 if (smartCNPInfo.fWordBreakAtSelStart and IsShouldBeSepWithWhitespaceWordChar (c) and IsShouldBeSepWithWhitespaceWordChar (prevC)) {
1103 // Programming Confusion Note:
1104 //
1105 // Not quite sure why we pass the updateCursorPosition=false param here. Seems like it should
1106 // be true. But that seems to screw up the enclosing undo information.
1107 // Well, I guess if it aint broke, don't fix it. Least not right now... --LGP 970315.
1108 InteractiveReplace_ (selStart, selStart, LED_TCHAR_OF (" "), 1, false);
1109 }
1110 else {
1111 if (IsSmartSpace (c) and IsSmartSpace (prevC)) {
1112 InteractiveReplace_ (selStart, FindNextCharacter (selStart), nullptr, 0, false);
1113 }
1114 }
1115 }
1116 }
1117}
1118
1119/*
1120@METHOD: TextInteractor::OptionallyExpandSelectionForSmartCutAndPasteModeDeletes
1121@DESCRIPTION: <p>See @'TextInteractor::GetSmartCutAndPasteMode'.</p>
1122*/
1123void TextInteractor::OptionallyExpandSelectionForSmartCutAndPasteModeDeletes (size_t* selStart, size_t* selEnd)
1124{
1125 RequireNotNull (selStart);
1126 RequireNotNull (selEnd);
1127 Require (0 <= *selStart);
1128 Require (*selStart <= *selEnd);
1129 Require (*selEnd <= GetTextStore ().GetEnd ());
1130
1131 if (GetSmartCutAndPasteMode ()) {
1132 size_t realStart = *selStart;
1133 size_t realEnd = *selEnd;
1134 size_t newStart = realStart;
1135 size_t newEnd = realEnd;
1136
1137 // if selStart is in whitespace, or in the middle of a word,
1138 // or selSend is just after whitespace or in the middle of a word then don't be smart.
1139 // LGP 970315 (amended LGP990415)
1140 {
1141 if (realStart < GetEnd ()) {
1142 Led_tChar c;
1143 CopyOut (realStart, 1, &c);
1144 if (Character (c).IsWhitespace ()) {
1145 return;
1146 }
1147 if (realStart > 0) {
1148 CopyOut (FindPreviousCharacter (realStart), 1, &c);
1149 if (IsShouldBeSepWithWhitespaceWordChar (c)) {
1150 return;
1151 }
1152 }
1153 }
1154
1155 if (realStart < realEnd and realEnd < GetEnd ()) {
1156 Led_tChar c;
1157 CopyOut (FindPreviousCharacter (realEnd), 1, &c);
1158 if (Character (c).IsWhitespace ()) {
1159 return;
1160 }
1161 if (realEnd < GetEnd ()) {
1162 CopyOut (realEnd, 1, &c);
1163 if (IsShouldBeSepWithWhitespaceWordChar (c)) {
1164 return;
1165 }
1166 }
1167 }
1168 }
1169
1170 // Now see if at the cursor we have extra spaces - scan first backward, and then forward. Should be at most one.
1171 Led_tChar c;
1172 // back
1173 {
1174 if (newStart > 0) {
1175 size_t prev = FindPreviousCharacter (newStart);
1176 CopyOut (prev, 1, &c);
1177 size_t prevprev = FindPreviousCharacter (prev);
1178 Led_tChar prevprevC;
1179 CopyOut (prevprev, 1, &prevprevC);
1180 if (prevprev != prev and IsSmartSpace (c) and not(IsSmartSpace (prevprevC) or prevprevC == '\n')) {
1181 newStart = prev;
1182 }
1183 }
1184 }
1185 // and forth
1186 {
1187 if (newEnd < GetEnd ()) {
1188 CopyOut (newEnd, 1, &c);
1189 Led_tChar charBeforeStart = '\0';
1190 if (newStart != 0) {
1191 CopyOut (FindPreviousCharacter (newStart), 1, &charBeforeStart);
1192 }
1193 if (IsSmartSpace (c)) {
1194 Assert (newEnd < FindNextCharacter (newEnd)); // Assert not looping!
1195 newEnd = FindNextCharacter (newEnd);
1196 if (newEnd < GetEnd ()) {
1197 // As a result of this change - don't put two chars together that should be separated by whitespace
1198 Led_tChar nextChar = '\0';
1199 CopyOut (newEnd, 1, &nextChar);
1200 if (IsShouldBeSepWithWhitespaceWordChar (charBeforeStart) and IsShouldBeSepWithWhitespaceWordChar (nextChar)) {
1201 newEnd = FindPreviousCharacter (newEnd);
1202 }
1203 }
1204 }
1205 }
1206 }
1207
1208 *selStart = newStart;
1209 *selEnd = newEnd;
1210 }
1211}
1212
1213/*
1214@METHOD: TextInteractor::SetSelectionShown
1215@DESCRIPTION: <p>See TextInteractor::GetSelectionShown. Typically this method isn't called directly by user code, but
1216 from within the class library wrappers (e.g. Led_MFC) on gain/lose focus events.</p>
1217*/
1218void TextInteractor::SetSelectionShown (bool shown)
1219{
1220 SetSelectionShown (shown, eDefaultUpdate);
1221}
1222
1223void TextInteractor::SetSelectionShown (bool shown, UpdateMode updateMode)
1224{
1225 if (GetSelectionShown () != shown) {
1226 TextImager::SetSelectionShown (shown);
1227 Refresh (GetSelectionStart (), GetSelectionEnd (), updateMode);
1228 }
1229}
1230
1231/*
1232@METHOD: TextInteractor::SetSelection
1233@DESCRIPTION: <p>See @'TextInteractor::GetSelection'.</p>
1234*/
1235void TextInteractor::SetSelection (size_t start, size_t end)
1236{
1237 Assert (end <= GetEnd ());
1238 UpdateMode updateMode = GetDefaultUpdateMode ();
1239
1240 // This isn't quite perfect for the case of eImmediateUpdate- but should be close enough...
1241 if (start != GetSelectionStart () or end != GetSelectionEnd ()) {
1242 IdleManager::NonIdleContext nonIdleContext;
1243
1244 size_t oldSelectionStart = GetSelectionStart ();
1245 size_t oldSelectionEnd = GetSelectionEnd ();
1246
1247 /*
1248 * Update fLeftSideOfSelectionInteresting. if start changed, we are interested in LHS, and
1249 * if end changed we are interested in RHS. But if BOTH changed then just assume interested
1250 * in RHS.
1251 */
1252 if (start == GetSelectionStart ()) {
1253 fLeftSideOfSelectionInteresting = false;
1254 }
1255 else if (end == GetSelectionEnd ()) {
1256 fLeftSideOfSelectionInteresting = true;
1257 }
1258 else if ((start < GetSelectionStart ()) == (end < GetSelectionEnd ())) {
1259 fLeftSideOfSelectionInteresting = (start < GetSelectionStart ());
1260 }
1261
1262 /*
1263 * Note we must invalidate the caret state before and after changing the selction so
1264 * that we erase the old position, and draw in the new.
1265 */
1266 InvalidateCaretState ();
1267
1268 TextImager::SetSelection (start, end);
1269
1270 if ((GetSelectionShown () or GetUseSecondaryHilight ()) and updateMode != eNoUpdate) {
1271 /*
1272 * What we REALLY want to invalidate is the CHANGE in hilight region. Not the UNION.
1273 *
1274 * The difference (invalid area) is Union (a,b)-Intersection(a,b). Now with the facilities at our
1275 * disposal, there doesn't appear any obvious way to compute this. So we hack it out
1276 * a bit.
1277 */
1278 UpdateMode useUpdateMode = (updateMode == eImmediateUpdate) ? eDelayedUpdate : updateMode;
1279
1280 size_t lhsOuter = min (oldSelectionStart, GetSelectionStart ()); // left of ALL 4
1281 size_t rhsOuter = max (oldSelectionEnd, GetSelectionEnd ()); // right of ALL 4
1282 size_t lhsInner = max (oldSelectionStart, GetSelectionStart ()); // the two inner ones - lhsInner could be >=< rhsInner
1283 size_t rhsInner = min (oldSelectionEnd, GetSelectionEnd ());
1284 Assert (lhsOuter <= rhsOuter); // See!- left of ALL 4
1285 Assert (lhsOuter <= lhsInner);
1286 Assert (lhsOuter <= rhsInner);
1287 Assert (lhsOuter <= rhsOuter); // See!- right of ALL 4
1288 Assert (lhsInner <= rhsOuter);
1289 Assert (rhsInner <= rhsOuter);
1290 /*
1291 * SPR#0973 - added in the FindPrev/FindNext calls to expand the region we update slightly. Reason is cuz
1292 * with new hilight display code - expanding the selection slightly at the end can affect the other end by
1293 * up to one character (rarely - but best to over-invalidate than under-invalidate).
1294 */
1295 Refresh (FindPreviousCharacter (lhsOuter), FindNextCharacter (lhsInner), useUpdateMode);
1296 Refresh (FindPreviousCharacter (rhsInner), FindNextCharacter (rhsOuter), useUpdateMode);
1297 }
1298
1299 InvalidateCaretState ();
1300 }
1301 if (updateMode == eImmediateUpdate) {
1302 Update ();
1303 }
1304 RecomputeSelectionGoalColumn ();
1305}
1306
1307void TextInteractor::SetSelection (size_t start, size_t end, UpdateMode updateMode)
1308{
1309 TemporarilySetUpdateMode updateModeSetter (*this, updateMode);
1310 TextImager* tim = this; // Dynamicly bind to 1-arg version. Direct call would select overloaded version from this class!
1311 tim->SetSelection (start, end);
1312}
1313
1314/*
1315@METHOD: TextInteractor::ScrollToSelection
1316@DESCRIPTION: <p>Scroll the current window, so that the selection is showing. Calls TextInteractor::ScrollSoShowing ().
1317 Since TextInteractor::ScrollSoShowing () requires two marker-positions to try to show, we use the instance variable
1318 fLeftSideOfSelectionInteresting to guess which side of the selection the user is probably most interested in.
1319 This routine also pays attention to @'TextImager::GetCaretShownAfterPos' so that it shows the right part of
1320 the screen in ambiguous cases.</p>
1321 <p>See TextImager::ScrollSoShowing for more details. But basicly this tries to show the entire selection, if
1322 possible. And uses the flag to pick which side of the selection to use if only part of it can be made visible in the
1323 current window at once.</p>
1324*/
1325void TextInteractor::ScrollToSelection (UpdateMode updateMode, bool forceShowSelectionEndpoint)
1326{
1327 size_t selStart = GetSelectionStart ();
1328 size_t selEnd = GetSelectionEnd ();
1329
1330 /*
1331 * Based on SPR#0940. If some of the selection is showing - its not totally clear the user would want us
1332 * to scroll the entire screen to show another part of the selection. Maybe yes - maybe no. But clearly - if the
1333 * ENTIRE SCREEN is selected (as in this SPR - then we don't want to scroll to show a different part of the selection.
1334 * I THINK so anyhow... LGP 2001-05-24
1335 *
1336 * PLUS - fix SPR#1051 - added forceShowSelectionEndpoint flag, so when using cursors, we can force a scroll.
1337 */
1338 if (not forceShowSelectionEndpoint and selStart <= GetMarkerPositionOfStartOfWindow () and selEnd >= GetMarkerPositionOfEndOfWindow ()) {
1339 return;
1340 }
1341
1342 /*
1343 * Why is this so complicated? And is it right?
1344 *
1345 * There are a couple of issues here. One is that the semantics of @'TextImager::ScrollSoShowing'
1346 * are very precise, but perhaps slightly unintuitive. We try to show the CHARACTERS (not marker positions)
1347 * given by the marker-positions specified.
1348 *
1349 * But when you are cursoring around, what you want to see are the CHARACTERS - not the MARKER POSITIONS.
1350 *
1351 * In these ambiguous cases (like at a wrap point), we want to make sure we get the interpretation of which
1352 * character to show correct. And when the user must choose (cannot show whole selection) we want to make sure
1353 * we AT LEAST show the IMPORTANT side of the selection.
1354 *
1355 * The 'lastCharShown = FindPreviousCharacter (selEnd)' bit is cuz we want to show the character BEFORE that
1356 * marker position. And for most APIs the marker pos names the character after that marker position.
1357 *
1358 * -- LGP990212
1359 */
1360 size_t firstCharShown = selStart;
1361 size_t lastCharShown = FindPreviousCharacter (selEnd);
1362 if (fLeftSideOfSelectionInteresting) {
1363 if (firstCharShown != lastCharShown and GetStartOfRowContainingPosition (firstCharShown) != GetStartOfRowContainingPosition (lastCharShown)) {
1364 lastCharShown = firstCharShown;
1365 }
1366 ScrollSoShowing (firstCharShown, lastCharShown, updateMode);
1367 }
1368 else {
1369 if (GetCaretShownAfterPos ()) {
1370 lastCharShown = FindNextCharacter (lastCharShown);
1371 }
1372 if (firstCharShown != lastCharShown and GetStartOfRowContainingPosition (firstCharShown) != GetStartOfRowContainingPosition (lastCharShown)) {
1373 firstCharShown = lastCharShown;
1374 }
1375 ScrollSoShowing (lastCharShown, firstCharShown, updateMode);
1376 }
1377}
1378
1379void TextInteractor::HookLosingTextStore ()
1380{
1381 HookLosingTextStore_ ();
1382 TextImager::HookLosingTextStore ();
1383}
1384
1385void TextInteractor::HookLosingTextStore_ ()
1386{
1387 AbortReplace (fTmpPreReplaceInfo);
1388 fExternalizer.reset ();
1389 fInternalizer.reset ();
1390}
1391
1392void TextInteractor::HookGainedNewTextStore ()
1393{
1394 HookGainedNewTextStore_ ();
1395 TextImager::HookGainedNewTextStore ();
1396}
1397
1398void TextInteractor::HookGainedNewTextStore_ ()
1399{
1400 if (fExternalizer.get () == nullptr) {
1401 SetExternalizer (MakeDefaultExternalizer ());
1402 }
1403 if (fInternalizer.get () == nullptr) {
1404 SetInternalizer (MakeDefaultInternalizer ());
1405 }
1406}
1407
1408/*
1409@METHOD: TextInteractor::ProcessSimpleClick
1410@ACCESS: protected
1411@DESCRIPTION: <p>Bad name. Basicly does a small portion of the click handling code. Vectored out
1412 not only for code sharing, but to provide a hook we can use in the StyledTextInteractor code
1413 to handle embeddings (without massive cut/n/paste duplication of code).</p>
1414 <p>What this does is to convert the click (or double or tripple click into an appropriate
1415 selection. The anchor only has meaning for double clicks with exteneded selection on.
1416 <p>This routine uses the existing selection, and just adjusts that selection.</p>
1417 <p>This routine returns true if all went well, and you should continue with interpretting
1418 click. It returns false if you should assume something clicked on swallowed up the click.</p>
1419 <p>This is not a GREAT line to have drawn for the API boundary. But its a start.</p>
1420 <p>LGP 960303</p>
1421*/
1422bool TextInteractor::ProcessSimpleClick (Led_Point clickedAt, unsigned clickCount, bool extendSelection, size_t* dragAnchor)
1423{
1424 RequireNotNull (dragAnchor);
1425 switch (clickCount) {
1426 case 1: {
1427 size_t newPos = GetCharAtClickLocation (clickedAt);
1428 size_t newSelStart = newPos;
1429 size_t newSelEnd = newPos;
1430 {
1431 WhileTrackingConstrainSelection (&newSelStart, &newSelEnd);
1432 newPos = newSelStart;
1433 }
1434 if (extendSelection) {
1435 newSelStart = min (newSelStart, GetSelectionStart ());
1436 newSelEnd = max (newSelEnd, GetSelectionEnd ());
1437 }
1438
1439 /*
1440 * Set the drag anchor:
1441 *
1442 * Note - we use @'TextInteractor::GetCharAtClickLocation' instead of @'TextImager::GetCharAtWindowLocation'
1443 * so we get the right intended click location between two characters (for an anchor point for drag selection).
1444 *
1445 * Code that is looking to see if the user clicked on an embedding itself will directly call
1446 * @'TextImager::GetCharAtWindowLocation'.
1447 */
1448 if (extendSelection) {
1449 // if we're extending the selection, the anchor should be the OTHER end of the selection (SPR#1364)
1450 if (newPos == newSelStart) {
1451 *dragAnchor = newSelEnd;
1452 }
1453 else {
1454 *dragAnchor = newSelStart;
1455 }
1456 }
1457 else {
1458 *dragAnchor = newPos;
1459 }
1460
1461 // Set flag for how to display caret based on where we clicked
1462 if (not extendSelection) {
1463 SetCaretShownAfterPos (GetCharWindowLocation (newPos).top <= clickedAt.v);
1464 }
1465
1466 WhileTrackingConstrainSelection (&newSelStart, &newSelEnd);
1467 SetSelection (newSelStart, newSelEnd);
1468 } break;
1469
1470 default: {
1471 // others are ignored
1472 } break;
1473 }
1474#if 0
1475 DbgTrace ("TextInteractor::ProcessSimpleClick (tickCount=%f, newMousePos=(%d,%d), clickCount=%d, extendSel=%d, newSelStart=%d, newSelEnd=%d)\n",
1476 Time::GetTickCount (), clickedAt.v, clickedAt.h, clickCount, extendSelection, GetSelectionStart (), GetSelectionEnd ()
1477 );
1478#endif
1479 return true;
1480}
1481
1482/*
1483@METHOD: TextInteractor::UpdateClickCount
1484@DESCRIPTION: <p>Helper to implemented best feeling UI for double click detection.</p>
1485*/
1486void TextInteractor::UpdateClickCount (Time::TimePointSeconds clickAtTime, const Led_Point& clickAtLocation)
1487{
1488 if (ClickTimesAreCloseForDoubleClick (clickAtTime) and PointsAreCloseForDoubleClick (clickAtLocation)) {
1489 IncrementCurClickCount (clickAtTime);
1490 }
1491 else {
1492 SetCurClickCount (1, clickAtTime);
1493 }
1494 fLastMouseDownAt = clickAtLocation;
1495}
1496
1497/*
1498@METHOD: TextInteractor::ClickTimesAreCloseForDoubleClick
1499@DESCRIPTION: <p>Helper to implemented best feeling UI for double click detection. See also @'TextInteractor::UpdateClickCount' ().</p>
1500*/
1501bool TextInteractor::ClickTimesAreCloseForDoubleClick (Time::TimePointSeconds thisClick)
1502{
1503 return (fLastClickedAt + Led_GetDoubleClickTime () >= thisClick);
1504}
1505
1506/*
1507@METHOD: TextInteractor::PointsAreCloseForDoubleClick
1508@DESCRIPTION: <p>Helper to implemented best feeling UI for double click detection. See also @'TextInteractor::UpdateClickCount' ().</p>
1509*/
1510bool TextInteractor::PointsAreCloseForDoubleClick (const Led_Point& p)
1511{
1512 const CoordinateType kMultiClickDistance = 4;
1513 CoordinateType hDelta = p.h - fLastMouseDownAt.h;
1514 if (hDelta < 0) {
1515 hDelta = -hDelta;
1516 }
1517 CoordinateType vDelta = p.v - fLastMouseDownAt.v;
1518 if (vDelta < 0) {
1519 vDelta = -vDelta;
1520 }
1521 return ((hDelta <= kMultiClickDistance) and (vDelta <= kMultiClickDistance));
1522}
1523
1524/*
1525@METHOD: TextInteractor::WhileSimpleMouseTracking
1526@DESCRIPTION: <p>Helper to share code among implementations. Call this while mouse is down to handle autoscrolling,
1527 and selection updating.</p>
1528*/
1529void TextInteractor::WhileSimpleMouseTracking (Led_Point newMousePos, size_t dragAnchor)
1530{
1531#if qDynamiclyChooseAutoScrollIncrement
1532 Foundation::Time::TimePointSeconds now = Time::GetTickCount ();
1533 static Foundation::Time::TimePointSeconds sLastTimeThrough{};
1534 const Foundation::Time::DurationSeconds kClickThreshold = Led_GetDoubleClickTime () / 3;
1535 bool firstClick = (now - sLastTimeThrough > kClickThreshold);
1536
1537 int increment = firstClick ? 1 : 2;
1538#else
1539 const int increment = 1;
1540#endif
1541
1542 size_t rhsPos = GetCharAtClickLocation (newMousePos);
1543 {
1544 size_t ignored = rhsPos;
1545 WhileTrackingConstrainSelection (&rhsPos, &ignored);
1546 }
1547
1548 /*
1549 * Handle vertical autoscrolling, if necessary.
1550 */
1551 if (rhsPos < GetMarkerPositionOfStartOfWindow ()) {
1552 ScrollByIfRoom (-1, eImmediateUpdate);
1553 rhsPos = GetMarkerPositionOfStartOfWindow ();
1554 }
1555 else if (rhsPos > GetMarkerPositionOfEndOfWindow ()) {
1556 ScrollByIfRoom (1, eImmediateUpdate);
1557 rhsPos = FindNextCharacter (GetMarkerPositionOfEndOfWindow ()); // So we select past to end of window
1558 }
1559
1560 /*
1561 * And horizontal.
1562 */
1563 const int kHScrollIncrementFactor = 4;
1564 if (newMousePos.h < GetWindowRect ().left) {
1565 if (GetHScrollPos () > 0) {
1566 SetHScrollPos (max (0, int (GetHScrollPos ()) - increment * kHScrollIncrementFactor));
1567 }
1568 }
1569 else if (newMousePos.h > GetWindowRect ().right) {
1570 SetHScrollPos (min (static_cast<CoordinateType> (GetHScrollPos () + increment * kHScrollIncrementFactor),
1571 static_cast<CoordinateType> (ComputeMaxHScrollPos ())));
1572 }
1573
1574 size_t newSelStart = min (rhsPos, dragAnchor);
1575 size_t newSelEnd = max (rhsPos, dragAnchor);
1576 WhileTrackingConstrainSelection (&newSelStart, &newSelEnd);
1577 SetSelection (newSelStart, newSelEnd, eImmediateUpdate);
1578
1579#if qDynamiclyChooseAutoScrollIncrement
1580 sLastTimeThrough = now;
1581#endif
1582}
1583
1584/*
1585@METHOD: TextInteractor::WhileTrackingConstrainSelection
1586@DESCRIPTION: <p>Override this to provide unusual selection behavior during
1587 tracking. For example, if you want to only allow selection of whole lines when
1588 the caps-lock key is down (OK, silly, but its an example), you could check for that
1589 state in this routine, and assure selStart and selEnd are adjusted to meet those
1590 criteria.</p>
1591 <p>By default this calls calls @'TextInteractor::WhileTrackingConstrainSelection_ForWholeWords',
1592 for double clicks and @'TextInteractor::WhileTrackingConstrainSelection_ForWholeRows' for 3 or more clicks.</p>
1593*/
1594void TextInteractor::WhileTrackingConstrainSelection (size_t* selStart, size_t* selEnd)
1595{
1596 RequireNotNull (selStart);
1597 RequireNotNull (selEnd);
1598 Require (GetCurClickCount () > 0);
1599 switch (GetCurClickCount ()) {
1600 case 1: {
1601 // No constraints on a single click
1602 } break;
1603
1604 case 2: {
1605 WhileTrackingConstrainSelection_ForWholeWords (selStart, selEnd);
1606 } break;
1607
1608 default: {
1609 // any more than 3 and we constrain to whole rows
1610 WhileTrackingConstrainSelection_ForWholeRows (selStart, selEnd);
1611 } break;
1612 }
1613}
1614
1615/*
1616@METHOD: TextInteractor::WhileTrackingConstrainSelection_ForWholeWords
1617@DESCRIPTION: <p>See @'TextInteractor::WhileTrackingConstrainSelection'</p>
1618*/
1619void TextInteractor::WhileTrackingConstrainSelection_ForWholeWords (size_t* selStart, size_t* selEnd)
1620{
1621 RequireNotNull (selStart);
1622 RequireNotNull (selEnd);
1623
1624 size_t wordStart = 0;
1625 size_t wordEnd = 0;
1626 bool wordReal = false;
1627 GetTextStore ().FindWordBreaks (*selStart, &wordStart, &wordEnd, &wordReal);
1628 *selStart = wordStart;
1629
1630 GetTextStore ().FindWordBreaks (*selEnd, &wordStart, &wordEnd, &wordReal);
1631#if qDoubleClickSelectsSpaceAfterWord
1632 if (wordReal) {
1633 // select the space forward...
1634 size_t xWordStart = 0;
1635 size_t xWordEnd = 0;
1636 bool xWordReal = false;
1637 GetTextStore ().FindWordBreaks (wordEnd, &xWordStart, &xWordEnd, &xWordReal);
1638 if (not xWordReal) {
1639 wordEnd = xWordEnd;
1640 }
1641 }
1642#endif
1643 *selEnd = wordEnd;
1644}
1645
1646/*
1647@METHOD: TextInteractor::WhileTrackingConstrainSelection_ForWholeRows
1648@DESCRIPTION: <p>See @'TextInteractor::WhileTrackingConstrainSelection'</p>
1649*/
1650void TextInteractor::WhileTrackingConstrainSelection_ForWholeRows (size_t* selStart, size_t* selEnd)
1651{
1652 RequireNotNull (selStart);
1653 RequireNotNull (selEnd);
1654 Require (*selStart <= *selEnd);
1655
1656 size_t origSelStart = *selStart;
1657 size_t origSelEnd = *selEnd;
1658
1659 *selStart = GetStartOfRowContainingPosition (origSelStart);
1660 *selEnd = GetStartOfNextRowFromRowContainingPosition (*selEnd);
1661 // if at end of buffer, go to end of buffer. Else, select to start of next row
1662 if (*selEnd <= origSelEnd) {
1663 *selEnd = GetEndOfRowContainingPosition (origSelEnd);
1664 }
1665}
1666
1667/*
1668@METHOD: TextInteractor::GetCharAtClickLocation
1669@DESCRIPTION: <p>Just like @'TextImager::GetCharAtWindowLocation'(), but doesn't try to
1670 find the character which encloses the particular point. Rather - we try to
1671 find the character the user was probably trying
1672 to click at to position the cursor. This is typically either the same, or the character
1673 just just following. If we are clicking towards the end of the character - we probably
1674 wanted to click just <em>before</em> the next character. Note this works for BIDI characters.</p>
1675*/
1676size_t TextInteractor::GetCharAtClickLocation (const Led_Point& where) const
1677{
1678 size_t clickedOnChar = GetCharAtWindowLocation (where);
1679 size_t endOfClickedRow = GetEndOfRowContainingPosition (clickedOnChar);
1680
1681 /*
1682 * SPR#1597 (was originally SPR#1232). A click past the end of the wrap point in a wrapped
1683 * line has a value EQUAL to the end of the row. However - its also EQUAL to the start of
1684 * the following row. The trouble is that GetEndOfRowContainingPosition (THAT_POSITION)
1685 * will return the end of the FOLLOWING row in that case.
1686 *
1687 * Since in order for this to happen there must be 1 or more characters in the row (to be a wrap point),
1688 * we can look at the PRECEEDING character, and compute ITS End-of-Row. If thats the same as the
1689 * returned click-position - we must have clicked on the end-of-row, and so we correct
1690 * the value of 'endOfClickedRow'.
1691 */
1692 if (GetEndOfRowContainingPosition (FindPreviousCharacter (clickedOnChar)) == clickedOnChar) {
1693 endOfClickedRow = clickedOnChar;
1694 }
1695
1696 Assert (GetStartOfRowContainingPosition (clickedOnChar) <= clickedOnChar);
1697 Assert (clickedOnChar <= endOfClickedRow);
1698
1699 if (clickedOnChar < endOfClickedRow) { // Don't wrap cuz click past end - LGP 950424
1700 Led_Rect charRect = GetCharWindowLocation (clickedOnChar);
1701 bool clickedToLHSOfChar = (where.h <= charRect.left + CoordinateType (charRect.GetWidth ()) / 2 and charRect.GetWidth () != 0);
1702 if (GetTextDirection (clickedOnChar) == eLeftToRight) {
1703 if (not clickedToLHSOfChar) {
1704 clickedOnChar = FindNextCharacter (clickedOnChar);
1705 }
1706 }
1707 else {
1708 if (clickedToLHSOfChar) {
1709 clickedOnChar = FindNextCharacter (clickedOnChar);
1710 }
1711 }
1712 }
1713 return (clickedOnChar);
1714}
1715
1716void TextInteractor::Draw (const Led_Rect& /*subsetToDraw*/, bool /*printing*/)
1717{
1718 NoteWindowPartiallyUpdated ();
1719 // don't call cuz there is none defined - pure virtual...TextImager::Draw (subsetToDraw, printing);
1720}
1721
1722void TextInteractor::DrawBefore (const Led_Rect& /*subsetToDraw*/, bool /*printing*/)
1723{
1724 // LGP 2003-03-20 - not 100% sure this is necessary or a good idea - but done to keep backward
1725 // compat with old behavior. Once new idle code in place - test without this. See SPR#1366.
1726 if (not fScrollBarParamsValid) {
1727 UpdateScrollBars ();
1728 }
1729}
1730
1731void TextInteractor::DrawAfter (const Led_Rect& /*subsetToDraw*/, bool printing)
1732{
1733 if (GetUseSecondaryHilight () and not GetSelectionShown () and not printing) {
1734 Region r;
1735 GetSelectionWindowRegion (&r, GetSelectionStart (), GetSelectionEnd ());
1736 Tablet_Acquirer tablet_ (this);
1737 Tablet* tablet = tablet_;
1738 tablet->FrameRegion (r, Led_GetSelectedTextBackgroundColor ());
1739 }
1740}
1741
1742void TextInteractor::SetTopRowInWindow (size_t newTopRow, UpdateMode updateMode)
1743{
1744 TemporarilySetUpdateMode updateModeSetter (*this, updateMode);
1745 TextImager* tim = this; // Dynamicly bind to 1-arg version. Direct call would select overloaded version from this class!
1746 tim->SetTopRowInWindow (newTopRow);
1747}
1748
1749void TextInteractor::ScrollByIfRoom (ptrdiff_t downBy, UpdateMode updateMode)
1750{
1751 TemporarilySetUpdateMode updateModeSetter (*this, updateMode);
1752 TextImager* tim = this; // Dynamicly bind to 1-arg version. Direct call would select overloaded version from this class!
1753 tim->ScrollByIfRoom (downBy);
1754}
1755
1756void TextInteractor::ScrollSoShowing (size_t markerPos, size_t andTryToShowMarkerPos, UpdateMode updateMode)
1757{
1758 TemporarilySetUpdateMode updateModeSetter (*this, updateMode);
1759 TextImager* tim = this; // Dynamicly bind to 2-arg version. Direct call would select overloaded version from this class!
1760 tim->ScrollSoShowing (markerPos, andTryToShowMarkerPos);
1761}
1762
1763void TextInteractor::SetDefaultFont (const IncrementalFontSpecification& defaultFont, UpdateMode updateMode)
1764{
1765 TemporarilySetUpdateMode updateModeSetter{*this, updateMode};
1766 TextImager* tim = this; // Dynamicly bind to 1-arg version. Direct call would select overloaded version from this class!
1767 tim->SetDefaultFont (defaultFont);
1768 Refresh ();
1769}
1770
1771void TextInteractor::SetWindowRect (const Led_Rect& windowRect, UpdateMode updateMode)
1772{
1773 TemporarilySetUpdateMode updateModeSetter (*this, updateMode);
1774 TextImager* tim = this; // Dynamicly bind to 1-arg version. Direct call would select overloaded version from this class!
1775 tim->SetWindowRect (windowRect);
1776}
1777
1778void TextInteractor::SetHScrollPos (CoordinateType hScrollPos)
1779{
1780 PreScrollInfo preScrollInfo;
1781 PreScrollHelper (eDefaultUpdate, &preScrollInfo);
1782 TextImager::SetHScrollPos (hScrollPos);
1783 PostScrollHelper (preScrollInfo);
1784}
1785
1786void TextInteractor::SetHScrollPos (CoordinateType hScrollPos, UpdateMode updateMode)
1787{
1788 TemporarilySetUpdateMode updateModeSetter (*this, updateMode);
1789 TextImager* tim = this; // Dynamicly bind to 1-arg version. Direct call would select overloaded version from this class!
1790 tim->SetHScrollPos (hScrollPos);
1791}
1792
1793void TextInteractor::PreScrollHelper (UpdateMode updateMode, PreScrollInfo* preScrollInfo)
1794{
1795 UpdateMode realUpdateMode = RealUpdateMode (updateMode);
1796 preScrollInfo->fUpdateMode = realUpdateMode;
1797 if (realUpdateMode != eNoUpdate) {
1798 preScrollInfo->fOldWindowStart = GetMarkerPositionOfStartOfWindow ();
1799 preScrollInfo->fOldHScrollPos = GetHScrollPos ();
1800 preScrollInfo->fTryTodoScrollbits = GetUseBitmapScrollingOptimization () and bool (realUpdateMode == eImmediateUpdate);
1801 if (preScrollInfo->fTryTodoScrollbits) {
1802 preScrollInfo->fOldLastRowStart = GetMarkerPositionOfStartOfLastRowOfWindow ();
1803 try {
1804 Update (false); // so the stuff we scroll-bits is up to date...
1805 // Wouldn't want to scroll stale bits :-)
1806 }
1807 catch (...) {
1808 preScrollInfo->fTryTodoScrollbits = false;
1809 }
1810 }
1811 }
1812}
1813
1814void TextInteractor::PostScrollHelper (PreScrollInfo preScrollInfo)
1815{
1816 /*
1817 * Only if we scrolled do we need to refresh screen.
1818 */
1819 size_t newStartOfWindow = GetMarkerPositionOfStartOfWindow ();
1820 preScrollInfo.fUpdateMode = RealUpdateMode (preScrollInfo.fUpdateMode);
1821 if (preScrollInfo.fUpdateMode != eNoUpdate and
1822 ((preScrollInfo.fOldWindowStart != newStartOfWindow) or (preScrollInfo.fOldHScrollPos != GetHScrollPos ()))) {
1823
1824 // Don't try this except if we only got vertical scrolling - at least for now...
1825 if (preScrollInfo.fTryTodoScrollbits and preScrollInfo.fOldHScrollPos == GetHScrollPos ()) {
1826 Led_Rect windowRect = GetWindowRect ();
1827
1828 Tablet_Acquirer tablet_{this};
1829 Tablet* tablet = tablet_;
1830 if (preScrollInfo.fOldWindowStart > newStartOfWindow) {
1831 /* *
1832 * Move text (bits) DOWN screen (UP /|\ ARROW).
1833 * |
1834 * |
1835 */
1836 CoordinateType newPos = GetCharWindowLocation (GetStartOfRowContainingPosition (preScrollInfo.fOldWindowStart)).top;
1837 if (newPos > 0) {
1838 try {
1839 tablet->ScrollBitsAndInvalRevealed (windowRect, newPos - windowRect.top);
1840 }
1841 catch (...) {
1842 // Ignore any errors - just don't do scrollbits then...
1843 preScrollInfo.fTryTodoScrollbits = false;
1844 }
1845 }
1846 else {
1847 preScrollInfo.fTryTodoScrollbits = false; // can happen, for example, if only one row fits in window...
1848 }
1849 if (preScrollInfo.fTryTodoScrollbits) {
1850 /*
1851 * Very subtle speed hack. It turns out, in this case, we may need to update a sliver on the top
1852 * of the window (exposed by scrollbits), and the bottom of the window (cuz we don't show partial
1853 * rows). So the BOUNDING RECTANLGE of the update region is the whole window. Why is this
1854 * a problem? In our update code, we use the bounding rectangle of the update region for
1855 * logical clipping. This means we end up drawing (though clipped out) every row of text.
1856 * This can make the scroll operation needlessly slow. By simply doing the update of the
1857 * top sliver first, we assure we always have a nice rectangular update region, so our
1858 * later optimizations work better. -- LGP 961030
1859 */
1860 Update ();
1861
1862 // Now we may not want to allow the partial line to be displayed. Leave that choice to the
1863 // logic in the imager, and repaint the area past the end of the last row
1864 CoordinateType lastRowBottom = GetCharWindowLocation (GetMarkerPositionOfStartOfLastRowOfWindow ()).bottom;
1865 Led_Rect eraser = windowRect;
1866 eraser.top = lastRowBottom;
1867 RefreshWindowRect (eraser, preScrollInfo.fUpdateMode);
1868 }
1869 }
1870 else {
1871 /*
1872 * Move text (bits) UP screen (DOWN | ARROW).
1873 * |
1874 * \|/
1875 * *
1876 */
1877 CoordinateType newPos = GetCharLocationRowRelativeByPosition (newStartOfWindow, preScrollInfo.fOldWindowStart, 5).top;
1878
1879 if (newPos > 0) {
1880 try {
1881 tablet->ScrollBitsAndInvalRevealed (windowRect, -newPos);
1882 }
1883 catch (...) {
1884 // Ignore any errors - just don't do scrollbits then...
1885 preScrollInfo.fTryTodoScrollbits = false;
1886 }
1887 }
1888 else {
1889 preScrollInfo.fTryTodoScrollbits = false; // can happen, for example, if only one row fits in window...
1890 }
1891
1892 if (preScrollInfo.fTryTodoScrollbits) {
1893 // now refresh the exposed portion at the bottom. Note that much of it may have been exposed
1894 // already by the InvalRgn/ScrollWindow calls. But we may have to invalidate even more cuz we don't
1895 // show entire bottom lines.
1896 CoordinateType lastRowBottom = GetCharWindowLocation (preScrollInfo.fOldLastRowStart).bottom;
1897 Led_Rect eraser = windowRect;
1898 eraser.top = lastRowBottom;
1899 RefreshWindowRect (eraser, eDelayedUpdate);
1900 }
1901
1902 if (preScrollInfo.fUpdateMode == eImmediateUpdate) {
1903 Update ();
1904 }
1905 }
1906
1907 // ScrollBitsing succeeded, so we can return now, and avoid the below REFRESH.
1908 // Otherwise, fall through, and handle things the old-fashioned way...
1909 if (preScrollInfo.fTryTodoScrollbits) {
1910 return;
1911 }
1912 }
1913
1914 Refresh (preScrollInfo.fUpdateMode);
1915 }
1916}
1917
1918void TextInteractor::Replace (size_t from, size_t to, const Led_tChar* withWhat, size_t withWhatCharCount, UpdateMode updateMode)
1919{
1920 if (from != to or withWhatCharCount != 0) {
1921 Assert (fDoingUpdateModeReplaceOn == nullptr);
1922 fDoingUpdateModeReplaceOn = this;
1923 try {
1924 PreReplaceInfo preReplaceInfo;
1925 PreReplace (from, to, withWhatCharCount, updateMode, &preReplaceInfo);
1926 GetTextStore ().Replace (from, to, withWhat, withWhatCharCount);
1927 PostReplace (preReplaceInfo);
1928 Assert (fDoingUpdateModeReplaceOn == this);
1929 }
1930 catch (...) {
1931 Assert (fDoingUpdateModeReplaceOn == this);
1932 fDoingUpdateModeReplaceOn = nullptr;
1933 throw;
1934 }
1935 fDoingUpdateModeReplaceOn = nullptr;
1936 }
1937}
1938
1939void TextInteractor::AboutToUpdateText (const MarkerOwner::UpdateInfo& updateInfo)
1940{
1941 TextImager::AboutToUpdateText (updateInfo);
1942 if (fDoingUpdateModeReplaceOn != this) {
1943 // Sometimes a textstore sends notifications about updates beyond the end of the text (e.g. when setting
1944 // styles which apply to newly typed characters). We only pay attention up to the end of the text
1945 // (since that is all we display)
1946 PreReplace (updateInfo.fReplaceFrom, min (updateInfo.fReplaceTo, GetEnd ()), updateInfo.fTextLength, eDefaultUpdate, &fTmpPreReplaceInfo);
1947 }
1948}
1949
1950void TextInteractor::DidUpdateText (const UpdateInfo& updateInfo) noexcept
1951{
1952 TextImager::DidUpdateText (updateInfo);
1953 if (fDoingUpdateModeReplaceOn != this) {
1954 try {
1955 PostReplace (fTmpPreReplaceInfo);
1956 }
1957 catch (...) {
1958 Refresh (); // shouldn't happen? But if it does - this is probably the best we can do to handle it...
1959 }
1960 }
1961}
1962
1963void TextInteractor::PreReplace (size_t from, size_t to, size_t withWhatCharCount, UpdateMode updateMode, PreReplaceInfo* preReplaceInfo)
1964{
1965 RequireNotNull (preReplaceInfo);
1966
1967 if (preReplaceInfo->fTextInteractor != nullptr) { // used as flag to indicate cleaned up state (marker removed)
1968 AbortReplace (*preReplaceInfo);
1969 }
1970
1971 updateMode = RealUpdateMode (updateMode);
1972
1973 preReplaceInfo->fFrom = from;
1974 preReplaceInfo->fTo = to;
1975 preReplaceInfo->fWithWhatCharCount = withWhatCharCount;
1976 preReplaceInfo->fUpdateMode = updateMode;
1977
1978 if (updateMode == eNoUpdate) {
1979 return;
1980 }
1981
1982 if (updateMode == eDelayedUpdate and IsWholeWindowInvalid ()) {
1983 preReplaceInfo->fUpdateMode = eNoUpdate;
1984 return;
1985 }
1986
1987 try {
1988 /*
1989 * Every once in a while, somebody will try todo an edit operation outside of
1990 * the visible window. Though this is rare, when it DOES happen, there are frequently
1991 * several of them (as in reading in a file). So we attempt to specially tweek this
1992 * case. Do a preliminary and then secondary test so we don't slow needlessly waste time
1993 * for the more common case.
1994 *
1995 * Note - we could also just as reasonably tweek the case where we are inserting BEFORE
1996 * the start of the window, but I've found no cases where that comes up, so I don't bother
1997 * for now.
1998 *
1999 * LGP 960515.
2000 */
2001 {
2002 size_t endOfWindow = GetMarkerPositionOfEndOfWindow ();
2003 if (from > endOfWindow) {
2004 /*
2005 * Adding text JUST after the end of the window could - in principle - affect
2006 * the word-breaks of the end of window, so we must be a little more careful.
2007 */
2008 size_t endOfNextRow = GetEndOfRowContainingPosition (FindNextCharacter (endOfWindow));
2009 if (from > endOfNextRow) {
2010 updateMode = eNoUpdate;
2011 preReplaceInfo->fUpdateMode = eNoUpdate;
2012 return;
2013 }
2014 }
2015 }
2016
2017 Assert (updateMode != eNoUpdate);
2018 /*
2019 * Grab a range that includes totally all the rows between the start selection and the end.
2020 *
2021 * Save this region in a Marker (so it gets its bounds adjusted for the edit). Then compute
2022 * its pixelBounds (really we just need its HEIGHT). We will use this later to see
2023 * if the edit has caused a change in the pixel-height of the region, which would mean we
2024 * have to draw not just that region, but all the way to the end of the window.
2025 *
2026 * Also we will use it later to decide if the starting or ending row (word-breaks) have
2027 * changed.
2028 */
2029 size_t startPositionOfRowWhereReplaceBegins = GetStartOfRowContainingPosition (from);
2030 size_t startPositionOfRowAfterReplaceEnds = GetEndOfRowContainingPosition (to);
2031 if (startPositionOfRowAfterReplaceEnds < GetTextStore ().GetEnd ()) {
2032 startPositionOfRowAfterReplaceEnds = GetStartOfRowContainingPosition (FindNextCharacter (startPositionOfRowAfterReplaceEnds));
2033 Assert (GetEndOfRowContainingPosition (to) <= startPositionOfRowAfterReplaceEnds);
2034 }
2035 Assert (startPositionOfRowWhereReplaceBegins <= startPositionOfRowAfterReplaceEnds);
2036
2037 preReplaceInfo->fBoundingUpdateHeight =
2038 GetTextWindowBoundingRect (startPositionOfRowWhereReplaceBegins, startPositionOfRowAfterReplaceEnds).GetHeight ();
2039
2040 /*
2041 * In case the above changes, we may be able to get away with only updating the stable
2042 * typing region, assuming its height hasn't changed.
2043 */
2044 {
2045 size_t expandedFromMarkerPos = 0;
2046 size_t expandedToMarkerPos = 0;
2047 GetStableTypingRegionContaingMarkerRange (from, to, &expandedFromMarkerPos, &expandedToMarkerPos);
2048 if (expandedFromMarkerPos == startPositionOfRowWhereReplaceBegins and expandedToMarkerPos == startPositionOfRowAfterReplaceEnds) {
2049 // Speed tweek. Avoid expensive call to GetTextWindowBoundingRect () - and use old value...
2050 preReplaceInfo->fStableTypingRegionHeight = preReplaceInfo->fBoundingUpdateHeight;
2051 Assert (preReplaceInfo->fStableTypingRegionHeight == GetTextWindowBoundingRect (expandedFromMarkerPos, expandedToMarkerPos).GetHeight ());
2052 }
2053 else {
2054 preReplaceInfo->fStableTypingRegionHeight = GetTextWindowBoundingRect (expandedFromMarkerPos, expandedToMarkerPos).GetHeight ();
2055 }
2056 }
2057
2058 /*
2059 * The marker is one past the end of the final row so any typing just after the end of the row
2060 * gets included.
2061 */
2062 GetTextStore ().AddMarker (&preReplaceInfo->fBoundingUpdateMarker, startPositionOfRowWhereReplaceBegins,
2063 (startPositionOfRowAfterReplaceEnds - startPositionOfRowWhereReplaceBegins) + 1, this);
2064 preReplaceInfo->fTextInteractor = this; // assign after add, cuz this var serves as flag to indicate addMarker call done...
2065 }
2066 catch (NotFullyInitialized&) {
2067 // Fine - we can ignore that..
2068 preReplaceInfo->fUpdateMode = eNoUpdate;
2069 return;
2070 }
2071 catch (...) {
2072 throw;
2073 }
2074}
2075
2076void TextInteractor::PostReplace (PreReplaceInfo& preReplaceInfo)
2077{
2078 UpdateMode updateMode = preReplaceInfo.fUpdateMode;
2079 if (updateMode != eNoUpdate) {
2080 size_t from = preReplaceInfo.fFrom;
2081 size_t withWhatCharCount = preReplaceInfo.fWithWhatCharCount;
2082
2083 size_t newTo = from + withWhatCharCount;
2084
2085 // Subtract one from end cuz we added one earlier so chars appended would grow marker...
2086 size_t startPositionOfRowWhereReplaceBegins = preReplaceInfo.fBoundingUpdateMarker.GetStart ();
2087 size_t startPositionOfRowAfterReplaceEnds = preReplaceInfo.fBoundingUpdateMarker.GetEnd () - 1;
2088 Assert (startPositionOfRowWhereReplaceBegins <= startPositionOfRowAfterReplaceEnds);
2089
2090 size_t stableTypingRegionStart = 0;
2091 size_t stableTypingRegionEnd = 0;
2092 GetStableTypingRegionContaingMarkerRange (from, newTo, &stableTypingRegionStart, &stableTypingRegionEnd);
2093
2094 size_t expandedFromMarkerPos = 0;
2095 size_t expandedToMarkerPos = 0;
2096 ExpandedFromAndToInPostReplace (from, newTo, stableTypingRegionStart, stableTypingRegionEnd, startPositionOfRowWhereReplaceBegins,
2097 startPositionOfRowAfterReplaceEnds, &expandedFromMarkerPos, &expandedToMarkerPos);
2098
2099 Led_Rect windowRect = GetWindowRect ();
2100 Led_Rect expandedFromToMarkerRect = GetTextWindowBoundingRect (expandedFromMarkerPos, expandedToMarkerPos);
2101 Led_Rect updateRect = expandedFromToMarkerRect;
2102
2103 // we must ALWAYS draw to the end of the row (including space after last character)
2104 updateRect.right = windowRect.right;
2105
2106 // Now if we've changed the height of the bounding rows region, we need to repaint to end.
2107 {
2108 Led_Rect revisedRect = expandedFromToMarkerRect; // Speed tweek. Avoid expensive call to GetTextWindowBoundingRect () - and use old value...
2109 if (expandedFromMarkerPos != startPositionOfRowWhereReplaceBegins or expandedToMarkerPos != startPositionOfRowAfterReplaceEnds) {
2110 revisedRect = GetTextWindowBoundingRect (startPositionOfRowWhereReplaceBegins, startPositionOfRowAfterReplaceEnds);
2111 }
2112 Assert (revisedRect == GetTextWindowBoundingRect (startPositionOfRowWhereReplaceBegins, startPositionOfRowAfterReplaceEnds));
2113
2114 if (preReplaceInfo.fBoundingUpdateHeight != revisedRect.GetHeight ()) {
2115 updateRect = Led_Rect (updateRect.top, windowRect.left, windowRect.bottom - updateRect.top, windowRect.GetWidth ());
2116 }
2117 }
2118
2119 // Now if we've changed the height of the stable region, we need to repaint to end of screen.
2120 {
2121 Led_Rect revisedRect = expandedFromToMarkerRect; // Speed tweek. Avoid expensive call to GetTextWindowBoundingRect () - and use old value...
2122 if (expandedFromMarkerPos != stableTypingRegionStart or expandedToMarkerPos != stableTypingRegionEnd) {
2123 revisedRect = GetTextWindowBoundingRect (stableTypingRegionStart, stableTypingRegionEnd);
2124 }
2125 Assert (revisedRect == GetTextWindowBoundingRect (stableTypingRegionStart, stableTypingRegionEnd));
2126 if (preReplaceInfo.fStableTypingRegionHeight != revisedRect.GetHeight ()) {
2127 updateRect = Led_Rect (updateRect.top, windowRect.left, windowRect.bottom - updateRect.top, windowRect.GetWidth ());
2128 }
2129 }
2130
2131 RefreshWindowRect (updateRect, updateMode);
2132 }
2133}
2134
2135void TextInteractor::AbortReplace (PreReplaceInfo& preReplaceInfo)
2136{
2137 if (preReplaceInfo.fTextInteractor != nullptr) {
2138 // remove marker, and set to nullptr to indicate cleaned up.
2139 preReplaceInfo.fTextInteractor->GetTextStore ().RemoveMarker (&preReplaceInfo.fBoundingUpdateMarker);
2140 preReplaceInfo.fTextInteractor = nullptr;
2141 }
2142}
2143
2144void TextInteractor::ExpandedFromAndToInPostReplace (size_t from, size_t newTo, size_t stableTypingRegionStart,
2145 size_t stableTypingRegionEnd, size_t startPositionOfRowWhereReplaceBegins,
2146 size_t startPositionOfRowAfterReplaceEnds, size_t* expandedFrom, size_t* expandedTo)
2147{
2148 RequireNotNull (expandedFrom);
2149 RequireNotNull (expandedTo);
2150
2151 // Edits in a row practically never change the word-break from the previous line, but they CAN - in
2152 // principle - (eg. in the second row there is a very long word, and you insert a space near the
2153 // beginning, the little word-let created might fit on the previous row, in which case you would
2154 // need to redisplay BOTH rows. But we only need to update previous rows or earlier text in this
2155 // row if the row-break point changes - thats what our marker-test here measures.
2156 size_t expandedFromMarkerPos = 0;
2157 if (GetStartOfRowContainingPosition (from) == startPositionOfRowWhereReplaceBegins) {
2158 expandedFromMarkerPos = from;
2159
2160 // On windows - at least temporarily - editing one char can change the WIDTH/MeasureText
2161 // of the preceeding character, since we don't need to add on the overhang anymore.
2162 // So be sure to draw that previous character in that subtle case
2163 // - See SPR# 0340 - LGP 960516
2164 if (expandedFromMarkerPos > startPositionOfRowWhereReplaceBegins) {
2165 expandedFromMarkerPos = FindPreviousCharacter (expandedFromMarkerPos);
2166 }
2167 }
2168 else {
2169 // practically never happens....
2170 expandedFromMarkerPos = stableTypingRegionStart;
2171 }
2172
2173 // Edits much more commonly (though still only a few percent of edits) can change the ending
2174 // row word-break line. A change here means we must redraw to the end of the stable-region
2175 // (typically line - aka paragraph).
2176 size_t expandedToMarkerPos = GetEndOfRowContainingPosition (newTo);
2177 {
2178 size_t nowStartOfNextRow = expandedToMarkerPos;
2179 if (nowStartOfNextRow < GetTextStore ().GetEnd ()) {
2180 nowStartOfNextRow = GetStartOfRowContainingPosition (FindNextCharacter (nowStartOfNextRow));
2181 Assert (expandedToMarkerPos <= nowStartOfNextRow);
2182 }
2183
2184 if (nowStartOfNextRow != startPositionOfRowAfterReplaceEnds) {
2185 // then we changed word-breaks! Must go all the way to the end of the stable region.
2186 expandedToMarkerPos = stableTypingRegionEnd;
2187 }
2188 }
2189
2190 *expandedFrom = expandedFromMarkerPos;
2191 *expandedTo = expandedToMarkerPos;
2192}
2193
2194void TextInteractor::InteractiveReplace (const Led_tChar* withWhat, size_t withWhatCharCount, UpdateMode updateMode)
2195{
2196 BreakInGroupedCommandsIfDifferentCommand (GetCommandNames ().fTypingCommandName);
2197 InteractiveModeUpdater iuMode{*this};
2198 UndoableContextHelper undoContext{*this, GetCommandNames ().fTypingCommandName, withWhatCharCount == 0};
2199 {
2200 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), withWhat, withWhatCharCount, true, true, updateMode);
2201 bool anyChanges = InteractiveReplaceEarlyPostReplaceHook (withWhatCharCount);
2202 if (withWhatCharCount == 1 and not anyChanges) {
2203 // need other tests as well???? Like same start location ?? Maybe done inside command-handler stuff????
2204 undoContext.SetSimplePlainTextInsertOptimization (true);
2205 }
2206 }
2207 undoContext.CommandComplete ();
2208}
2209
2210/*
2211@METHOD: TextInteractor::InteractiveReplace_
2212@DESCRIPTION:
2213*/
2214void TextInteractor::InteractiveReplace_ (size_t from, size_t to, const Led_tChar* withWhat, size_t withWhatCharCount,
2215 bool updateCursorPosition, bool validateTextForCharsetConformance, UpdateMode updateMode)
2216{
2217 // Assert selection bounardaries valid Led_tChar boundaries
2218 if (validateTextForCharsetConformance) {
2219 // Then check the GIVEN text - no assert here, just bad_input if text bad...
2220 if (not ValidateTextForCharsetConformance (withWhat, withWhatCharCount)) {
2221 OnBadUserInput ();
2222 return; // in case OnBadUserInput () doesn't throw
2223 }
2224 }
2225
2226 TempMarker newSel{GetTextStore (), to + 1, to + 1}; // NB: This marker pos MAY split a character - but that should cause
2227 // no problems.
2228 // We are only keeping this temporarily and we subtract one at the
2229 // end. Just want to make it past point where we do the insertion
2230 // so we get the right adjustment on
2231
2232 Tablet_Acquirer performanceHackTablet{this}; // sometimes acquiring/releaseing the tablet can be expensive,
2233 // and as a result of stuff later in this call, it happens
2234 // several times. By acquiring it here, we make the other calls
2235 // much cheaper (cuz its basicly free to acquire when already
2236 // acuired).
2237
2238 SetCaretShownAfterPos (true); // by default show pos after - this is what works best for NL's and is what we
2239 // want most of the time...
2240 Replace (from, to, withWhat, withWhatCharCount, updateMode);
2241 if (updateCursorPosition) {
2242 size_t newSelection = newSel.GetStart ();
2243 if (newSelection > 0) {
2244 --newSelection;
2245 }
2246
2247 SetSelection (newSelection, newSelection);
2248 }
2249}
2250
2251/*
2252@METHOD: TextInteractor::InteractiveReplaceEarlyPostReplaceHook
2253@DESCRIPTION: <p>Hook function called AFTER the @'TextInteractor::InteractiveReplace_' in @'TextInteractor::InteractiveReplace', but
2254 <em>before</em> the @'TextInteractor::PreInteractiveUndoHelper' call. The need to get into this interval is if you need to update
2255 the text to <em>augment</em> the effect of the user typing, and what that effect to be reflected in the UNDO information.</p>
2256 <p>This happens, for example, with @'StandardStyledTextInteractor::InteractiveReplaceEarlyPostReplaceHook' where
2257 we want to set the font of the newly typed character according to the 'fEmptySelection' style. See spr#0604 for more details.</p>
2258 <p><em>OLD-CODE-NOTE</em>NB: This routine was changed in Led 3.1a8 to return a boolean result.</p>
2259 </p>The boolean return value must be true if any changes were made that could affect undo processing (i.e. that could affect
2260 what infomration must be saved for proper undo processing).</p>
2261*/
2262bool TextInteractor::InteractiveReplaceEarlyPostReplaceHook (size_t /*withWhatCharCount*/)
2263{
2264 return false;
2265}
2266
2267/*
2268@METHOD: TextInteractor::PreInteractiveUndoHelper
2269@DESCRIPTION: <p>This is called early on - before any change happens - to preserve the contents of a region about to be updated - so
2270 that the region can be restored upon an UNDO command. The argument selStart/selEnd are NOT the actual selection regions - but
2271 rather the region (which maybe slightly larger) that needs to be preserved (perhaps the arg names should change?).</p>
2272 <p>Note - this can differ from the selection region because of SmartCutAndPaste - where we expand the selection slightly for
2273 a command to do funky stuff with whitespace.</p>
2274 <p>Note that the handed in selStart/selEnd arguments can be modified by this routine (due to a call to
2275 @'TextInteractor::PreInteractiveUndoHelperHook').</p>
2276 <p>We then preserve the ACTUAL selection at the time this was called in the resulting 'beforeRep' object.</p>
2277 <p>NB: new in Led 3.1a6 - we require that fCommandHandler != nullptr to call this.</p>
2278*/
2279void TextInteractor::PreInteractiveUndoHelper (InteractiveReplaceCommand::SavedTextRep** beforeRep, size_t regionStart, size_t regionEnd,
2280 size_t selStart, size_t selEnd)
2281{
2282 Require (regionStart <= regionEnd);
2283 Require (selStart <= selEnd);
2284 RequireNotNull (beforeRep);
2285 Require ((*beforeRep) == nullptr);
2286 RequireNotNull (fCommandHandler);
2287
2288 try {
2289 (*beforeRep) = InteractiveUndoHelperMakeTextRep (regionStart, regionEnd, selStart, selEnd);
2290 }
2291 catch (...) {
2292 // any exceptions in here are cuz we don't have enuf memory to make this
2293 // command undoable. No matter. Proceed anyhow, ignoring the exception...
2294 // And commit any existing commands to make it more likely real code
2295 // succeeds, and cuz we don't want any funny undo behavior where some
2296 // commands in sequence might get skipped.
2297 AssertNotNull (fCommandHandler);
2298 fCommandHandler->Commit ();
2299 }
2300}
2301
2302/*
2303@METHOD: TextInteractor::PostInteractiveUndoHelper
2304@DESCRIPTION: <p>This routine is called after a user action has taken place which is to be recorded for UNDOing.
2305 The 'startOfInsert' / 'endOfInsert' passed here refer to the region of text which must be preserved.
2306 The actual selection saved will be the currently selected text at the time this method is called.</p>
2307 <p>Note - the startOfInsert/endOfInsert can differ from the selection region because things like
2308 SmartCutAndPaste can expand the affected area of text to BEYOND what was actually selected by the user.</p>
2309 <p>This method operatates by calling @'TextInteractor::PostInteractiveUndoPostHelper' with the
2310 beforeRep argument given this function and an afterRep computed herein.</p>
2311 <p>NB: As of Led 3.1a6 - we require that fCommandHandler != nullptr to call this.</p>
2312*/
2313void TextInteractor::PostInteractiveUndoHelper (InteractiveReplaceCommand::SavedTextRep** beforeRep, size_t startOfInsert,
2314 size_t endOfInsert, const SDKString& cmdName)
2315{
2316 RequireNotNull (beforeRep);
2317 RequireNotNull (*beforeRep); // This shouldn't be called if there was a problem creating beforeRep (exception)
2318 RequireNotNull (fCommandHandler);
2319 SavedTextRep* afterRep = nullptr;
2320 try {
2321 afterRep = InteractiveUndoHelperMakeTextRep (startOfInsert, endOfInsert, GetSelectionStart (), GetSelectionEnd ());
2322 PostInteractiveUndoPostHelper (beforeRep, &afterRep, startOfInsert, cmdName);
2323 }
2324 catch (...) {
2325 delete (*beforeRep);
2326 (*beforeRep) = nullptr;
2327 delete afterRep;
2328 afterRep = nullptr;
2329 throw; // safe at this point to throw - but perhaps better to silently eat the throw?
2330 }
2331}
2332
2333/*
2334@METHOD: TextInteractor::PostInteractiveSimpleCharInsertUndoHelper
2335@DESCRIPTION: <p>Utility function for optimized undo support - keeping smaller objects in the undo buffer, and trying re-use/tweek
2336 an existing one in the common case of multiple consecutive characters typed.</p>
2337*/
2338void TextInteractor::PostInteractiveSimpleCharInsertUndoHelper (InteractiveReplaceCommand::SavedTextRep** beforeRep, size_t startOfInsert,
2339 size_t endOfInsert, const SDKString& cmdName)
2340{
2341 RequireNotNull (beforeRep);
2342 RequireNotNull (*beforeRep); // This shouldn't be called if there was a problem creating beforeRep (exception)
2343 RequireNotNull (fCommandHandler);
2344 if (endOfInsert - startOfInsert == 1) {
2345 Led_tChar c;
2346 CopyOut (startOfInsert, 1, &c);
2347 if (fCommandHandler->PostUpdateSimpleTextInsert (startOfInsert, c)) {
2348 delete *beforeRep;
2349 *beforeRep = nullptr;
2350 return;
2351 }
2352 // at least create a plain-text guy if we cannot update current one...
2353 SavedTextRep* afterRep = new InteractiveReplaceCommand::PlainTextRep (GetSelectionStart (), GetSelectionEnd (), &c, 1);
2354 PostInteractiveUndoPostHelper (beforeRep, &afterRep, startOfInsert, cmdName);
2355 Assert (afterRep == nullptr); // cleared out by PostInteractiveUndoPostHelper ()
2356 return;
2357 }
2358
2359 PostInteractiveUndoHelper (beforeRep, startOfInsert, endOfInsert, cmdName);
2360}
2361
2362/*
2363@METHOD: TextInteractor::PostInteractiveUndoPostHelper
2364@DESCRIPTION: <p>This routine is called after a user action has taken place which is to be recorded for UNDOing.
2365 The routine simply posts an @'InteractiveReplaceCommand' (with the already saved before/after reps) to the current
2366 command handler.</p>
2367 <p>This method is typically called by @'TextInteractor::PostInteractiveUndoHelper'.</p>
2368 <p>NB: As of Led 3.1a6 - we require that fCommandHandler != nullptr to call this.</p>
2369*/
2370void TextInteractor::PostInteractiveUndoPostHelper (InteractiveReplaceCommand::SavedTextRep** beforeRep,
2371 InteractiveReplaceCommand::SavedTextRep** afterRep, size_t startOfInsert, const SDKString& cmdName)
2372{
2373 RequireNotNull (beforeRep);
2374 RequireNotNull (afterRep);
2375 RequireNotNull (fCommandHandler);
2376 try {
2377 if (*beforeRep != nullptr and *afterRep != nullptr) {
2378 // We declare temporaries here, and be careful to set things to nullptr at each stage to prevent double
2379 // deletes in the event of a badly timed exception
2380 InteractiveReplaceCommand* cmd = new InteractiveReplaceCommand (*beforeRep, *afterRep, startOfInsert, cmdName);
2381 *beforeRep = nullptr;
2382 *afterRep = nullptr;
2383 fCommandHandler->Post (cmd);
2384 }
2385 }
2386 catch (...) {
2387 delete *beforeRep;
2388 *beforeRep = nullptr;
2389 delete *afterRep;
2390 *afterRep = nullptr;
2391 throw; // safe at this point to throw - but perhaps better to silently eat the throw?
2392 }
2393}
2394
2395InteractiveReplaceCommand::SavedTextRep* TextInteractor::InteractiveUndoHelperMakeTextRep (size_t regionStart, size_t regionEnd,
2396 size_t selStart, size_t selEnd)
2397{
2398 if (regionStart == regionEnd) {
2399 // optimization, cuz these are smaller
2400 return new InteractiveReplaceCommand::PlainTextRep (selStart, selEnd, nullptr, 0);
2401 }
2402 else {
2403 return new FlavorSavorTextRep (this, regionStart, regionEnd, selStart, selEnd);
2404 }
2405}
2406
2407void TextInteractor::OnUndoCommand ()
2408{
2409 InteractiveModeUpdater iuMode (*this);
2410 if (GetCommandHandler () != nullptr and GetCommandHandler ()->CanUndo ()) {
2411 GetCommandHandler ()->DoUndo (*this);
2412 ScrollToSelection ();
2413 }
2414 else {
2415 Led_BeepNotify ();
2416 }
2417}
2418
2419void TextInteractor::OnRedoCommand ()
2420{
2421 InteractiveModeUpdater iuMode (*this);
2422 if (GetCommandHandler () != nullptr and GetCommandHandler ()->CanRedo ()) {
2423 GetCommandHandler ()->DoRedo (*this);
2424 ScrollToSelection ();
2425 }
2426 else {
2427 Led_BeepNotify ();
2428 }
2429}
2430
2431void TextInteractor::Refresh (size_t from, size_t to, UpdateMode updateMode) const
2432{
2433 Require (from <= to);
2434 updateMode = RealUpdateMode (updateMode);
2435 if ((updateMode != eNoUpdate) and (from != to)) {
2436 if (updateMode == eDelayedUpdate and IsWholeWindowInvalid ()) {
2437 return;
2438 }
2439 // we could be more precise - but no need. Just take the box bounding the two
2440 // endpoints.
2441 // I'd be more inclined to worry about optimizing this, but the most relevant plausible
2442 // usage of this routine - during typing - doesn't use it. So I believe we can get away with
2443 // being sloppy here - LGP 960516
2444 Led_Rect refreshRect = GetTextWindowBoundingRect (from, to);
2445 RefreshWindowRect_ (refreshRect, updateMode);
2446 }
2447}
2448
2449void TextInteractor::Refresh (const Marker* range, UpdateMode updateMode) const
2450{
2451 RequireNotNull (range);
2452 updateMode = RealUpdateMode (updateMode);
2453 if (updateMode != eNoUpdate) {
2454 if (updateMode == eDelayedUpdate and IsWholeWindowInvalid ()) {
2455 return;
2456 }
2457 Refresh (range->GetStart (), range->GetEnd (), updateMode);
2458 }
2459}
2460
2461/*
2462@METHOD: TextInteractor::DoSingleCharCursorEdit
2463@ACCESS: protected
2464@DESCRIPTION: <p>Helper routine for handling cursoring done by user.</p>
2465 */
2466void TextInteractor::DoSingleCharCursorEdit (CursorMovementDirection direction, CursorMovementUnit movementUnit,
2467 CursorMovementAction action, UpdateMode updateMode, bool scrollToSelection)
2468{
2469 IdleManager::NonIdleContext nonIdleContext;
2470
2471 size_t oldStartSel = GetSelectionStart ();
2472 size_t oldEndSel = GetSelectionEnd ();
2473 Assert (GetSelectionEnd () <= GetLength () + 1);
2474
2475 size_t newStartSel = oldStartSel;
2476 size_t newEndSel = oldEndSel;
2477
2478 UpdateMode useUpdateMode = (updateMode == eImmediateUpdate) ? eDelayedUpdate : updateMode;
2479
2480 GoalColumnRecomputerControlContext skipRecompute (*this, action == eCursorMoving and movementUnit == eCursorByRow and
2481 (direction == eCursorBack or direction == eCursorForward));
2482
2483 // In a couple of cases, we get burned by the ambiguity of location-specification at start and end
2484 // of row. This only matters when the users says navigate to start/end of row, and we happen to already
2485 // be there. In that special case, we will navigate to the start/end of the next row.
2486 //
2487 // This situation only occurs when we are at the end of a row which has been word-wrapped. This is because
2488 // in that special case, there is a flag - CaretShownAfterPos - which says if you show the caret before or
2489 // after the marker-pos of the selection.
2490 //
2491 // If we are in this situation, OVERRIDE the usual navigation logic.
2492 if (movementUnit == eCursorByRow and GetStartOfRowContainingPosition (newStartSel) == newStartSel) {
2493 if (GetCaretShownAfterPos ()) {
2494 if (direction == eCursorToEnd and GetEndOfRowContainingPosition (newStartSel) == newStartSel) {
2495 goto SkipNavigation;
2496 }
2497 }
2498 else {
2499 if (direction == eCursorToStart) {
2500 // fall through with usual 'to start of row' logic, but backup one characater within the row so
2501 // that code doesn't get fooled about which row we are on.
2502 newStartSel = FindPreviousCharacter (newStartSel);
2503 }
2504 if (direction == eCursorToEnd) {
2505 goto SkipNavigation;
2506 }
2507 }
2508 }
2509
2510 // Generally don't want to mess with the caret shown pos. But in these cases few cases,
2511 // the user is indicating which he'd prefer.
2512 // Perhaps there are more cases? Can we generalize/simplify?
2513 if (movementUnit == eCursorByChar) {
2514 SetCaretShownAfterPos (true);
2515 }
2516 if (movementUnit == eCursorByRow) {
2517 switch (direction) {
2518 case eCursorToStart:
2519 SetCaretShownAfterPos (true);
2520 break;
2521 case eCursorToEnd:
2522 SetCaretShownAfterPos (false);
2523 break;
2524 }
2525 }
2526
2527 if (action != eCursorExtendingSelection or (oldStartSel == oldEndSel)) {
2528 fLeftSideOfSelectionInteresting = (direction == eCursorBack or direction == eCursorToStart);
2529 }
2530 if (fLeftSideOfSelectionInteresting) {
2531 newStartSel = ComputeRelativePosition (newStartSel, direction, movementUnit);
2532 if (action == eCursorMoving) { // only case where we do this - destroy/extend we keep track of start
2533 newEndSel = newStartSel;
2534 }
2535 }
2536 else {
2537 newEndSel = ComputeRelativePosition (newEndSel, direction, movementUnit);
2538 if (action == eCursorMoving) { // only case where we do this - destroy/extend we keep track of start
2539 newStartSel = newEndSel;
2540 }
2541 }
2542
2543SkipNavigation:
2544
2545 // The above can reverse start/end, so make sure that doesn't happen...
2546 if (newEndSel < newStartSel) {
2547 size_t tmp = newStartSel;
2548 newStartSel = newEndSel;
2549 newEndSel = tmp;
2550 }
2551 Assert (newStartSel <= newEndSel);
2552
2553 /*
2554 * Now that we know the new and old selection region, we can perform the action
2555 */
2556 switch (action) {
2557 case eCursorDestroying: {
2558 if (oldStartSel == oldEndSel) {
2559 /*
2560 * In this case, then the computations above for the NEW selection
2561 * can be considered valid.
2562 */
2563 Assert (newEndSel >= newStartSel);
2564 size_t howMany = newEndSel - newStartSel;
2565 if (howMany >= 1) { // might be zero if we were backed up against the start of the buffer.
2566 bool oldCutAndPaste = GetSmartCutAndPasteMode ();
2567 try {
2568 SetSmartCutAndPasteMode (false); // See SPR#1044 - when user hits backspace and has empty selection, dont use smart cut and paste...
2569 {
2570 BreakInGroupedCommandsIfDifferentCommand (GetCommandNames ().fClearCommandName);
2571 InteractiveModeUpdater iuMode (*this);
2572 UndoableContextHelper undoContext (*this, GetCommandNames ().fClearCommandName, newStartSel, newEndSel,
2573 GetSelectionStart (), GetSelectionEnd (), true);
2574 {
2575 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), LED_TCHAR_OF (""),
2576 0, true, true, useUpdateMode);
2577 }
2578 undoContext.CommandComplete ();
2579 }
2580 // After the above deletion, we already end up the the selection adjustment being handled for us (and more correctly
2581 // than we can easily due to smart cut and paste). So just grab the current (already correct) selection, to prevent
2582 // our later call to SetSelection () from doing any harm.
2583 GetSelection (&newStartSel, &newEndSel);
2584 SetSmartCutAndPasteMode (oldCutAndPaste);
2585 }
2586 catch (...) {
2587 SetSmartCutAndPasteMode (oldCutAndPaste);
2588 throw;
2589 }
2590 }
2591 }
2592 else {
2593 /*
2594 * Otherwise, if there WAS some selection, and we get any kind of
2595 * delete key, we REALLY just want to delete the selection, and
2596 * ignore whatever computation was done above for where to put
2597 * the new selection - it goes right to the old startSel.
2598 */
2599 Assert (oldEndSel >= oldStartSel);
2600 Assert (oldStartSel == GetSelectionStart ());
2601 Assert (oldEndSel == GetSelectionEnd ());
2602 OnClearCommand ();
2603 // After the above deletion, we already end up the the selection adjustment being handled for us (and more correctly
2604 // than we can easily due to smart cut and paste). So just grab the current (already correct) selection, to prevent
2605 // our later call to SetSelection () from doing any harm.
2606 GetSelection (&newStartSel, &newEndSel);
2607 }
2608 } break;
2609
2610 case eCursorMoving: {
2611 // Nothing todo (actual setting of selection done at the end)
2612 } break;
2613
2614 case eCursorExtendingSelection: {
2615 // Nothing todo (actual setting of selection done at the end)
2616 } break;
2617
2618 default:
2619 Assert (false); // bad direction argument
2620 }
2621
2622 /*
2623 * Buy this point, we've computed where we should be, and performed
2624 * any actions on the text that needed to be taken (e.g. deleting).
2625 * Nothing should have yet been redisplayed. Now we update the selection,
2626 * perform any needed scrolling, and only then - display - if prescribed.
2627 */
2628 SetSelection (newStartSel, newEndSel, useUpdateMode);
2629
2630 if (scrollToSelection) {
2631 ScrollToSelection (useUpdateMode, true);
2632 }
2633
2634 if (updateMode == eImmediateUpdate) {
2635 Update ();
2636 }
2637}
2638
2639void TextInteractor::OnCutCommand ()
2640{
2641 InteractiveModeUpdater iuMode (*this);
2642 BreakInGroupedCommands ();
2643 if (GetSelectionStart () != GetSelectionEnd ()) {
2644 OnCopyCommand ();
2645 UndoableContextHelper undoContext (*this, GetCommandNames ().fCutCommandName, true);
2646 {
2647 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), LED_TCHAR_OF (""), 0);
2648 }
2649 undoContext.CommandComplete ();
2650 }
2651 BreakInGroupedCommands ();
2652}
2653
2654void TextInteractor::OnCopyCommand ()
2655{
2656 size_t start = GetSelectionStart ();
2657 size_t end = GetSelectionEnd ();
2658 Assert (start <= end);
2659 if (start < end) {
2660 BreakInGroupedCommands ();
2661
2662 if (OnCopyCommand_Before ()) {
2663 try {
2664 OnCopyCommand_CopyFlavors ();
2665 }
2666 catch (...) {
2667 OnCopyCommand_After ();
2668 throw;
2669 }
2670 OnCopyCommand_After ();
2671 }
2672 }
2673}
2674
2675void TextInteractor::OnPasteCommand ()
2676{
2677 InteractiveModeUpdater iuMode (*this);
2678 BreakInGroupedCommands ();
2679
2680 if (OnPasteCommand_Before ()) {
2681 try {
2682 UndoableContextHelper undoContext (*this, GetCommandNames ().fPasteCommandName, false);
2683 {
2684 OnPasteCommand_PasteBestFlavor ();
2685 }
2686 undoContext.CommandComplete ();
2687 }
2688 catch (...) {
2689 OnPasteCommand_After ();
2690 throw;
2691 }
2692 OnPasteCommand_After ();
2693 }
2694 BreakInGroupedCommands ();
2695}
2696
2697void TextInteractor::OnClearCommand ()
2698{
2699 InteractiveModeUpdater iuMode (*this);
2700 BreakInGroupedCommands ();
2701 UndoableContextHelper undoContext (*this, GetCommandNames ().fClearCommandName, true);
2702 {
2703 InteractiveReplace_ (undoContext.GetUndoRegionStart (), undoContext.GetUndoRegionEnd (), LED_TCHAR_OF (""), 0);
2704 }
2705 undoContext.CommandComplete ();
2706 BreakInGroupedCommands ();
2707}
2708
2709/*
2710@METHOD: TextInteractor::OnCopyCommand_Before
2711@ACCESS: protected
2712@DESCRIPTION: <p>Hook overriden by SDK-specific classes or templates which does special things in the before
2713 a clipboard <em>copy</em> operation can begin (like opening a clipboard object). Generally should not be
2714 called directly or overridden, except when implementing new SDK wrappers.</p>
2715 <p>Return false or throw if fail</p>
2716 <p>See also @'TextInteractor::OnCopyCommand_After',
2717 @'TextInteractor::OnPasteCommand_Before',
2718 and
2719 @'TextInteractor::OnPasteCommand_After'
2720 .</p>
2721 */
2722bool TextInteractor::OnCopyCommand_Before ()
2723{
2724 return true;
2725}
2726
2727/*
2728@METHOD: TextInteractor::OnCopyCommand_After
2729@ACCESS: protected
2730@DESCRIPTION: <p>See also @'TextInteractor::OnCopyCommand_Before'.</p>
2731 */
2732void TextInteractor::OnCopyCommand_After ()
2733{
2734}
2735
2736/*
2737@METHOD: TextInteractor::OnCopyCommand_CopyFlavors
2738@ACCESS: protected
2739@DESCRIPTION: <p></p>
2740 */
2741void TextInteractor::OnCopyCommand_CopyFlavors ()
2742{
2743 WriterClipboardFlavorPackage writer;
2744 ExternalizeFlavors (writer);
2745}
2746
2747bool TextInteractor::ShouldEnablePasteCommand () const
2748{
2749 return Led_ClipboardObjectAcquire::FormatAvailable_TEXT ();
2750}
2751
2752/*
2753@METHOD: TextInteractor::OnPasteCommand_Before
2754@DESCRIPTION: <p>Hook overriden by SDK-specific classes or templates which does special things in the before
2755 a clipboard <em>paste</em> operation can begin (like opening a clipboard object). Generally should not be
2756 called directly or overridden, except when implementing new SDK wrappers.</p>
2757 <p>Return false or throw if fail</p>
2758 <p>See also @'TextInteractor::OnPasteCommand_After',
2759 @'TextInteractor::OnCopyCommand_Before',
2760 and
2761 @'TextInteractor::OnCopyCommand_After'
2762 .</p>
2763 */
2764bool TextInteractor::OnPasteCommand_Before ()
2765{
2766 return true;
2767}
2768
2769/*
2770@METHOD: TextInteractor::OnPasteCommand_After
2771@ACCESS: protected
2772@DESCRIPTION: <p>See also @'TextInteractor::OnPasteCommand_Before'.</p>
2773 */
2774void TextInteractor::OnPasteCommand_After ()
2775{
2776}
2777
2778/*
2779@METHOD: TextInteractor::OnPasteCommand_PasteBestFlavor
2780@ACCESS: protected
2781@DESCRIPTION: <p></p>
2782 */
2783void TextInteractor::OnPasteCommand_PasteBestFlavor ()
2784{
2785#if qStroika_Foundation_Common_Platform_Windows && 0
2786 // A little debugging hack for windows - sometimes helpful to turn this on
2787 // to peek in the debugger at what is on the clipboard - LGP 960430
2788
2789 long clipFormat = 0;
2790 while ((clipFormat = ::EnumClipboardFormats (clipFormat)) != 0) {
2791 TCHAR buf[1024];
2792 int nChars = ::GetClipboardFormatName (clipFormat, buf, Memory::NEltsOf (buf));
2793 int breakHere = 0;
2794 }
2795#endif
2796
2797 ReaderClipboardFlavorPackage clipData;
2798
2799 SmartCNPInfo smartCNPInfo;
2800 bool doSmartCNP = PasteLooksLikeSmartCNP (&smartCNPInfo);
2801 size_t savedSelStart = GetSelectionStart (); // save cuz InternalizeBestFlavor () will tend to adjust selStart
2802 InternalizeBestFlavor (clipData);
2803 Assert (savedSelStart <= GetSelectionStart ()); // InternalizeBestFlavor can only adjust it FORWARD - not backward...
2804 if (doSmartCNP) {
2805 OptionallyAddExtraSpaceForSmartCutAndPasteModeAdds (savedSelStart, smartCNPInfo);
2806 }
2807}
2808
2809/*
2810@METHOD: TextInteractor::OnPasteCommand_PasteFlavor_Specific
2811@ACCESS: protected
2812@DESCRIPTION: <p></p>
2813 */
2814void TextInteractor::OnPasteCommand_PasteFlavor_Specific (Led_ClipFormat format)
2815{
2816 ReaderClipboardFlavorPackage clipData;
2817 SmartCNPInfo smartCNPInfo;
2818 bool doSmartCNP = PasteLooksLikeSmartCNP (&smartCNPInfo);
2819 size_t savedSelStart = GetSelectionStart (); // save cuz InternalizeBestFlavor () will tend to adjust selStart
2820 InternalizeFlavor_Specific (clipData, format);
2821 Assert (savedSelStart <= GetSelectionStart ()); // InternalizeBestFlavor can only adjust it FORWARD - not backward...
2822 if (doSmartCNP) {
2823 OptionallyAddExtraSpaceForSmartCutAndPasteModeAdds (savedSelStart, smartCNPInfo);
2824 }
2825}
2826
2827/*
2828@METHOD: TextInteractor::PasteLooksLikeSmartCNP
2829@ACCESS: protected
2830@DESCRIPTION: <p></p>
2831 */
2832bool TextInteractor::PasteLooksLikeSmartCNP (SmartCNPInfo* scnpInfo) const
2833{
2834 RequireNotNull (scnpInfo);
2835 ReaderClipboardFlavorPackage clipData;
2836 bool doSmartCNP = GetSmartCutAndPasteMode () and clipData.GetFlavorAvailable_TEXT ();
2837 if (doSmartCNP) {
2838 /*
2839 * Check if this REALLY looks like a good opportunity todo a smart-cut-and-paste whitespace adjustment.
2840 */
2841 size_t length = clipData.GetFlavorSize (kTEXTClipFormat);
2842 Led_ClipFormat textFormat = kTEXTClipFormat;
2843
2844 Memory::StackBuffer<char> buf{Memory::eUninitialized, length}; // could use bufsize=(len+1)/sizeof (Led_tChar)
2845 length = clipData.ReadFlavorData (textFormat, length, buf.data ());
2846 if (doSmartCNP) {
2847 Led_tChar* buffp = reinterpret_cast<Led_tChar*> (static_cast<char*> (buf));
2848 size_t nTChars = length / sizeof (Led_tChar);
2849 doSmartCNP = LooksLikeSmartPastableText (buffp, nTChars, scnpInfo);
2850 }
2851 }
2852 return doSmartCNP;
2853}
2854
2855/*
2856@METHOD: TextInteractor::OnSelectAllCommand
2857@DESCRIPTION: <p>Command to implement the "Select All" UI. Trivial implementation, but nearly all UI's want it, so why
2858 write it each time?</p>
2859*/
2860void TextInteractor::OnSelectAllCommand ()
2861{
2862 SetSelection (0, GetLength ());
2863}
2864
2865bool TextInteractor::CanAcceptFlavor (Led_ClipFormat clipFormat) const
2866{
2867 return (kTEXTClipFormat == clipFormat or kFILEClipFormat == clipFormat);
2868}
2869
2870void TextInteractor::InternalizeBestFlavor (ReaderFlavorPackage& flavorPackage, bool updateCursorPosition, bool autoScroll, UpdateMode updateMode)
2871{
2872 size_t start = GetSelectionStart ();
2873 size_t end = GetSelectionEnd ();
2874
2875 bool good = false;
2876 {
2877 TempMarker newSel{GetTextStore (), end + 1, end + 1}; // NB: This marker pos MAY split a multibyte character - but that should cause
2878 // no problems.
2879 // We are only keeping this temporarily and we subtract one at the
2880 // end. Just want to make it past point where we do the insertion
2881 // so we get the right adjustment on
2882 good = fInternalizer->InternalizeBestFlavor (flavorPackage, start, end);
2883 if (good and updateCursorPosition) {
2884 SetCaretShownAfterPos (true); // by default show pos after - this is what works best for NL's and is what we
2885 // want most of the time...
2886
2887 // We placed a marker one past the end of the selection. Then we replaced the given selection.
2888 // So the marker now points one past where it really should. Backward adjust it, and set the selection there.
2889 size_t newSelection = newSel.GetStart ();
2890 if (newSelection > start) {
2891 --newSelection;
2892 }
2893
2894 SetSelection (newSelection, newSelection);
2895 }
2896 }
2897
2898 if (good) {
2899 if (autoScroll) {
2900 ScrollToSelection ();
2901 }
2902 if (updateMode == eImmediateUpdate) {
2903 Update ();
2904 }
2905 }
2906 else {
2907 OnBadUserInput ();
2908 }
2909}
2910
2911/*
2912@METHOD: TextInteractor::InternalizeFlavor_Specific
2913@DESCRIPTION: <p></p>
2914 */
2915void TextInteractor::InternalizeFlavor_Specific (ReaderFlavorPackage& flavorPackage, Led_ClipFormat format, bool updateCursorPosition,
2916 bool autoScroll, UpdateMode updateMode)
2917{
2918 size_t start = GetSelectionStart ();
2919 size_t end = GetSelectionEnd ();
2920
2921 bool good = false;
2922 {
2923 TempMarker newSel{GetTextStore (), end + 1, end + 1}; // NB: This marker pos MAY split a multibyte character - but that should cause
2924 // no problems.
2925 // We are only keeping this temporarily and we subtract one at the
2926 // end. Just want to make it past point where we do the insertion
2927 // so we get the right adjustment on
2928
2929 if (format == kTEXTClipFormat) {
2930 good = fInternalizer->InternalizeFlavor_TEXT (flavorPackage, start, end);
2931 }
2932 else if (format == kFILEClipFormat) {
2933 good = fInternalizer->InternalizeFlavor_FILE (flavorPackage, start, end);
2934 }
2935 else {
2936 good = fInternalizer->InternalizeBestFlavor (flavorPackage, start, end);
2937 }
2938
2939 if (good and updateCursorPosition) {
2940 SetCaretShownAfterPos (true); // by default show pos after - this is what works best for NL's and is what we
2941 // want most of the time...
2942
2943 // We placed a marker one past the end of the selection. Then we replaced the given selection.
2944 // So the marker now points one past where it really should. Backward adjust it, and set the selection there.
2945 size_t newSelection = newSel.GetStart ();
2946 if (newSelection > start) {
2947 --newSelection;
2948 }
2949
2950 SetSelection (newSelection, newSelection);
2951 }
2952 }
2953
2954 if (good) {
2955 if (autoScroll) {
2956 ScrollToSelection ();
2957 }
2958 if (updateMode == eImmediateUpdate) {
2959 Update ();
2960 }
2961 }
2962 else {
2963 OnBadUserInput ();
2964 }
2965}
2966
2967/*
2968@METHOD: TextInteractor::MakeDefaultInternalizer
2969@DESCRIPTION: <p>Make a @'FlavorPackageInternalizer' which is appropriate for this text interactor. Override this
2970 to make a different subclass, which supports different style and file formats from the (simple) default.</p>
2971 <p>By default, this creates a @'FlavorPackageInternalizer'.</p>
2972*/
2973shared_ptr<FlavorPackageInternalizer> TextInteractor::MakeDefaultInternalizer ()
2974{
2975 return make_shared<FlavorPackageInternalizer> (GetTextStore ());
2976}
2977
2978/*
2979@METHOD: TextInteractor::HookInternalizerChanged
2980@DESCRIPTION: <p>Called by @'TextInteractor::SetInternalizer' whenever there is a new internalizer specified.</p>
2981*/
2982void TextInteractor::HookInternalizerChanged ()
2983{
2984}
2985
2986/*
2987@METHOD: TextInteractor::ExternalizeFlavors
2988@DESCRIPTION: <p>Use the associated externalizer (see @'TextInteractor::SetExternalizer') to call
2989 ExternalizeFlavors applied to the current selection.</p>
2990*/
2991void TextInteractor::ExternalizeFlavors (WriterFlavorPackage& flavorPackage)
2992{
2993 fExternalizer->ExternalizeFlavors (flavorPackage, GetSelectionStart (), GetSelectionEnd ());
2994}
2995
2996/*
2997@METHOD: TextInteractor::ExternalizeBestFlavor
2998@DESCRIPTION: <p>Use the associated externalizer (see @'TextInteractor::SetExternalizer') to call
2999 ExternalizeBestFlavor applied to the current selection.</p>
3000*/
3001void TextInteractor::ExternalizeBestFlavor (WriterFlavorPackage& flavorPackage)
3002{
3003 fExternalizer->ExternalizeBestFlavor (flavorPackage, GetSelectionStart (), GetSelectionEnd ());
3004}
3005
3006/*
3007@METHOD: TextInteractor::MakeDefaultExternalizer
3008@DESCRIPTION: <p>Make a @'FlavorPackageExternalize' which is appropriate for this text interactor. Override this
3009 to make a different subclass, which supports different style and file formats from the (simple) default.</p>
3010 <p>By default, this creates a @'FlavorPackageExternalizer'.</p>
3011*/
3012shared_ptr<FlavorPackageExternalizer> TextInteractor::MakeDefaultExternalizer ()
3013{
3014 return make_shared<FlavorPackageExternalizer> (GetTextStore ());
3015}
3016
3017/*
3018@METHOD: TextInteractor::HookExternalizerChanged
3019@DESCRIPTION: <p>Called by @'TextInteractor::SetExternalizer' whenever there is a new externalizer specified.</p>
3020*/
3021void TextInteractor::HookExternalizerChanged ()
3022{
3023}
3024
3025/*
3026@METHOD: TextInteractor::OnBadUserInput
3027@DESCRIPTION: <p>By default this throws @'TextInteractor::BadUserInput' but it can be overriden to
3028 NOT throw anything, and just beep or something. BEWARE then when calling this that it may or
3029 may not throw, and may or may not return.</p>
3030*/
3031void TextInteractor::OnBadUserInput ()
3032{
3033 // you may want to OVERRIDE this to do a staged alert, or to throw an exception???
3034 //Led_BeepNotify ();
3035 throw BadUserInput{}; // default catcher should call Led_BeepNotify ()
3036}
3037
3038/*
3039@METHOD: TextInteractor::SetScrollBarType
3040@DESCRIPTION: <p>Specify whether or not the interactor will display / manage scrollbars. This really is handled in OS/ClassLib specific subclasses.
3041 But the API is here to keep it uniform across the platforms.</p>
3042 <p>The default settings for each direction (v/h) are 'TextInteractor::eScrollBarNever'
3043 (except than for the Windows platform, @'Led_Win32_Helper<BASE_INTERACTOR>::OnCreate_Msg'
3044 and @'Led_MFC_Helper<MFC_BASE_CLASS,BASE_INTERACTOR>::OnCreate' will set the initial value according to
3045 the windows style passed into the WM_CREATE message).</p>
3046 <p>On windows I recall that it USED to be standard that an edit text would suddenly
3047 spawn a scrollbar when needed. This doesn't appear to be the case any longer.
3048 I always hated this behavior, but since I didn't know better, I implemented it.</p>
3049 <p>Even though this behavior is no longer particularly standard or common, it is sometimes
3050 desired, and so supported.</p>
3051 <p>NB: You need not turn on the WS_V/HSCROLL styles to make the scrollbars appear/disapear.
3052 When this is on, it is handled automagically.</p>
3053 <p>NB: This USED to be controlled by a compile-time variable @'qMakeWindowsScrollbarAppearOnlyIfNeeded',
3054 which is now obsolete.</p>
3055 <p>See also 'TextInteractor::GetScrollBarType' and @'TextInteractor::InvalidateScrollBarParameters'.</p>
3056*/
3057void TextInteractor::SetScrollBarType (VHSelect vh, ScrollBarType scrollBarType)
3058{
3059 if (GetScrollBarType (vh) != scrollBarType) {
3060 InvalidateScrollBarParameters ();
3061 SetScrollBarType_ (vh, scrollBarType);
3062 }
3063}
3064
3065/*
3066@METHOD: TextInteractor::InvalidateScrollBarParameters
3067@DESCRIPTION: <p>Mark the contents of the scrollbars as invalid. Someone, sometime later soon will call
3068 @'TextInteractor::UpdateScrollBars' to fix them up again.</p>
3069*/
3070void TextInteractor::InvalidateScrollBarParameters ()
3071{
3072 InvalidateScrollBarParameters_ ();
3073}
3074
3075/*
3076@METHOD: TextInteractor::UpdateScrollBars
3077@DESCRIPTION: <p>Override this to handle any update of the scrollbars you may need to. Something has happened to
3078 invalidate what they now display (new text added, scrolled, or whatever - someone called @'TextInteractor::InvalidateScrollBarParameters').
3079 This is usually taken care of in any class library wrapper code, such as @'Led_MFC_Helper<MFC_BASE_CLASS,BASE_INTERACTOR>'
3080 or @'Led_Win32_Helper<BASE_INTERACTOR>'.</p>
3081*/
3082void TextInteractor::UpdateScrollBars ()
3083{
3084 UpdateScrollBars_ ();
3085}
3086
3087/*
3088@METHOD: TextInteractor::SetCaretShown
3089@DESCRIPTION: <p>See also @'TextInteractor::GetCaretShownSituation' and @'TextInteractor::GetCaretShown'.</p>
3090*/
3091void TextInteractor::SetCaretShown (bool shown)
3092{
3093 if (GetCaretShown () != shown) {
3094 fCaretShown = shown;
3095 InvalidateCaretState ();
3096#if qStroika_Foundation_Common_Platform_MacOS
3097 // On the mac - when it was shown, and now is not - we MAY need to force an update to get it erased - and when not shown- the
3098 // InvalidateCaretState () method doesn't force an update.
3099 if (not shown) {
3100 RefreshWindowRect (CalculateCaretRect ());
3101 }
3102#endif
3103 }
3104}
3105
3106/*
3107@METHOD: TextInteractor::GetCaretShownSituation
3108@DESCRIPTION: <p>To decide if its appropriate for the editor to display a blinking caret, we check an
3109 overall state variable @'TextInteractor::GetCaretShown' which is logically tied to
3110 whether or not the edit widget has the focus. But - thats not all that needs to be
3111 considered to decide if you should display the caret. You also - typically - need
3112 to have an empty selection. This virtual method does that particular check.</p>
3113*/
3114bool TextInteractor::GetCaretShownSituation () const
3115{
3116 size_t selStart = 0;
3117 size_t selEnd = 0;
3118 GetSelection (&selStart, &selEnd);
3119 return selStart == selEnd;
3120}
3121
3122/*
3123@METHOD: TextInteractor::SetCaretShownAfterPos
3124@DESCRIPTION: <p>See @'TextInteractor::GetCaretShownAfterPos' for meaning of this flag.</p>
3125 */
3126void TextInteractor::SetCaretShownAfterPos (bool shownAfterPos)
3127{
3128 if (GetCaretShownAfterPos () != shownAfterPos) {
3129 InvalidateCaretState (); // before and after so we draw/erase in new and old places
3130 fCaretShownAfterPos = shownAfterPos;
3131 InvalidateCaretState ();
3132 }
3133}
3134
3135/*
3136@METHOD: TextInteractor::CalculateCaretRect
3137@ACCESS: protected
3138@DESCRIPTION: <p>This is complicated due to worrying about bidirectional editing, and due to the fact
3139 that two adjacent characters in logical order, maybe in very different parts of the screen
3140 (e.g. the marker position between the end of a row and the start of the next row maybe identical, or
3141 the marker position between one character and another WITHIN a row at a directional boundary may
3142 indicate two very different caret locations).</p>
3143 <p>The first issue to resolve is - <em>which character</em> to we care most about - the one <em>preceeding</em>
3144 the marker position or the one <em>following</em>. This question is arbitrated by the <em>@'TextInteractor::GetCaretShownAfterPos'</em>
3145 API.</p>
3146 <p>The second question - as to which side of the character to display the caret - is decided
3147 by the text direction. But WHICH text direction? The one of the preceeding or following character? Easy - we use
3148 the same choice as chosen by the first question.
3149 </p>
3150 */
3151Led_Rect TextInteractor::CalculateCaretRect () const
3152{
3153 size_t selEnd = GetSelectionEnd ();
3154 if (GetSelectionStart () == selEnd) {
3155 bool showAfter = GetCaretShownAfterPos ();
3156 if (selEnd == 0) {
3157 showAfter = true;
3158 }
3159
3160 size_t charAfterPos = showAfter ? selEnd : FindPreviousCharacter (selEnd);
3161 TextDirection textDirection = GetTextDirection (charAfterPos);
3162 Led_Rect caretRect = GetCharWindowLocation (charAfterPos);
3163
3164 if (caretRect.GetBottom () < GetWindowRect ().GetTop () or caretRect.GetTop () > GetWindowRect ().GetBottom ()) {
3165 return Led_Rect{0, 0, 0, 0};
3166 }
3167
3168 Led_Rect origCaretRect = caretRect;
3169 FontMetrics fontMetrics = GetFontMetricsAt (charAfterPos);
3170
3171 // WE NEED THE WHOLE ROW BASELINE!!! THEN CAN COMPUTE caretrect from that!!!
3172 DistanceType baseLineFromTop = GetRowRelativeBaselineOfRowContainingPosition (charAfterPos);
3173 CoordinateType realBaseLine = baseLineFromTop + caretRect.top;
3174
3175 // now adjust caretrect to be font-metrics from the base line
3176 caretRect.top = realBaseLine - fontMetrics.GetAscent ();
3177 caretRect.bottom = realBaseLine + fontMetrics.GetDescent ();
3178
3179 // Before pinning CaretRect to full rowRect, first try to adjust it so its inside.
3180 // It can come outside the rowRect if the baseLineFromTop is zero - for example - when
3181 // the preceeding text is hidden.
3182 if (caretRect.top < origCaretRect.top) {
3183 DistanceType diff = origCaretRect.GetTop () - caretRect.GetTop ();
3184 caretRect += Led_Point (diff, 0);
3185 }
3186
3187 // pin CaretRect to full rowRect
3188 caretRect.SetTop (max (caretRect.GetTop (), origCaretRect.GetTop ()));
3189 caretRect.bottom = min (caretRect.GetBottom (), origCaretRect.GetBottom ());
3190
3191 if (textDirection == eLeftToRight) {
3192 if (not showAfter) {
3193 caretRect.left = caretRect.right;
3194 }
3195 }
3196 else {
3197 caretRect.left = caretRect.right;
3198 }
3199
3200 const CoordinateType kCaretWidth = 1;
3201 // quickie hack to be sure caret doesn't go off right side of window!!!
3202 const CoordinateType kSluff = kCaretWidth + 1;
3203 if (caretRect.GetLeft () + kSluff > GetWindowRect ().GetRight ()) {
3204 caretRect.SetLeft (GetWindowRect ().GetRight () - kSluff);
3205 }
3206 caretRect.SetRight (caretRect.GetLeft () + kCaretWidth);
3207
3208 Ensure (not caretRect.IsEmpty ());
3209 return caretRect;
3210 }
3211 else {
3212 return (Led_Rect{0, 0, 0, 0});
3213 }
3214}
3215
3216void TextInteractor::InvalidateCaretState ()
3217{
3218 if (IsWholeWindowInvalid ()) {
3219 return;
3220 }
3221 if (GetCaretShown () and (GetSelectionStart () == GetSelectionEnd ())) {
3222 RefreshWindowRect (CalculateCaretRect ());
3223 }
3224}
3225
3226/*
3227@METHOD: TextInteractor::OnTypedNormalCharacter
3228@DESCRIPTION: <p>High level handling that does basically all of the portable support for a typed character.
3229 This is typically what you would call (the class library integration classes call this directly) to handle
3230 keyboard input. You might also plausibly OVERRIDE this method to provide special handling for
3231 particular key sequences (e.g. LedLineIt! overrides this to map shift-tab when there is a selection
3232 to an indent command rather than an insertion of text).</p>
3233 */
3234void TextInteractor::OnTypedNormalCharacter (Led_tChar theChar, bool /*optionPressed*/, bool /*shiftPressed*/, bool /*commandPressed*/,
3235 bool controlPressed, bool /*altKeyPressed*/)
3236{
3237 IdleManager::NonIdleContext nonIdleContext;
3238
3239 Assert (GetSelectionEnd () <= GetLength () + 1);
3240
3241 if (GetSuppressTypedControlCharacters ()) {
3242 bool controlChar = Character (theChar).IsControl ();
3243 if (controlChar && (theChar == '\r' || theChar == '\n' || theChar == ' ' || theChar == '\t' || theChar == '\b')) {
3244 controlChar = false;
3245 }
3246 if (controlChar) {
3247 OnBadUserInput ();
3248 return;
3249 }
3250 }
3251
3252 switch (theChar) {
3253 case '\b': {
3254 CursorMovementDirection dir = eCursorBack;
3255 CursorMovementUnit unit = controlPressed ? eCursorByWord : eCursorByChar;
3256 CursorMovementAction action = eCursorDestroying;
3257 DoSingleCharCursorEdit (dir, unit, action, eDefaultUpdate);
3258 } break;
3259
3260 default: {
3261 if (theChar == '\n') {
3262 BreakInGroupedCommands ();
3263 }
3264 InteractiveReplace (&theChar, 1, eDefaultUpdate);
3265 } break;
3266 }
3267
3268 ScrollToSelection ();
3269#if qPeekForMoreCharsOnUserTyping
3270 UpdateIfNoKeysPending ();
3271#else
3272 Update ();
3273#endif
3274}
3275
3276#if qStroika_Foundation_Common_Platform_MacOS || qStroika_FeatureSupported_XWindows
3277float TextInteractor::GetTickCountBetweenBlinks ()
3278{
3279#if qStroika_Foundation_Common_Platform_MacOS
3280 return ::GetCaretTime () / 60.0;
3281#elif qStroika_FeatureSupported_XWindows
3282 return 0.4f;
3283#endif
3284}
3285#endif
3286
3287bool TextInteractor::DelaySomeForScrollBarClick ()
3288{
3289 const Time::DurationSeconds kDelayAfterFirstTicks = 0.20s; // maybe should use ::GetDblClickTime()???
3290 const Time::DurationSeconds kDelayAfterOtherTicks = 0.02s; // This delay is so on really fast computers, text doesn't scroll too quickly
3291 const int kTimesForFirstClick = 2;
3292 const Time::DurationSeconds kLongTime = 1.0s; // any click after this time deemed we start again with first-tick
3293 static short sTimesThruBeforeReset;
3294
3295 Foundation::Time::TimePointSeconds now = Time::GetTickCount ();
3296 if (fLastScrolledAt == Time::TimePointSeconds{0s} or fLastScrolledAt + kLongTime < now) {
3297 fLastScrolledAt = now + kDelayAfterFirstTicks;
3298 sTimesThruBeforeReset = 1;
3299 return true; // first time through - handle click immediately
3300 }
3301 else if (fLastScrolledAt < now) {
3302 ++sTimesThruBeforeReset;
3303 fLastScrolledAt = now + (sTimesThruBeforeReset <= kTimesForFirstClick ? kDelayAfterFirstTicks : kDelayAfterOtherTicks);
3304 return true; // enuf time has elapsed
3305 }
3306 else {
3307 return false; // not enough time has elapsed
3308 }
3309}
3310#endif
#define AssertNotNull(p)
Definition Assertions.h:333
#define RequireNotNull(p)
Definition Assertions.h:347
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
#define DbgTrace
Definition Trace.h:309
constexpr bool IsControl() const noexcept
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
nonvirtual pointer data() noexcept
returns a (possibly const) pointer to the start of the live buffer data. This return value can be inv...
basic_string< SDKChar > SDKString
Definition SDKString.h:38