iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
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 */)
26std::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
31using StringConvert = std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>;
32
33using namespace iplug;
34using 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
74ITextEntryControl::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
142template<typename Proc>
143bool 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
157void 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
189void 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
199void ITextEntryControl::OnMouseDblClick(float x, float y, const IMouseMod& mod)
200{
201 SelectAll();
202}
203
204void 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
216bool 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
337void ITextEntryControl::OnEndAnimation()
338{
339 if(mEditing)
340 SetDirty(true);
341}
342
343void 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
353void 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
365void ITextEntryControl::Cut()
366{
367 CopySelection();
368 CallSTB([&] {
369 stb_textedit_cut(this, &mEditState);
370 });
371}
372
373void ITextEntryControl::SelectAll()
374{
375 CallSTB([&]() {
376 mEditState.select_start = 0;
377 mEditState.select_end = static_cast<int>(mEditString.length());
378 });
379}
380
381//static
382int 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
391int 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
400char16_t ITextEntryControl::GetChar(ITextEntryControl* _this, int pos)
401{
402 return _this->mEditString[pos];
403}
404
405//static
406int ITextEntryControl::GetLength(ITextEntryControl* _this)
407{
408 return static_cast<int>(_this->mEditString.size());
409}
410
411//static
412void 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
471float ITextEntryControl::GetCharWidth(ITextEntryControl* _this, int n, int i)
472{
473 _this->FillCharWidthCache();
474 return _this->mCharWidths.Get()[i]; // TODO: n not used?
475}
476
477void ITextEntryControl::OnStateChanged()
478{
479 SetDirty(false);
480}
481
482void ITextEntryControl::OnTextChange()
483{
484 mCharWidths.Resize(0, false);
485 FillCharWidthCache();
486}
487
488void 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
502void 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
513float 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
530void 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
543void ITextEntryControl::DismissEdit()
544{
545 mEditing = false;
549}
550
551void ITextEntryControl::CommitEdit()
552{
553 mEditing = false;
554 GetUI()->SetControlValueAfterTextEdit(StringConvert{}.to_bytes(mEditString).c_str());
557}
558
559void ITextEntryControl::SetStr(const char* str)
560{
561 mCharWidths.Resize(0, false);
562 mEditString = StringConvert{}.from_bytes(std::string(str));
563}
Include to get consistently named preprocessor macros for different platforms and logging functionali...
A Text entry widget drawn by IGraphics to optionally override platform text entries.
The lowest level base class of an IGraphics control.
Definition: IControl.h:49
IGraphics * GetUI()
Definition: IControl.h:467
const IRECT & GetRECT() const
Get the rectangular draw area for this control, within the graphics context.
Definition: IControl.h:311
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:292
double GetAnimationProgress() const
Get the progress in a control's animation, in the range 0-1.
Definition: IControl.cpp:431
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:122
const IText & GetText() const
Get the Text object for the control.
Definition: IControl.h:288
void SetTargetAndDrawRECTs(const IRECT &bounds)
Set BOTH the draw rect and the target area, within the graphics context for this control.
Definition: IControl.h:327
virtual void SetDirty(bool triggerAction=true, int valIdx=kNoValIdx)
Mark the control as dirty, i.e.
Definition: IControl.cpp:198
The lowest level base class of an IGraphics context.
Definition: IGraphics.h:86
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:670
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:1960
void ClearInTextEntryControl()
Called when the text entry is dismissed, to reset mInTextEntry.
Definition: IGraphics.h:1134
virtual bool SetTextInClipboard(const char *str)=0
Set text in the clipboard.
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:2569
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:235
void SetAllControlsDirty()
Calls SetDirty() on every control.
Definition: IGraphics.cpp:582
IControl * GetControlInTextEntry()
Definition: IGraphics.h:1131
virtual float MeasureText(const IText &text, const char *str, IRECT &bounds) const
Measure the rectangular region that some text will occupy.
Definition: IGraphics.cpp:678
IPlug's parameter class.
EParamType Type() const
Get the parameter's type.
A class for setting the contents of a pop up menu.
A Text entry widget drawn by IGraphics.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
void Draw(IGraphics &g) override
Draw the control to the graphics context.
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.
void OnMouseDblClick(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse double click event on this control.
bool OnKeyDown(float x, float y, const IKeyPress &key) override
Implement this method to respond to a key down event on this control.
void OnMouseUp(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse up event on this control.
Used to manage composite/blend operations, independent of draw class/platform.
Used for key press info, such as ASCII representation, virtual key (mapped to win32 codes) and modifi...
Definition: IPlugStructs.h:616
Used to manage mouse modifiers i.e.
Used to manage a rectangular area, independent of draw class/platform.
IRECT GetVPadded(float padding) const
Get a copy of this IRECT padded in the Y-axis N.B.
float H() const
float MW() const
bool Contains(const IRECT &rhs) const
Returns true if this IRECT completely contains rhs.
IText is used to manage font and text/text entry style for a piece of text on the UI,...