1 /*!********************************************************************
2 *
3 Audacity: A Digital Audio Editor
4
5 TextEditHelper.h
6
7 Vitaly Sverchinsky
8
9 The major part of the logic is extracted from LabelTrackView.cpp and
10 LabelTextHandle.cpp
11
12 **********************************************************************/
13
14 #include "TextEditHelper.h"
15
16 #include <wx/app.h>
17 #include <wx/dc.h>
18 #include <wx/dcmemory.h>
19 #include <wx/clipbrd.h>
20
21 #include "../../ProjectWindow.h"
22 #include "../../RefreshCode.h"
23
24 TextEditDelegate::~TextEditDelegate() = default;
25
IsGoodEditKeyCode(int keyCode)26 bool TextEditHelper::IsGoodEditKeyCode(int keyCode)
27 {
28 // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys
29 // within that range that are usually printable, plus the ones we use for
30 // keyboard navigation.
31 return keyCode < WXK_START ||
32 (keyCode >= WXK_END && keyCode < WXK_UP) ||
33 (keyCode == WXK_RIGHT) ||
34 (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
35 (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) ||
36 (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) ||
37 (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) ||
38 #if defined(__WXMAC__)
39 (keyCode > WXK_RAW_CONTROL) ||
40 #endif
41 (keyCode > WXK_WINDOWS_MENU);
42 }
43
TextEditHelper(const std::weak_ptr<TextEditDelegate> & delegate,const wxString & text,const wxFont & font)44 TextEditHelper::TextEditHelper(const std::weak_ptr<TextEditDelegate>& delegate, const wxString& text, const wxFont& font)
45 : mFont(font),
46 mDelegate(delegate),
47 mText(text),
48 mInitialCursorPos(0)
49 {
50 mCurrentCursorPos = text.Length();
51 }
52
SetTextColor(const wxColor & textColor)53 void TextEditHelper::SetTextColor(const wxColor& textColor)
54 {
55 mTextColor = textColor;
56 }
57
SetTextSelectionColor(const wxColor & textSelectionColor)58 void TextEditHelper::SetTextSelectionColor(const wxColor& textSelectionColor)
59 {
60 mTextSelectionColor = textSelectionColor;
61 }
62
Cancel(AudacityProject * project)63 void TextEditHelper::Cancel(AudacityProject* project)
64 {
65 if (auto lock = mDelegate.lock())
66 lock->OnTextEditCancelled(project);
67 }
68
Finish(AudacityProject * project)69 void TextEditHelper::Finish(AudacityProject* project)
70 {
71 if (auto lock = mDelegate.lock())
72 lock->OnTextEditFinished(project, mText);
73 }
74
GetSelection() const75 std::pair<int, int> TextEditHelper::GetSelection() const
76 {
77 return std::make_pair(mInitialCursorPos, mCurrentCursorPos);
78 }
79
SetSelection(int from,int to)80 void TextEditHelper::SetSelection(int from, int to)
81 {
82 mInitialCursorPos = from;
83 mCurrentCursorPos = to;
84 }
85
SelectAll()86 void TextEditHelper::SelectAll()
87 {
88 mInitialCursorPos = 0;
89 mCurrentCursorPos = mText.Length();
90 }
91
IsSelectionEmpty()92 bool TextEditHelper::IsSelectionEmpty()
93 {
94 return mCurrentCursorPos == mInitialCursorPos;
95 }
96
CaptureKey(int,int mods)97 bool TextEditHelper::CaptureKey(int, int mods)
98 {
99 return mods == wxMOD_NONE || mods == wxMOD_SHIFT;
100 }
101
OnKeyDown(int keyCode,int mods,AudacityProject * project)102 bool TextEditHelper::OnKeyDown(int keyCode, int mods, AudacityProject* project)
103 {
104 auto delegate = mDelegate.lock();
105 if (!delegate)
106 return false;
107
108 if (!CaptureKey(keyCode, mods))
109 return false;
110
111 wxUniChar wchar;
112 bool more = true;
113
114 switch (keyCode) {
115
116 case WXK_BACK:
117 {
118 //IF the label is not blank THEN get rid of a letter or letters according to cursor position
119 if (!mText.empty())
120 {
121 // IF there are some highlighted letters, THEN DELETE them
122 if (mInitialCursorPos != mCurrentCursorPos)
123 RemoveSelectedText(project);
124 else
125 {
126 // DELETE one codepoint leftwards
127 while ((mCurrentCursorPos > 0) && more) {
128 wchar = mText.at(mCurrentCursorPos - 1);
129 mText.erase(mCurrentCursorPos - 1, 1);
130 mCurrentCursorPos--;
131 if (((int)wchar > 0xDFFF) || ((int)wchar < 0xDC00))
132 {
133 delegate->OnTextModified(project, mText);
134 more = false;
135 }
136 }
137 }
138 mInitialCursorPos = mCurrentCursorPos;
139 mOffset = std::clamp(mOffset, 0, std::max(0, static_cast<int>(mText.Length()) - 1));
140 return true;
141 }
142 }
143 break;
144
145 case WXK_DELETE:
146 case WXK_NUMPAD_DELETE:
147 {
148 int len = mText.length();
149 //If the label is not blank get rid of a letter according to cursor position
150 if (len > 0)
151 {
152 // if there are some highlighted letters, DELETE them
153 if (mInitialCursorPos != mCurrentCursorPos)
154 RemoveSelectedText(project);
155 else
156 {
157 // DELETE one codepoint rightwards
158 while ((mCurrentCursorPos < len) && more) {
159 wchar = mText.at(mCurrentCursorPos);
160 mText.erase(mCurrentCursorPos, 1);
161 if (((int)wchar > 0xDBFF) || ((int)wchar < 0xD800))
162 {
163 delegate->OnTextModified(project, mText);
164 more = false;
165 }
166 }
167 }
168 mInitialCursorPos = mCurrentCursorPos;
169 mOffset = std::clamp(mOffset, 0, std::max(0, static_cast<int>(mText.Length()) - 1));
170 return true;
171 }
172 }
173 break;
174
175 case WXK_HOME:
176 case WXK_NUMPAD_HOME:
177 // Move cursor to beginning of label
178 mCurrentCursorPos = 0;
179 if (mods == wxMOD_SHIFT)
180 ;
181 else
182 mInitialCursorPos = mCurrentCursorPos;
183 return true;
184 case WXK_END:
185 case WXK_NUMPAD_END:
186 // Move cursor to end of label
187 mCurrentCursorPos = (int)mText.length();
188 if (mods == wxMOD_SHIFT)
189 ;
190 else
191 mInitialCursorPos = mCurrentCursorPos;
192 return true;
193
194 case WXK_LEFT:
195 case WXK_NUMPAD_LEFT:
196 // Moving cursor left
197 if (mods != wxMOD_SHIFT && mCurrentCursorPos != mInitialCursorPos)
198 //put cursor to the left edge of selection
199 mInitialCursorPos = mCurrentCursorPos =
200 std::min(mInitialCursorPos, mCurrentCursorPos);
201 else
202 {
203 while ((mCurrentCursorPos > 0) && more) {
204 wchar = mText.at(mCurrentCursorPos - 1);
205 more = !(((int)wchar > 0xDFFF) || ((int)wchar < 0xDC00));
206
207 --mCurrentCursorPos;
208 }
209 if (mods != wxMOD_SHIFT)
210 mInitialCursorPos = mCurrentCursorPos;
211 }
212 return true;
213
214 case WXK_RIGHT:
215 case WXK_NUMPAD_RIGHT:
216 // Moving cursor right
217 if (mods != wxMOD_SHIFT && mCurrentCursorPos != mInitialCursorPos)
218 //put cursor to the right edge of selection
219 mInitialCursorPos = mCurrentCursorPos =
220 std::max(mInitialCursorPos, mCurrentCursorPos);
221 else
222 {
223 while ((mCurrentCursorPos < (int)mText.length()) && more) {
224 wchar = mText.at(mCurrentCursorPos);
225 more = !(((int)wchar > 0xDBFF) || ((int)wchar < 0xD800));
226
227 ++mCurrentCursorPos;
228 }
229 if (mods != wxMOD_SHIFT)
230 mInitialCursorPos = mCurrentCursorPos;
231 }
232
233 return true;
234
235 case WXK_ESCAPE:
236 delegate->OnTextEditCancelled(project);
237 return true;
238 case WXK_RETURN:
239 case WXK_NUMPAD_ENTER:
240 case WXK_TAB:
241 delegate->OnTextEditFinished(project, mText);
242 return true;
243 }
244 return false;
245 }
246
OnChar(int charCode,AudacityProject * project)247 bool TextEditHelper::OnChar(int charCode, AudacityProject* project)
248 {
249 auto delegate = mDelegate.lock();
250 if (!delegate)
251 return false;
252
253 if (charCode == 0 || wxIscntrl(charCode)) {
254 return false;
255 }
256
257 // Test if cursor is in the end of string or not
258 if (mInitialCursorPos != mCurrentCursorPos)
259 RemoveSelectedText(project);
260
261 if (mCurrentCursorPos < (int)mText.length()) {
262 // Get substring on the righthand side of cursor
263 wxString rightPart = mText.Mid(mCurrentCursorPos);
264 // Set title to substring on the lefthand side of cursor
265 mText = mText.Left(mCurrentCursorPos);
266 //append charcode
267 mText += charCode;
268 //append the right part substring
269 mText += rightPart;
270 }
271 else
272 //append charCode
273 mText += charCode;
274
275 delegate->OnTextModified(project, mText);
276
277 //moving cursor position forward
278 mInitialCursorPos = ++mCurrentCursorPos;
279
280 return true;
281 }
282
OnClick(const wxMouseEvent & event,AudacityProject *)283 bool TextEditHelper::OnClick(const wxMouseEvent& event, AudacityProject*)
284 {
285 if (event.ButtonDown())
286 {
287 bool result = false;
288 if (mBBox.Contains(event.GetPosition()))
289 {
290 if (event.LeftDown())
291 {
292 mRightDragging = false;
293 auto position = FindCursorIndex(event.GetPosition());
294 auto initial = mInitialCursorPos;
295 if (event.ShiftDown()) {
296 #ifdef __WXMAC__
297 // Set the drag anchor at the end of the previous selection
298 // that is farther from the NEW drag end
299 const auto current = mCurrentCursorPos;
300 if (abs(position - current) > abs(position - initial))
301 initial = current;
302 #else
303 // initial position remains as before
304 #endif
305 }
306 else
307 initial = position;
308
309 mInitialCursorPos = initial;
310 mCurrentCursorPos = position;
311 }
312 else
313 {
314 if (mInitialCursorPos == mCurrentCursorPos)
315 {
316 auto position = FindCursorIndex(event.GetPosition());
317 mInitialCursorPos = mCurrentCursorPos = position;
318 }
319 // Actually this might be right or middle down
320 mRightDragging = true;
321 }
322 result = true;
323 }
324 #if defined(__WXGTK__) && (HAVE_GTK)
325 if (evt.MiddleDown()) {
326 // Paste text, making a NEW label if none is selected.
327 wxTheClipboard->UsePrimarySelection(true);
328 view.PasteSelectedText(project, newSel.t0(), newSel.t1());
329 wxTheClipboard->UsePrimarySelection(false);
330 result = true;
331 }
332 #endif
333 return result;
334 }
335 return false;
336 }
337
OnDrag(const wxMouseEvent & event,AudacityProject * project)338 bool TextEditHelper::OnDrag(const wxMouseEvent& event, AudacityProject* project)
339 {
340 return HandleDragRelease(event, project);
341 }
342
OnRelease(const wxMouseEvent & event,AudacityProject * project)343 bool TextEditHelper::OnRelease(const wxMouseEvent& event, AudacityProject* project)
344 {
345 return HandleDragRelease(event, project);
346 }
347
Draw(wxDC & dc,const wxRect & rect)348 void TextEditHelper::Draw(wxDC& dc, const wxRect& rect)
349 {
350 mBBox = rect;
351 dc.SetFont(mFont);
352
353 const auto cursorHeight = dc.GetFontMetrics().height;
354
355 wxDCClipper clipper(dc, rect);
356
357 auto rtl = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft;
358
359 auto curPosX = 0;
360 auto maxOffset = static_cast<int>(mText.Length());
361 mOffset = std::clamp(mOffset, 0, maxOffset);
362 {
363 auto leftBound = rect.GetLeft();
364 auto rightBound = rect.GetRight() + 1;
365 GetCharPositionX(mCurrentCursorPos, &curPosX);
366
367 if ((!rtl && curPosX >= rightBound) || (rtl && curPosX < leftBound))
368 {
369 while (mOffset < maxOffset)
370 {
371 GetCharPositionX(mCurrentCursorPos, &curPosX);
372 if (curPosX < rightBound && curPosX >= leftBound)
373 break;
374 ++mOffset;
375 }
376 }
377 if ((!rtl && curPosX < leftBound) || (rtl && curPosX >= rightBound))
378 {
379 while (mOffset > 0)
380 {
381 GetCharPositionX(mCurrentCursorPos, &curPosX);
382 if (curPosX >= leftBound && curPosX < rightBound)
383 break;
384 --mOffset;
385 }
386 }
387 }
388
389 if (mCurrentCursorPos != mInitialCursorPos)
390 {
391 auto left = 0;
392 auto right = 0;
393 GetCharPositionX(std::min(mCurrentCursorPos, mInitialCursorPos), &left);
394 GetCharPositionX(std::max(mCurrentCursorPos, mInitialCursorPos), &right);
395 dc.SetPen(*wxTRANSPARENT_PEN);
396 dc.SetBrush(mTextSelectionColor);
397 dc.DrawRectangle(wxRect(left, rect.GetTop() + (rect.GetHeight() - cursorHeight) / 2, right - left, cursorHeight));
398 }
399
400 dc.SetTextBackground(wxTransparentColour);
401 dc.SetTextForeground(mTextColor);
402 dc.SetFont(wxFont(wxFontInfo()));
403 dc.DrawLabel(mText.Mid(mOffset), rect, (rtl ? wxALIGN_RIGHT : wxALIGN_LEFT) | wxALIGN_CENTER_VERTICAL);
404
405 if (mCurrentCursorPos == mInitialCursorPos)
406 {
407 dc.SetPen(mTextColor);
408 auto top = rect.GetTop() + (rect.GetHeight() - cursorHeight) / 2;
409 dc.DrawLine(curPosX, top, curPosX, top + cursorHeight);
410 }
411 }
412
HandleDragRelease(const wxMouseEvent & event,AudacityProject * project)413 bool TextEditHelper::HandleDragRelease(const wxMouseEvent& event, AudacityProject* project)
414 {
415 if (event.Dragging())
416 {
417 if (!mRightDragging)
418 {
419 mCurrentCursorPos = FindCursorIndex(event.GetPosition());
420 return true;
421 }
422 }
423 else if (event.RightUp() && mBBox.Contains(event.GetPosition()))
424 {
425 auto delegate = mDelegate.lock();
426 if (delegate)
427 {
428 // popup menu for editing
429 // TODO: handle context menus via CellularPanel?
430 delegate->OnTextContextMenu(project, event.GetPosition());
431 return true;
432 }
433 }
434 return false;
435 }
436
RemoveSelectedText(AudacityProject * project)437 void TextEditHelper::RemoveSelectedText(AudacityProject* project)
438 {
439 auto delegate = mDelegate.lock();
440 if (!delegate)
441 return;
442
443 wxString left, right;
444
445 int init = mInitialCursorPos;
446 int cur = mCurrentCursorPos;
447 if (init > cur)
448 std::swap(init, cur);
449
450 if (init > 0)
451 left = mText.Left(init);
452
453 if (cur < (int)mText.length())
454 right = mText.Mid(cur);
455
456 mText = left + right;
457
458 delegate->OnTextModified(project, mText);
459
460 mInitialCursorPos = mCurrentCursorPos = left.length();
461 }
462
FindCursorIndex(const wxPoint & point)463 int TextEditHelper::FindCursorIndex(const wxPoint& point)
464 {
465 int result = -1;
466 wxMemoryDC dc;
467 if (mFont.Ok())
468 dc.SetFont(mFont);
469
470 // A bool indicator to see if set the cursor position or not
471 bool finished = false;
472 int charIndex = 1;
473 int partWidth;
474 int oneWidth;
475 //double bound;
476 wxString subString;
477
478 auto offsetX = 0;
479 if (mOffset > 0)
480 offsetX = dc.GetTextExtent(mText.Left(mOffset)).GetWidth();
481
482 const auto layout = wxTheApp->GetLayoutDirection();
483
484 const int length = mText.length();
485 while (!finished && (charIndex < length + 1))
486 {
487 int unichar = (int)mText.at(charIndex - 1);
488 if ((0xDC00 <= unichar) && (unichar <= 0xDFFF)) {
489 charIndex++;
490 continue;
491 }
492 subString = mText.Left(charIndex);
493 // Get the width of substring
494 dc.GetTextExtent(subString, &partWidth, NULL);
495
496 // Get the width of the last character
497 dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
498
499 if (layout == wxLayout_RightToLeft)
500 {
501 auto bound = mBBox.GetRight() - partWidth + offsetX + oneWidth / 2;
502 if (point.x >= bound)
503 {
504 result = charIndex - 1;
505 finished = true;
506 }
507 }
508 else
509 {
510 auto bound = mBBox.GetLeft() + partWidth - offsetX - oneWidth / 2;
511 if (point.x <= bound)
512 {
513 result = charIndex - 1;
514 finished = true;
515 }
516 }
517 if (!finished)
518 ++charIndex;
519 else
520 break;
521 }
522 if (!finished)
523 // Cursor should be in the last position
524 result = length;
525
526 return result;
527 }
528
GetCharPositionX(int index,int * outX)529 bool TextEditHelper::GetCharPositionX(int index, int* outX)
530 {
531 if (!mFont.Ok())
532 return false;
533
534 wxMemoryDC dc;
535 dc.SetFont(mFont);
536
537 int offsetX{ 0 };
538 if (mOffset > 0)
539 {
540 offsetX = dc.GetTextExtent(mText.Left(mOffset)).GetWidth();
541 }
542
543 if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft)
544 {
545 if (index <= 0)
546 *outX = mBBox.GetRight() + offsetX;
547 else
548 *outX = mBBox.GetRight() - dc.GetTextExtent(mText.Left(index)).GetWidth() + offsetX;
549 }
550 else
551 {
552 if (index <= 0)
553 *outX = mBBox.GetLeft() - offsetX;
554 else
555 *outX = mBBox.GetLeft() + dc.GetTextExtent(mText.Left(index)).GetWidth() - offsetX;
556 }
557
558 return true;
559 }
560
GetBBox() const561 const wxRect& TextEditHelper::GetBBox() const
562 {
563 return mBBox;
564 }
565
566 /// Cut the selected text in the text box
567 /// @return true if text is selected in text box, false otherwise
CutSelectedText(AudacityProject & project)568 bool TextEditHelper::CutSelectedText(AudacityProject& project)
569 {
570 auto delegate = mDelegate.lock();
571 if (!delegate)
572 return false;
573
574 if (mCurrentCursorPos == mInitialCursorPos)
575 return false;
576
577 int init = mInitialCursorPos;
578 int cur = mCurrentCursorPos;
579 if (init > cur)
580 std::swap(init, cur);
581
582 wxString left, right;
583 // data for cutting
584 wxString data = mText.Mid(init, cur - init);
585
586 // get left-remaining text
587 if (init > 0)
588 left = mText.Left(init);
589
590 // get right-remaining text
591 if (cur < (int)mText.length())
592 right = mText.Mid(cur);
593
594 // set title to the combination of the two remainders
595 mText = left + right;
596
597 delegate->OnTextModified(&project, mText);
598 // copy data onto clipboard
599 if (wxTheClipboard->Open()) {
600 // Clipboard owns the data you give it
601 wxTheClipboard->SetData(safenew wxTextDataObject(data));
602 wxTheClipboard->Close();
603 }
604
605 // set cursor positions
606 mInitialCursorPos = mCurrentCursorPos = left.length();
607
608 return true;
609 }
610
611 /// Copy the selected text in the text box
612 /// @return true if text is selected in text box, false otherwise
CopySelectedText(AudacityProject & project)613 bool TextEditHelper::CopySelectedText(AudacityProject& project)
614 {
615 if (mCurrentCursorPos == mInitialCursorPos)
616 return false;
617
618 int init = mInitialCursorPos;
619 int cur = mCurrentCursorPos;
620 if (init > cur)
621 std::swap(init, cur);
622
623 if (init == cur)
624 return false;
625
626 // data for copying
627 wxString data = mText.Mid(init, cur - init);
628
629 // copy the data on clipboard
630 if (wxTheClipboard->Open()) {
631 // Clipboard owns the data you give it
632 wxTheClipboard->SetData(safenew wxTextDataObject(data));
633 wxTheClipboard->Close();
634 }
635
636 return true;
637 }
638
639 // PRL: should this set other fields of the label selection?
640 /// Paste the text on the clipboard to text box
641 /// @return true if mouse is clicked in text box, false otherwise
PasteSelectedText(AudacityProject & project)642 bool TextEditHelper::PasteSelectedText(AudacityProject& project)
643 {
644 auto delegate = mDelegate.lock();
645 if (!delegate)
646 return false;
647
648 wxString text, left, right;
649
650 // if text data is available
651 if (wxTheClipboard->IsSupported(wxDF_UNICODETEXT))
652 {
653 if (wxTheClipboard->Open()) {
654 wxTextDataObject data;
655 wxTheClipboard->GetData(data);
656 wxTheClipboard->Close();
657 text = data.GetText();
658 }
659
660 // Convert control characters to blanks
661 for (int i = 0; i < (int)text.length(); i++) {
662 if (wxIscntrl(text[i])) {
663 text[i] = wxT(' ');
664 }
665 }
666 }
667
668 int cur = mCurrentCursorPos, init = mInitialCursorPos;
669 if (init > cur)
670 std::swap(init, cur);
671
672 left = mText.Left(init);
673 if (cur < (int)mText.length())
674 right = mText.Mid(cur);
675
676 mText = left + text + right;
677
678 delegate->OnTextModified(&project, mText);
679
680 mInitialCursorPos = mCurrentCursorPos = left.length() + text.length();
681
682 return true;
683 }
684