iPlug2 - C++ Audio Plug-in Framework
ITextEntryControl.cpp
Go to the documentation of this file.
1 /*
2  ==============================================================================
3 
4  This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5 
6  See LICENSE.txt for more info.
7 
8  ==============================================================================
9  */
10 
17 #include "ITextEntryControl.h"
18 #include "IPlugPlatform.h"
19 #include "wdlutf8.h"
20 #include <string>
21 #include <codecvt>
22 #include <locale>
23 
24 #ifdef _MSC_VER
25 #if (_MSC_VER >= 1900 /* VS 2015*/) && (_MSC_VER < 1920 /* pre VS 2019 */)
26 std::locale::id std::codecvt<char16_t, char, _Mbstatet>::id;
27 #endif
28 #endif
29 
30 //TODO: use either wdlutf8, iplug2 UTF8/UTF16 or cpp11 wstring_convert
31 using StringConvert = std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>;
32 
33 using namespace iplug;
34 using namespace igraphics;
35 
36 #define VIRTUAL_KEY_BIT 0x80000000
37 #define STB_TEXTEDIT_K_SHIFT 0x40000000
38 #define STB_TEXTEDIT_K_CONTROL 0x20000000
39 #define STB_TEXTEDIT_K_ALT 0x10000000
40 // key-bindings
41 #define STB_TEXTEDIT_K_LEFT (VIRTUAL_KEY_BIT | kVK_LEFT)
42 #define STB_TEXTEDIT_K_RIGHT (VIRTUAL_KEY_BIT | kVK_RIGHT)
43 #define STB_TEXTEDIT_K_UP (VIRTUAL_KEY_BIT | kVK_UP)
44 #define STB_TEXTEDIT_K_DOWN (VIRTUAL_KEY_BIT | kVK_DOWN)
45 #define STB_TEXTEDIT_K_LINESTART (VIRTUAL_KEY_BIT | kVK_HOME)
46 #define STB_TEXTEDIT_K_LINEEND (VIRTUAL_KEY_BIT | kVK_END)
47 #define STB_TEXTEDIT_K_WORDLEFT (STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_CONTROL)
48 #define STB_TEXTEDIT_K_WORDRIGHT (STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_CONTROL)
49 #define STB_TEXTEDIT_K_TEXTSTART (STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_CONTROL)
50 #define STB_TEXTEDIT_K_TEXTEND (STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_CONTROL)
51 #define STB_TEXTEDIT_K_DELETE (VIRTUAL_KEY_BIT | kVK_DELETE)
52 #define STB_TEXTEDIT_K_BACKSPACE (VIRTUAL_KEY_BIT | kVK_BACK)
53 #define STB_TEXTEDIT_K_UNDO (STB_TEXTEDIT_K_CONTROL | 'z')
54 #define STB_TEXTEDIT_K_REDO (STB_TEXTEDIT_K_CONTROL | STB_TEXTEDIT_K_SHIFT | 'z')
55 #define STB_TEXTEDIT_K_INSERT (VIRTUAL_KEY_BIT | kVK_INSERT)
56 #define STB_TEXTEDIT_K_PGUP (VIRTUAL_KEY_BIT | kVK_PRIOR)
57 #define STB_TEXTEDIT_K_PGDOWN (VIRTUAL_KEY_BIT | kVK_NEXT)
58 // functions
59 #define STB_TEXTEDIT_STRINGLEN(tc) ITextEntryControl::GetLength (tc)
60 #define STB_TEXTEDIT_LAYOUTROW ITextEntryControl::Layout
61 #define STB_TEXTEDIT_GETWIDTH(tc, n, i) ITextEntryControl::GetCharWidth (tc, n, i)
62 #define STB_TEXTEDIT_KEYTOTEXT(key) \
63 ((key & VIRTUAL_KEY_BIT) ? 0 : ((key & STB_TEXTEDIT_K_CONTROL) ? 0 : (key & (~0xF0000000))));
64 #define STB_TEXTEDIT_GETCHAR(tc, i) ITextEntryControl::GetChar (tc, i)
65 #define STB_TEXTEDIT_NEWLINE '\n'
66 #define STB_TEXTEDIT_IS_SPACE(ch) isspace(ch)
67 #define STB_TEXTEDIT_DELETECHARS ITextEntryControl::DeleteChars
68 #define STB_TEXTEDIT_INSERTCHARS ITextEntryControl::InsertChars
69 
70 #define STB_TEXTEDIT_IMPLEMENTATION
71 #include "stb_textedit.h"
72 
73 
74 ITextEntryControl::ITextEntryControl()
75 : IControl(IRECT())
76 {
77  stb_textedit_initialize_state(&mEditState, true);
78 
79  SetActionFunction([&](IControl* pCaller) {
80 
81  mDrawCursor = true;
82 
83  SetAnimation([&](IControl* pCaller) {
84  auto progress = pCaller->GetAnimationProgress();
85 
86  if(progress > 0.5) {
87  mDrawCursor = false;
88  pCaller->SetDirty(false);
89  }
90 
91  if(progress > 1.) {
92  pCaller->OnEndAnimation();
93  return;
94  }
95 
96  },
97  1000);
98  });
99 }
100 
102 {
103  g.FillRect(mText.mTextEntryBGColor, mRECT);
104 
105  StbTexteditRow row;
106  Layout(&row, this, 0);
107 
108  const bool hasSelection = mEditState.select_start != mEditState.select_end;
109  if (hasSelection)
110  {
111  float selectionStart = row.x0, selectionEnd = row.x0;
112  const int start = std::min(mEditState.select_start, mEditState.select_end);
113  const int end = std::max(mEditState.select_start, mEditState.select_end);
114  for (int i = 0; i < mCharWidths.GetSize() && i < end; ++i)
115  {
116  if (i < start)
117  selectionStart += mCharWidths.Get()[i];
118 
119  selectionEnd += mCharWidths.Get()[i];
120  }
121  IRECT selectionRect(selectionStart, mRECT.T + row.ymin, selectionEnd, mRECT.T + row.ymax);
122  selectionRect = selectionRect.GetVPadded(-mText.mSize*0.1f);
123  IBlend blend(EBlend::Default, 0.2f);
124  g.FillRect(mText.mTextEntryFGColor, selectionRect, &blend);
125  }
126 
127  g.DrawText(mText, StringConvert{}.to_bytes(mEditString).c_str(), mRECT);
128 
129  if (mDrawCursor && !hasSelection)
130  {
131  float cursorPos = row.x0;
132  for (int i = 0; i < mCharWidths.GetSize() && i < mEditState.cursor; ++i)
133  {
134  cursorPos += mCharWidths.Get()[i];
135  }
136  IRECT cursorRect(roundf(cursorPos-1), mRECT.T + row.ymin, roundf(cursorPos), mRECT.T + row.ymax);
137  cursorRect = cursorRect.GetVPadded(-mText.mSize*0.1f);
138  g.FillRect(mText.mTextEntryFGColor, cursorRect);
139  }
140 }
141 
142 template<typename Proc>
143 bool ITextEntryControl::CallSTB(Proc proc)
144 {
145  auto oldState = mEditState;
146  proc();
147 
148  if(memcmp(&oldState, &mEditState, sizeof (STB_TexteditState)) != 0)
149  {
150  OnStateChanged(); //TODO:
151  return true;
152  }
153 
154  return false;
155 }
156 
157 void ITextEntryControl::OnMouseDown(float x, float y, const IMouseMod& mod)
158 {
159  if(!mRECT.Contains(x, y))
160  {
161  CommitEdit();
162  return;
163  }
164 
165  if(mod.L)
166  {
167  CallSTB ([&]() {
168  stb_textedit_click(this, &mEditState, x, y);
169  });
170  }
171 
172  if(mod.R)
173  {
174  static IPopupMenu menu {"", {"Cut", "Copy", "Paste"}, [&](IPopupMenu* pMenu) {
175  switch (pMenu->GetChosenItemIdx()) {
176  case 0: Cut(); break;
177  case 1: CopySelection(); break;
178  case 2: Paste(); break;
179  default:
180  break;
181  }
182  }
183  };
184 
185  GetUI()->CreatePopupMenu(*this, menu, x, y);
186  }
187 }
188 
189 void ITextEntryControl::OnMouseDrag(float x, float y, float dX, float dY, const IMouseMod& mod)
190 {
191  if (mod.L)
192  {
193  CallSTB([&]() {
194  stb_textedit_drag(this, &mEditState, x, y);
195  });
196  }
197 }
198 
199 void ITextEntryControl::OnMouseDblClick(float x, float y, const IMouseMod& mod)
200 {
201  SelectAll();
202 }
203 
204 void ITextEntryControl::OnMouseUp(float x, float y, const IMouseMod& mod)
205 {
206  if (mod.L)
207  {
208  CallSTB([&]() {
209  stb_textedit_drag(this, &mEditState, x, y);
210  });
211 
212  SetDirty(true);
213  }
214 }
215 
216 bool ITextEntryControl::OnKeyDown(float x, float y, const IKeyPress& key)
217 {
218  if (key.C)
219  {
220  switch (key.VK)
221  {
222  case 'A':
223  {
224  SelectAll();
225  return true;
226  }
227  case 'X':
228  {
229  Cut();
230  return true;
231  }
232  case 'C':
233  {
234  CopySelection();
235  return true;
236  }
237  case 'V':
238  {
239  Paste();
240  return true;
241  }
242  case 'Z':
243  {
244  if (key.S)
245  CallSTB([&]() { stb_textedit_key(this, &mEditState, STB_TEXTEDIT_K_REDO); });
246  else
247  CallSTB([&]() { stb_textedit_key(this, &mEditState, STB_TEXTEDIT_K_UNDO); });
248  return true;
249  }
250 
251  default:
252  break;
253  }
254  }
255 
256  int stbKey;
257 
258  wdl_utf8_parsechar(key.utf8, &stbKey);
259 
260  switch (key.VK)
261  {
262  case kVK_SPACE: stbKey = ' '; break;
263  case kVK_TAB: return false;
264  case kVK_DELETE: stbKey = STB_TEXTEDIT_K_DELETE; break;
265  case kVK_BACK: stbKey = STB_TEXTEDIT_K_BACKSPACE; break;
266  case kVK_LEFT: stbKey = STB_TEXTEDIT_K_LEFT; break;
267  case kVK_RIGHT: stbKey = STB_TEXTEDIT_K_RIGHT; break;
268  case kVK_UP: stbKey = STB_TEXTEDIT_K_UP; break;
269  case kVK_DOWN: stbKey = STB_TEXTEDIT_K_DOWN; break;
270  case kVK_PRIOR: stbKey = STB_TEXTEDIT_K_PGUP; break;
271  case kVK_NEXT: stbKey = STB_TEXTEDIT_K_PGDOWN; break;
272  case kVK_HOME: stbKey = STB_TEXTEDIT_K_LINESTART; break;
273  case kVK_END: stbKey = STB_TEXTEDIT_K_LINEEND; break;
274  case kVK_RETURN: CommitEdit(); break;
275  case kVK_ESCAPE: DismissEdit(); break;
276  default:
277  {
278  // validate input based on param type
279  IControl* pControlInTextEntry = GetUI()->GetControlInTextEntry();
280 
281  if(!pControlInTextEntry)
282  return false;
283 
284  const IParam* pParam = pControlInTextEntry->GetParam();
285 
286  if(pParam)
287  {
288  switch (pParam->Type())
289  {
290  case IParam::kTypeEnum:
291  case IParam::kTypeInt:
292  case IParam::kTypeBool:
293  {
294  if (key.VK >= '0' && key.VK <= '9' && !key.S)
295  break;
296  if (key.VK >= kVK_NUMPAD0 && key.VK <= kVK_NUMPAD9)
297  break;
298  if (stbKey == '+' || stbKey == '-')
299  break;
300  stbKey = 0;
301  break;
302  }
303  case IParam::kTypeDouble:
304  {
305  if (key.VK >= '0' && key.VK <= '9' && !key.S)
306  break;
307  if (key.VK >= kVK_NUMPAD0 && key.VK <= kVK_NUMPAD9)
308  break;
309  if (stbKey == '+' || stbKey == '-' || stbKey == '.')
310  break;
311  stbKey = 0;
312  break;
313  }
314  default:
315  break;
316  }
317  }
318 
319  if (stbKey == 0)
320  {
321  stbKey = (key.VK) | VIRTUAL_KEY_BIT;
322  }
323  break;
324  }
325  }
326 
327  if (key.C)
328  stbKey |= STB_TEXTEDIT_K_CONTROL;
329  if (key.A)
330  stbKey |= STB_TEXTEDIT_K_ALT;
331  if (key.S)
332  stbKey |= STB_TEXTEDIT_K_SHIFT;
333 
334  return CallSTB([&]() { stb_textedit_key(this, &mEditState, stbKey); }) ? true : false;
335 }
336 
337 void ITextEntryControl::OnEndAnimation()
338 {
339  if(mEditing)
340  SetDirty(true);
341 }
342 
343 void ITextEntryControl::CopySelection()
344 {
345  if (mEditState.select_start != mEditState.select_end)
346  {
347  const int start = std::min(mEditState.select_start, mEditState.select_end);
348  const int end = std::max(mEditState.select_start, mEditState.select_end);
349  GetUI()->SetTextInClipboard(StringConvert{}.to_bytes(mEditString.data() + start, mEditString.data() + end).c_str());
350  }
351 }
352 
353 void ITextEntryControl::Paste()
354 {
355  WDL_String fromClipboard;
356  if (GetUI()->GetTextFromClipboard(fromClipboard))
357  {
358  CallSTB([&] {
359  auto uText = StringConvert{}.from_bytes (fromClipboard.Get(), fromClipboard.Get() + fromClipboard.GetLength());
360  stb_textedit_paste (this, &mEditState, uText.data(), (int) uText.size());
361  });
362  }
363 }
364 
365 void ITextEntryControl::Cut()
366 {
367  CopySelection();
368  CallSTB([&] {
369  stb_textedit_cut(this, &mEditState);
370  });
371 }
372 
373 void ITextEntryControl::SelectAll()
374 {
375  CallSTB([&]() {
376  mEditState.select_start = 0;
377  mEditState.select_end = static_cast<int>(mEditString.length());
378  });
379 }
380 
381 //static
382 int ITextEntryControl::DeleteChars(ITextEntryControl* _this, size_t pos, size_t num)
383 {
384  _this->mEditString.erase(pos, num);
385  _this->SetStr(StringConvert{}.to_bytes(_this->mEditString).c_str());
386  _this->OnTextChange();
387  return true; // TODO: Error checking
388 }
389 
390 //static
391 int ITextEntryControl::InsertChars(ITextEntryControl* _this, size_t pos, const char16_t* text, size_t num)
392 {
393  _this->mEditString.insert(pos, text, num);
394  _this->SetStr(StringConvert{}.to_bytes(_this->mEditString).c_str());
395  _this->OnTextChange();
396  return true;
397 }
398 
399 //static
400 char16_t ITextEntryControl::GetChar(ITextEntryControl* _this, int pos)
401 {
402  return _this->mEditString[pos];
403 }
404 
405 //static
406 int ITextEntryControl::GetLength(ITextEntryControl* _this)
407 {
408  return static_cast<int>(_this->mEditString.size());
409 }
410 
411 //static
412 void ITextEntryControl::Layout(StbTexteditRow* row, ITextEntryControl* _this, int start_i)
413 {
414  assert (start_i == 0);
415 
416  _this->FillCharWidthCache();
417  float textWidth = 0.;
418 
419  for (int i = 0; i < _this->mCharWidths.GetSize(); i++)
420  {
421  textWidth += _this->mCharWidths.Get()[i];
422  }
423 
424  row->num_chars = GetLength(_this);
425  row->baseline_y_delta = 1.25;
426 
427  switch (_this->GetText().mAlign)
428  {
429  case EAlign::Near:
430  {
431  row->x0 = _this->GetRECT().L;
432  row->x1 = row->x0 + textWidth;
433  break;
434  }
435  case EAlign::Center:
436  {
437  row->x0 = _this->GetRECT().MW() - (textWidth * 0.5f);
438  row->x1 = row->x0 + textWidth;
439  break;
440  }
441  case EAlign::Far:
442  {
443  row->x0 = _this->GetRECT().R - textWidth;
444  row->x1 = row->x0 + textWidth;
445  }
446  }
447 
448  switch (_this->GetText().mVAlign)
449  {
450  case EVAlign::Top:
451  {
452  row->ymin = 0;
453  break;
454  }
455  case EVAlign::Middle:
456  {
457  row->ymin = _this->GetRECT().H()*0.5f - _this->GetText().mSize * 0.5f;
458  break;
459  }
460  case EVAlign::Bottom:
461  {
462  row->ymin = _this->GetRECT().H() - _this->GetText().mSize;
463  break;
464  }
465  }
466 
467  row->ymax = row->ymin + static_cast<float> (_this->GetText().mSize);
468 }
469 
470 //static
471 float ITextEntryControl::GetCharWidth(ITextEntryControl* _this, int n, int i)
472 {
473  _this->FillCharWidthCache();
474  return _this->mCharWidths.Get()[i]; // TODO: n not used?
475 }
476 
477 void ITextEntryControl::OnStateChanged()
478 {
479  SetDirty(false);
480 }
481 
482 void ITextEntryControl::OnTextChange()
483 {
484  mCharWidths.Resize(0, false);
485  FillCharWidthCache();
486 }
487 
488 void ITextEntryControl::FillCharWidthCache()
489 {
490  // only calculate when empty
491  if (mCharWidths.GetSize())
492  return;
493 
494  const int len = static_cast<int>(mEditString.size());
495  mCharWidths.Resize(len, false);
496  for (int i = 0; i < len; ++i)
497  {
498  mCharWidths.Get()[i] = MeasureCharWidth(mEditString[i], i == 0 ? 0 : mEditString[i - 1]);
499  }
500 }
501 
502 void ITextEntryControl::CalcCursorSizes()
503 {
504  //TODO: cache cursor size and location?
505 }
506 
507 // the width of character 'c' should include the kerning between it and the next character,
508 // so that when adding up character widths in the cache we can get to the beginning of the visible glyph,
509 // which is important for cursor placement to look correct.
510 // stb_textedit behavior for clicking in the text field is to place the cursor in front of a character
511 // when the xpos is less than halfway into the width of the character and in front of the following character otherwise.
512 // see: https://github.com/nothings/stb/issues/6
513 float ITextEntryControl::MeasureCharWidth(char16_t c, char16_t nc)
514 {
515  IRECT bounds;
516 
517  if (nc)
518  {
519  std::string str (StringConvert{}.to_bytes (nc));
520  float ncWidth = GetUI()->MeasureText(mText, str.c_str(), bounds);
521  str += StringConvert{}.to_bytes (c);
522  float tcWidth = GetUI()->MeasureText(mText, str.c_str(), bounds);
523  return tcWidth - ncWidth;
524  }
525 
526  std::string str (StringConvert{}.to_bytes (c));
527  return GetUI()->MeasureText(mText, str.c_str(), bounds);
528 }
529 
530 void ITextEntryControl::CreateTextEntry(int paramIdx, const IText& text, const IRECT& bounds, int length, const char* str)
531 {
532  SetTargetAndDrawRECTs(bounds);
533  SetText(text);
534  mText.mFGColor = mText.mTextEntryFGColor;
535  SetStr(str);
536  SelectAll();
537  mEditState.cursor = 0;
538  OnTextChange();
539  SetDirty(true);
540  mEditing = true;
541 }
542 
543 void ITextEntryControl::DismissEdit()
544 {
545  mEditing = false;
547  GetUI()->mInTextEntry = nullptr;
549 }
550 
551 void ITextEntryControl::CommitEdit()
552 {
553  mEditing = false;
554  GetUI()->SetControlValueAfterTextEdit(StringConvert{}.to_bytes(mEditString).c_str());
557 }
558 
559 void ITextEntryControl::SetStr(const char* str)
560 {
561  mCharWidths.Resize(0, false);
562  mEditString = StringConvert{}.from_bytes(std::string(str));
563 }
bool Contains(const IRECT &rhs) const
Returns true if this IRECT completely contains rhs.
float MW() const
The lowest level base class of an IGraphics control.
Definition: IControl.h:42
A Text entry widget drawn by IGraphics.
virtual void SetText(const IText &txt)
Set the Text object typically used to determine font/layout/size etc of the main text in a control...
Definition: IControl.h:286
Used to manage a rectangular area, independent of draw class/platform.
Used to manage composite/blend operations, independent of draw class/platform.
A Text entry widget drawn by IGraphics to optionally override platform text entries.
Used to manage mouse modifiers i.e.
void SetControlValueAfterTextEdit(const char *str)
Called by the platform class after returning from a text entry in order to update a control with a ne...
Definition: IGraphics.cpp:232
const IParam * GetParam(int valIdx=0) const
Get a const pointer to the IParam object (owned by the editor delegate class), associated with this c...
Definition: IControl.cpp:120
Include to get consistently named preprocessor macros for different platforms and logging functionali...
void OnMouseDrag(float x, float y, float dX, float dY, const IMouseMod &mod) override
Implement this method to respond to a mouse drag event on this control.
IPlug&#39;s parameter class.
IControl * SetActionFunction(IActionFunction actionFunc)
Set an Action Function for this control.
Definition: IControl.h:201
IRECT GetVPadded(float padding) const
Get a copy of this IRECT padded in the Y-axis N.B.
void DrawText(const IText &text, const char *str, const IRECT &bounds, const IBlend *pBlend=0)
Draw some text to the graphics context in a specific rectangle.
Definition: IGraphics.cpp:631
bool OnKeyDown(float x, float y, const IKeyPress &key) override
Implement this method to respond to a key down event on this control.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
void SetAllControlsDirty()
Calls SetDirty() on every control.
Definition: IGraphics.cpp:543
void Draw(IGraphics &g) override
Draw the control to the graphics context.
float R
Right side of the rectangle (X + W)
virtual bool SetTextInClipboard(const char *str)=0
Set text in the clipboard.
IControl * GetControlInTextEntry()
Definition: IGraphics.h:1094
float H() const
double GetAnimationProgress() const
Get the progress in a control&#39;s animation, in the range 0-1.
Definition: IControl.cpp:429
const IRECT & GetRECT() const
Get the rectangular draw area for this control, within the graphics context.
Definition: IControl.h:305
void CreatePopupMenu(IControl &control, IPopupMenu &menu, const IRECT &bounds, int valIdx=0)
Shows a pop up/contextual menu in relation to a rectangular region of the graphics context...
Definition: IGraphics.cpp:1937
A class for setting the contents of a pop up menu.
IText is used to manage font and text/text entry style for a piece of text on the UI...
void OnMouseUp(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse up event on this control.
The lowest level base class of an IGraphics context.
Definition: IGraphics.h:86
void SetAnimation(IAnimationFunction func)
Set the animation function.
Definition: IControl.h:477
virtual void FillRect(const IColor &color, const IRECT &bounds, const IBlend *pBlend=0)
Fill a rectangular region of the graphics context with a color.
Definition: IGraphics.cpp:2547
void SetTargetAndDrawRECTs(const IRECT &bounds)
Set BOTH the draw rect and the target area, within the graphics context for this control.
Definition: IControl.h:321
void OnMouseDblClick(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse double click event on this control.
EParamType Type() const
Get the parameter&#39;s type.
IGraphics * GetUI()
Definition: IControl.h:452
float L
Left side of the rectangle (X)
Used for key press info, such as ASCII representation, virtual key (mapped to win32 codes) and modifi...
Definition: IPlugStructs.h:612
float T
Top of the rectangle (Y)
virtual float MeasureText(const IText &text, const char *str, IRECT &bounds) const
Measure the rectangular region that some text will occupy.
Definition: IGraphics.cpp:639
virtual void SetDirty(bool triggerAction=true, int valIdx=kNoValIdx)
Mark the control as dirty, i.e.
Definition: IControl.cpp:196
const IText & GetText() const
Get the Text object for the control.
Definition: IControl.h:282