Audacity 3.2.0
TextEditHelper.cpp
Go to the documentation of this file.
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
22#include "../../RefreshCode.h"
23
25
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
44TextEditHelper::TextEditHelper(const std::weak_ptr<TextEditDelegate>& delegate, const wxString& text, const wxFont& font)
45 : mText(text),
46 mFont(font),
47 mInitialCursorPos(0),
48 mDelegate(delegate)
49{
50 mCurrentCursorPos = text.Length();
51}
52
53void TextEditHelper::SetTextColor(const wxColor& textColor)
54{
55 mTextColor = textColor;
56}
57
58void TextEditHelper::SetTextSelectionColor(const wxColor& textSelectionColor)
59{
60 mTextSelectionColor = textSelectionColor;
61}
62
64{
65 if (auto lock = mDelegate.lock())
66 lock->OnTextEditCancelled(project);
67}
68
70{
71 if (auto lock = mDelegate.lock())
72 lock->OnTextEditFinished(project, mText);
73}
74
75std::pair<int, int> TextEditHelper::GetSelection() const
76{
77 return std::make_pair(mInitialCursorPos, mCurrentCursorPos);
78}
79
80void TextEditHelper::SetSelection(int from, int to)
81{
82 mInitialCursorPos = from;
84}
85
87{
89 mCurrentCursorPos = mText.Length();
90}
91
93{
95}
96
97bool TextEditHelper::CaptureKey(int, int mods)
98{
99 return mods == wxMOD_NONE || mods == wxMOD_SHIFT;
100}
101
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
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);
131 if (((int)wchar > 0xDFFF) || ((int)wchar < 0xDC00))
132 {
133 delegate->OnTextModified(project, mText);
134 more = false;
135 }
136 }
137 }
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
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 }
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
179 if (mods == wxMOD_SHIFT)
180 ;
181 else
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
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
201 else
202 {
203 while ((mCurrentCursorPos > 0) && more) {
204 wchar = mText.at(mCurrentCursorPos - 1);
205 more = !(((int)wchar > 0xDFFF) || ((int)wchar < 0xDC00));
206
208 }
209 if (mods != wxMOD_SHIFT)
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
221 else
222 {
223 while ((mCurrentCursorPos < (int)mText.length()) && more) {
224 wchar = mText.at(mCurrentCursorPos);
225 more = !(((int)wchar > 0xDBFF) || ((int)wchar < 0xD800));
226
228 }
229 if (mods != wxMOD_SHIFT)
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
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
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
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
279
280 return true;
281}
282
283bool 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 {
315 {
316 auto position = FindCursorIndex(event.GetPosition());
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
338bool TextEditHelper::OnDrag(const wxMouseEvent& event, AudacityProject* project)
339{
340 return HandleDragRelease(event, project);
341}
342
343bool TextEditHelper::OnRelease(const wxMouseEvent& event, AudacityProject* project)
344{
345 return HandleDragRelease(event, project);
346}
347
348bool TextEditHelper::Draw(wxDC& dc, const wxRect& rect)
349{
350 mBBox = rect;
351
352 if(rect.IsEmpty())
353 return false;
354
355 const auto cursorHeight = dc.GetFontMetrics().height;
356
357 dc.SetFont(mFont);
358
359 wxDCClipper clipper(dc, rect);
360
361 auto curPosX = 0;
362 auto maxOffset = static_cast<int>(mText.Length());
363 mOffset = 0;
364 if(maxOffset > 0)
365 {
366 const auto rtl = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft;
367 {
368 auto leftBound = rect.GetLeft();
369 auto rightBound = rect.GetRight() + 1;
371
372 if ((!rtl && curPosX >= rightBound) || (rtl && curPosX < leftBound))
373 {
374 while (mOffset < maxOffset)
375 {
377 if (curPosX < rightBound && curPosX >= leftBound)
378 break;
379 ++mOffset;
380 }
381 }
382 if ((!rtl && curPosX < leftBound) || (rtl && curPosX >= rightBound))
383 {
384 while (mOffset > 0)
385 {
387 if (curPosX >= leftBound && curPosX < rightBound)
388 break;
389 --mOffset;
390 }
391 }
392 }
393 // Text doesn't fit into rectangle
394 if(mOffset >= maxOffset)
395 return false;
396
398 {
399 auto left = 0;
400 auto right = 0;
403 dc.SetPen(*wxTRANSPARENT_PEN);
404 dc.SetBrush(mTextSelectionColor);
405 dc.DrawRectangle(wxRect(left, rect.GetTop() + (rect.GetHeight() - cursorHeight) / 2, right - left, cursorHeight));
406 }
407
408
409 dc.SetTextBackground(wxTransparentColour);
410 dc.SetTextForeground(mTextColor);
411 dc.SetFont(wxFont(wxFontInfo()));
412 dc.DrawLabel(mText.Mid(mOffset), rect, (rtl ? wxALIGN_RIGHT : wxALIGN_LEFT) | wxALIGN_CENTER_VERTICAL);
413 }
414 else
415 {
418 }
419
421 {
422 dc.SetPen(mTextColor);
423 auto top = rect.GetTop() + (rect.GetHeight() - cursorHeight) / 2;
424 dc.DrawLine(curPosX, top, curPosX, top + cursorHeight);
425 }
426 return true;
427}
428
430{
431 if (event.Dragging())
432 {
433 if (!mRightDragging)
434 {
435 mCurrentCursorPos = FindCursorIndex(event.GetPosition());
436 return true;
437 }
438 }
439 else if (event.RightUp() && mBBox.Contains(event.GetPosition()))
440 {
441 auto delegate = mDelegate.lock();
442 if (delegate)
443 {
444 // popup menu for editing
445 // TODO: handle context menus via CellularPanel?
446 delegate->OnTextContextMenu(project, event.GetPosition());
447 return true;
448 }
449 }
450 return false;
451}
452
454{
455 auto delegate = mDelegate.lock();
456 if (!delegate)
457 return;
458
459 wxString left, right;
460
461 int init = mInitialCursorPos;
462 int cur = mCurrentCursorPos;
463 if (init > cur)
464 std::swap(init, cur);
465
466 if (init > 0)
467 left = mText.Left(init);
468
469 if (cur < (int)mText.length())
470 right = mText.Mid(cur);
471
472 mText = left + right;
473
474 delegate->OnTextModified(project, mText);
475
476 mInitialCursorPos = mCurrentCursorPos = left.length();
477}
478
479int TextEditHelper::FindCursorIndex(const wxPoint& point)
480{
481 int result = -1;
482 wxMemoryDC dc;
483 if (mFont.Ok())
484 dc.SetFont(mFont);
485
486 // A bool indicator to see if set the cursor position or not
487 bool finished = false;
488 int charIndex = 1;
489 int partWidth;
490 int oneWidth;
491 //double bound;
492 wxString subString;
493
494 auto offsetX = 0;
495 if (mOffset > 0)
496 offsetX = dc.GetTextExtent(mText.Left(mOffset)).GetWidth();
497
498 const auto layout = wxTheApp->GetLayoutDirection();
499
500 const int length = mText.length();
501 while (!finished && (charIndex < length + 1))
502 {
503 int unichar = (int)mText.at(charIndex - 1);
504 if ((0xDC00 <= unichar) && (unichar <= 0xDFFF)) {
505 charIndex++;
506 continue;
507 }
508 subString = mText.Left(charIndex);
509 // Get the width of substring
510 dc.GetTextExtent(subString, &partWidth, NULL);
511
512 // Get the width of the last character
513 dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
514
515 if (layout == wxLayout_RightToLeft)
516 {
517 auto bound = mBBox.GetRight() - partWidth + offsetX + oneWidth / 2;
518 if (point.x >= bound)
519 {
520 result = charIndex - 1;
521 finished = true;
522 }
523 }
524 else
525 {
526 auto bound = mBBox.GetLeft() + partWidth - offsetX - oneWidth / 2;
527 if (point.x <= bound)
528 {
529 result = charIndex - 1;
530 finished = true;
531 }
532 }
533 if (!finished)
534 ++charIndex;
535 else
536 break;
537 }
538 if (!finished)
539 // Cursor should be in the last position
540 result = length;
541
542 return result;
543}
544
545bool TextEditHelper::GetCharPositionX(int index, int* outX)
546{
547 if (!mFont.Ok())
548 return false;
549
550 wxMemoryDC dc;
551 dc.SetFont(mFont);
552
553 int offsetX{ 0 };
554 if (mOffset > 0)
555 {
556 offsetX = dc.GetTextExtent(mText.Left(mOffset)).GetWidth();
557 }
558
559 if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft)
560 {
561 if (index <= 0)
562 *outX = mBBox.GetRight() + offsetX;
563 else
564 *outX = mBBox.GetRight() - dc.GetTextExtent(mText.Left(index)).GetWidth() + offsetX;
565 }
566 else
567 {
568 if (index <= 0)
569 *outX = mBBox.GetLeft() - offsetX;
570 else
571 *outX = mBBox.GetLeft() + dc.GetTextExtent(mText.Left(index)).GetWidth() - offsetX;
572 }
573
574 return true;
575}
576
577const wxRect& TextEditHelper::GetBBox() const
578{
579 return mBBox;
580}
581
585{
586 auto delegate = mDelegate.lock();
587 if (!delegate)
588 return false;
589
591 return false;
592
593 int init = mInitialCursorPos;
594 int cur = mCurrentCursorPos;
595 if (init > cur)
596 std::swap(init, cur);
597
598 wxString left, right;
599 // data for cutting
600 wxString data = mText.Mid(init, cur - init);
601
602 // get left-remaining text
603 if (init > 0)
604 left = mText.Left(init);
605
606 // get right-remaining text
607 if (cur < (int)mText.length())
608 right = mText.Mid(cur);
609
610 // set title to the combination of the two remainders
611 mText = left + right;
612
613 delegate->OnTextModified(&project, mText);
614 // copy data onto clipboard
615 if (wxTheClipboard->Open()) {
616 // Clipboard owns the data you give it
617 wxTheClipboard->SetData(safenew wxTextDataObject(data));
618 wxTheClipboard->Close();
619 }
620
621 // set cursor positions
622 mInitialCursorPos = mCurrentCursorPos = left.length();
623
624 return true;
625}
626
630{
632 return false;
633
634 int init = mInitialCursorPos;
635 int cur = mCurrentCursorPos;
636 if (init > cur)
637 std::swap(init, cur);
638
639 if (init == cur)
640 return false;
641
642 // data for copying
643 wxString data = mText.Mid(init, cur - init);
644
645 // copy the data on clipboard
646 if (wxTheClipboard->Open()) {
647 // Clipboard owns the data you give it
648 wxTheClipboard->SetData(safenew wxTextDataObject(data));
649 wxTheClipboard->Close();
650 }
651
652 return true;
653}
654
655// PRL: should this set other fields of the label selection?
659{
660 auto delegate = mDelegate.lock();
661 if (!delegate)
662 return false;
663
664 wxString text, left, right;
665
666 // if text data is available
667 if (wxTheClipboard->IsSupported(wxDF_UNICODETEXT))
668 {
669 if (wxTheClipboard->Open()) {
670 wxTextDataObject data;
671 wxTheClipboard->GetData(data);
672 wxTheClipboard->Close();
673 text = data.GetText();
674 }
675
676 // Convert control characters to blanks
677 for (int i = 0; i < (int)text.length(); i++) {
678 if (wxIscntrl(text[i])) {
679 text[i] = wxT(' ');
680 }
681 }
682 }
683
684 int cur = mCurrentCursorPos, init = mInitialCursorPos;
685 if (init > cur)
686 std::swap(init, cur);
687
688 left = mText.Left(init);
689 if (cur < (int)mText.length())
690 right = mText.Mid(cur);
691
692 mText = left + text + right;
693
694 delegate->OnTextModified(&project, mText);
695
696 mInitialCursorPos = mCurrentCursorPos = left.length() + text.length();
697
698 return true;
699}
wxT("CloseDown"))
int min(int a, int b)
#define safenew
Definition: MemoryX.h:9
const auto project
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
virtual ~TextEditDelegate()
bool HandleDragRelease(const wxMouseEvent &event, AudacityProject *project)
void SetTextSelectionColor(const wxColor &textSelectionColor)
bool OnChar(int charCode, AudacityProject *project)
bool OnDrag(const wxMouseEvent &event, AudacityProject *project)
static bool IsGoodEditKeyCode(int keyCode)
bool OnKeyDown(int keyCode, int mods, AudacityProject *project)
void Finish(AudacityProject *project)
wxColor mTextColor
bool OnClick(const wxMouseEvent &event, AudacityProject *project)
bool CaptureKey(int keyCode, int mods)
int FindCursorIndex(const wxPoint &point)
bool CutSelectedText(AudacityProject &project)
bool GetCharPositionX(int index, int *outX)
bool Draw(wxDC &dc, const wxRect &rect)
void RemoveSelectedText(AudacityProject *project)
const wxRect & GetBBox() const
TextEditHelper(const std::weak_ptr< TextEditDelegate > &delegate, const wxString &text, const wxFont &font)
void SetSelection(int from, int to)
bool PasteSelectedText(AudacityProject &project)
bool OnRelease(const wxMouseEvent &event, AudacityProject *project)
void SetTextColor(const wxColor &textColor)
bool CopySelectedText(AudacityProject &project)
void Cancel(AudacityProject *project)
std::pair< int, int > GetSelection() const
wxColor mTextSelectionColor
std::weak_ptr< TextEditDelegate > mDelegate
void swap(std::unique_ptr< Alg_seq > &a, std::unique_ptr< Alg_seq > &b)
Definition: NoteTrack.cpp:645