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