| 1 | /* |
| 2 | src/textbox.cpp -- Fancy text box with builtin regular |
| 3 | expression-based validation |
| 4 | |
| 5 | The text box widget was contributed by Christian Schueller. |
| 6 | |
| 7 | NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. |
| 8 | The widget drawing code is based on the NanoVG demo application |
| 9 | by Mikko Mononen. |
| 10 | |
| 11 | All rights reserved. Use of this source code is governed by a |
| 12 | BSD-style license that can be found in the LICENSE.txt file. |
| 13 | */ |
| 14 | |
| 15 | #include <nanogui/window.h> |
| 16 | #include <nanogui/screen.h> |
| 17 | #include <nanogui/textbox.h> |
| 18 | #include <nanogui/opengl.h> |
| 19 | #include <nanogui/theme.h> |
| 20 | #include <nanogui/serializer/core.h> |
| 21 | #include <regex> |
| 22 | #include <iostream> |
| 23 | |
| 24 | NAMESPACE_BEGIN(nanogui) |
| 25 | |
| 26 | TextBox::TextBox(Widget *parent,const std::string &value) |
| 27 | : Widget(parent), |
| 28 | mEditable(false), |
| 29 | mSpinnable(false), |
| 30 | mCommitted(true), |
| 31 | mValue(value), |
| 32 | mDefaultValue("" ), |
| 33 | mAlignment(Alignment::Center), |
| 34 | mUnits("" ), |
| 35 | mFormat("" ), |
| 36 | mUnitsImage(-1), |
| 37 | mValidFormat(true), |
| 38 | mValueTemp(value), |
| 39 | mCursorPos(-1), |
| 40 | mSelectionPos(-1), |
| 41 | mMousePos(Vector2i(-1,-1)), |
| 42 | mMouseDownPos(Vector2i(-1,-1)), |
| 43 | mMouseDragPos(Vector2i(-1,-1)), |
| 44 | mMouseDownModifier(0), |
| 45 | mTextOffset(0), |
| 46 | mLastClick(0) { |
| 47 | if (mTheme) mFontSize = mTheme->mTextBoxFontSize; |
| 48 | mIconExtraScale = 0.8f;// widget override |
| 49 | } |
| 50 | |
| 51 | void TextBox::setEditable(bool editable) { |
| 52 | mEditable = editable; |
| 53 | setCursor(editable ? Cursor::IBeam : Cursor::Arrow); |
| 54 | } |
| 55 | |
| 56 | void TextBox::setTheme(Theme *theme) { |
| 57 | Widget::setTheme(theme); |
| 58 | if (mTheme) |
| 59 | mFontSize = mTheme->mTextBoxFontSize; |
| 60 | } |
| 61 | |
| 62 | Vector2i TextBox::preferredSize(NVGcontext *ctx) const { |
| 63 | Vector2i size(0, fontSize() * 1.4f); |
| 64 | |
| 65 | float uw = 0; |
| 66 | if (mUnitsImage > 0) { |
| 67 | int w, h; |
| 68 | nvgImageSize(ctx, mUnitsImage, &w, &h); |
| 69 | float uh = size(1) * 0.4f; |
| 70 | uw = w * uh / h; |
| 71 | } else if (!mUnits.empty()) { |
| 72 | uw = nvgTextBounds(ctx, 0, 0, mUnits.c_str(), nullptr, nullptr); |
| 73 | } |
| 74 | float sw = 0; |
| 75 | if (mSpinnable) { |
| 76 | sw = 14.f; |
| 77 | } |
| 78 | |
| 79 | float ts = nvgTextBounds(ctx, 0, 0, mValue.c_str(), nullptr, nullptr); |
| 80 | size(0) = size(1) + ts + uw + sw; |
| 81 | return size; |
| 82 | } |
| 83 | |
| 84 | void TextBox::draw(NVGcontext* ctx) { |
| 85 | Widget::draw(ctx); |
| 86 | |
| 87 | NVGpaint bg = nvgBoxGradient(ctx, |
| 88 | mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2, |
| 89 | 3, 4, Color(255, 32), Color(32, 32)); |
| 90 | NVGpaint fg1 = nvgBoxGradient(ctx, |
| 91 | mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2, |
| 92 | 3, 4, Color(150, 32), Color(32, 32)); |
| 93 | NVGpaint fg2 = nvgBoxGradient(ctx, |
| 94 | mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2, |
| 95 | 3, 4, nvgRGBA(255, 0, 0, 100), nvgRGBA(255, 0, 0, 50)); |
| 96 | |
| 97 | nvgBeginPath(ctx); |
| 98 | nvgRoundedRect(ctx, mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, |
| 99 | mSize.y() - 2, 3); |
| 100 | |
| 101 | if (mEditable && focused()) |
| 102 | mValidFormat ? nvgFillPaint(ctx, fg1) : nvgFillPaint(ctx, fg2); |
| 103 | else if (mSpinnable && mMouseDownPos.x() != -1) |
| 104 | nvgFillPaint(ctx, fg1); |
| 105 | else |
| 106 | nvgFillPaint(ctx, bg); |
| 107 | |
| 108 | nvgFill(ctx); |
| 109 | |
| 110 | nvgBeginPath(ctx); |
| 111 | nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1, |
| 112 | mSize.y() - 1, 2.5f); |
| 113 | nvgStrokeColor(ctx, Color(0, 48)); |
| 114 | nvgStroke(ctx); |
| 115 | |
| 116 | nvgFontSize(ctx, fontSize()); |
| 117 | nvgFontFace(ctx, "sans" ); |
| 118 | Vector2i drawPos(mPos.x(), mPos.y() + mSize.y() * 0.5f + 1); |
| 119 | |
| 120 | float xSpacing = mSize.y() * 0.3f; |
| 121 | |
| 122 | float unitWidth = 0; |
| 123 | |
| 124 | if (mUnitsImage > 0) { |
| 125 | int w, h; |
| 126 | nvgImageSize(ctx, mUnitsImage, &w, &h); |
| 127 | float unitHeight = mSize.y() * 0.4f; |
| 128 | unitWidth = w * unitHeight / h; |
| 129 | NVGpaint imgPaint = nvgImagePattern( |
| 130 | ctx, mPos.x() + mSize.x() - xSpacing - unitWidth, |
| 131 | drawPos.y() - unitHeight * 0.5f, unitWidth, unitHeight, 0, |
| 132 | mUnitsImage, mEnabled ? 0.7f : 0.35f); |
| 133 | nvgBeginPath(ctx); |
| 134 | nvgRect(ctx, mPos.x() + mSize.x() - xSpacing - unitWidth, |
| 135 | drawPos.y() - unitHeight * 0.5f, unitWidth, unitHeight); |
| 136 | nvgFillPaint(ctx, imgPaint); |
| 137 | nvgFill(ctx); |
| 138 | unitWidth += 2; |
| 139 | } else if (!mUnits.empty()) { |
| 140 | unitWidth = nvgTextBounds(ctx, 0, 0, mUnits.c_str(), nullptr, nullptr); |
| 141 | nvgFillColor(ctx, Color(255, mEnabled ? 64 : 32)); |
| 142 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE); |
| 143 | nvgText(ctx, mPos.x() + mSize.x() - xSpacing, drawPos.y(), |
| 144 | mUnits.c_str(), nullptr); |
| 145 | unitWidth += 2; |
| 146 | } |
| 147 | |
| 148 | float spinArrowsWidth = 0.f; |
| 149 | |
| 150 | if (mSpinnable && !focused()) { |
| 151 | spinArrowsWidth = 14.f; |
| 152 | |
| 153 | nvgFontFace(ctx, "icons" ); |
| 154 | nvgFontSize(ctx, ((mFontSize < 0) ? mTheme->mButtonFontSize : mFontSize) * icon_scale()); |
| 155 | |
| 156 | bool spinning = mMouseDownPos.x() != -1; |
| 157 | |
| 158 | /* up button */ { |
| 159 | bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea::Top; |
| 160 | nvgFillColor(ctx, (mEnabled && (hover || spinning)) ? mTheme->mTextColor : mTheme->mDisabledTextColor); |
| 161 | auto icon = utf8(mTheme->mTextBoxUpIcon); |
| 162 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); |
| 163 | Vector2f iconPos(mPos.x() + 4.f, |
| 164 | mPos.y() + mSize.y()/2.f - xSpacing/2.f); |
| 165 | nvgText(ctx, iconPos.x(), iconPos.y(), icon.data(), nullptr); |
| 166 | } |
| 167 | |
| 168 | /* down button */ { |
| 169 | bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea::Bottom; |
| 170 | nvgFillColor(ctx, (mEnabled && (hover || spinning)) ? mTheme->mTextColor : mTheme->mDisabledTextColor); |
| 171 | auto icon = utf8(mTheme->mTextBoxDownIcon); |
| 172 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); |
| 173 | Vector2f iconPos(mPos.x() + 4.f, |
| 174 | mPos.y() + mSize.y()/2.f + xSpacing/2.f + 1.5f); |
| 175 | nvgText(ctx, iconPos.x(), iconPos.y(), icon.data(), nullptr); |
| 176 | } |
| 177 | |
| 178 | nvgFontSize(ctx, fontSize()); |
| 179 | nvgFontFace(ctx, "sans" ); |
| 180 | } |
| 181 | |
| 182 | switch (mAlignment) { |
| 183 | case Alignment::Left: |
| 184 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); |
| 185 | drawPos.x() += xSpacing + spinArrowsWidth; |
| 186 | break; |
| 187 | case Alignment::Right: |
| 188 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE); |
| 189 | drawPos.x() += mSize.x() - unitWidth - xSpacing; |
| 190 | break; |
| 191 | case Alignment::Center: |
| 192 | nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); |
| 193 | drawPos.x() += mSize.x() * 0.5f; |
| 194 | break; |
| 195 | } |
| 196 | |
| 197 | nvgFontSize(ctx, fontSize()); |
| 198 | nvgFillColor(ctx, mEnabled && (!mCommitted || !mValue.empty()) ? |
| 199 | mTheme->mTextColor : |
| 200 | mTheme->mDisabledTextColor); |
| 201 | |
| 202 | // clip visible text area |
| 203 | float clipX = mPos.x() + xSpacing + spinArrowsWidth - 1.0f; |
| 204 | float clipY = mPos.y() + 1.0f; |
| 205 | float clipWidth = mSize.x() - unitWidth - spinArrowsWidth - 2 * xSpacing + 2.0f; |
| 206 | float clipHeight = mSize.y() - 3.0f; |
| 207 | |
| 208 | nvgSave(ctx); |
| 209 | nvgIntersectScissor(ctx, clipX, clipY, clipWidth, clipHeight); |
| 210 | |
| 211 | Vector2i oldDrawPos(drawPos); |
| 212 | drawPos.x() += mTextOffset; |
| 213 | |
| 214 | if (mCommitted) { |
| 215 | nvgText(ctx, drawPos.x(), drawPos.y(), |
| 216 | mValue.empty() ? mPlaceholder.c_str() : mValue.c_str(), nullptr); |
| 217 | } else { |
| 218 | const int maxGlyphs = 1024; |
| 219 | NVGglyphPosition glyphs[maxGlyphs]; |
| 220 | float textBound[4]; |
| 221 | nvgTextBounds(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), |
| 222 | nullptr, textBound); |
| 223 | float lineh = textBound[3] - textBound[1]; |
| 224 | |
| 225 | // find cursor positions |
| 226 | int nglyphs = |
| 227 | nvgTextGlyphPositions(ctx, drawPos.x(), drawPos.y(), |
| 228 | mValueTemp.c_str(), nullptr, glyphs, maxGlyphs); |
| 229 | updateCursor(ctx, textBound[2], glyphs, nglyphs); |
| 230 | |
| 231 | // compute text offset |
| 232 | int prevCPos = mCursorPos > 0 ? mCursorPos - 1 : 0; |
| 233 | int nextCPos = mCursorPos < nglyphs ? mCursorPos + 1 : nglyphs; |
| 234 | float prevCX = cursorIndex2Position(prevCPos, textBound[2], glyphs, nglyphs); |
| 235 | float nextCX = cursorIndex2Position(nextCPos, textBound[2], glyphs, nglyphs); |
| 236 | |
| 237 | if (nextCX > clipX + clipWidth) |
| 238 | mTextOffset -= nextCX - (clipX + clipWidth) + 1; |
| 239 | if (prevCX < clipX) |
| 240 | mTextOffset += clipX - prevCX + 1; |
| 241 | |
| 242 | drawPos.x() = oldDrawPos.x() + mTextOffset; |
| 243 | |
| 244 | // draw text with offset |
| 245 | nvgText(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), nullptr); |
| 246 | nvgTextBounds(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), |
| 247 | nullptr, textBound); |
| 248 | |
| 249 | // recompute cursor positions |
| 250 | nglyphs = nvgTextGlyphPositions(ctx, drawPos.x(), drawPos.y(), |
| 251 | mValueTemp.c_str(), nullptr, glyphs, maxGlyphs); |
| 252 | |
| 253 | if (mCursorPos > -1) { |
| 254 | if (mSelectionPos > -1) { |
| 255 | float caretx = cursorIndex2Position(mCursorPos, textBound[2], |
| 256 | glyphs, nglyphs); |
| 257 | float selx = cursorIndex2Position(mSelectionPos, textBound[2], |
| 258 | glyphs, nglyphs); |
| 259 | |
| 260 | if (caretx > selx) |
| 261 | std::swap(caretx, selx); |
| 262 | |
| 263 | // draw selection |
| 264 | nvgBeginPath(ctx); |
| 265 | nvgFillColor(ctx, nvgRGBA(255, 255, 255, 80)); |
| 266 | nvgRect(ctx, caretx, drawPos.y() - lineh * 0.5f, selx - caretx, |
| 267 | lineh); |
| 268 | nvgFill(ctx); |
| 269 | } |
| 270 | |
| 271 | float caretx = cursorIndex2Position(mCursorPos, textBound[2], glyphs, nglyphs); |
| 272 | |
| 273 | // draw cursor |
| 274 | nvgBeginPath(ctx); |
| 275 | nvgMoveTo(ctx, caretx, drawPos.y() - lineh * 0.5f); |
| 276 | nvgLineTo(ctx, caretx, drawPos.y() + lineh * 0.5f); |
| 277 | nvgStrokeColor(ctx, nvgRGBA(255, 192, 0, 255)); |
| 278 | nvgStrokeWidth(ctx, 1.0f); |
| 279 | nvgStroke(ctx); |
| 280 | } |
| 281 | } |
| 282 | nvgRestore(ctx); |
| 283 | } |
| 284 | |
| 285 | bool TextBox::mouseButtonEvent(const Vector2i &p, int button, bool down, |
| 286 | int modifiers) { |
| 287 | |
| 288 | if (button == GLFW_MOUSE_BUTTON_1 && down && !mFocused) { |
| 289 | if (!mSpinnable || spinArea(p) == SpinArea::None) /* not on scrolling arrows */ |
| 290 | requestFocus(); |
| 291 | } |
| 292 | |
| 293 | if (mEditable && focused()) { |
| 294 | if (down) { |
| 295 | mMouseDownPos = p; |
| 296 | mMouseDownModifier = modifiers; |
| 297 | |
| 298 | double time = glfwGetTime(); |
| 299 | if (time - mLastClick < 0.25) { |
| 300 | /* Double-click: select all text */ |
| 301 | mSelectionPos = 0; |
| 302 | mCursorPos = (int) mValueTemp.size(); |
| 303 | mMouseDownPos = Vector2i(-1, -1); |
| 304 | } |
| 305 | mLastClick = time; |
| 306 | } else { |
| 307 | mMouseDownPos = Vector2i(-1, -1); |
| 308 | mMouseDragPos = Vector2i(-1, -1); |
| 309 | } |
| 310 | return true; |
| 311 | } else if (mSpinnable && !focused()) { |
| 312 | if (down) { |
| 313 | if (spinArea(p) == SpinArea::None) { |
| 314 | mMouseDownPos = p; |
| 315 | mMouseDownModifier = modifiers; |
| 316 | |
| 317 | double time = glfwGetTime(); |
| 318 | if (time - mLastClick < 0.25) { |
| 319 | /* Double-click: reset to default value */ |
| 320 | mValue = mDefaultValue; |
| 321 | if (mCallback) |
| 322 | mCallback(mValue); |
| 323 | |
| 324 | mMouseDownPos = Vector2i(-1, -1); |
| 325 | } |
| 326 | mLastClick = time; |
| 327 | } else { |
| 328 | mMouseDownPos = Vector2i(-1, -1); |
| 329 | mMouseDragPos = Vector2i(-1, -1); |
| 330 | } |
| 331 | } else { |
| 332 | mMouseDownPos = Vector2i(-1, -1); |
| 333 | mMouseDragPos = Vector2i(-1, -1); |
| 334 | } |
| 335 | return true; |
| 336 | } |
| 337 | |
| 338 | return false; |
| 339 | } |
| 340 | |
| 341 | bool TextBox::mouseMotionEvent(const Vector2i &p, const Vector2i & /* rel */, |
| 342 | int /* button */, int /* modifiers */) { |
| 343 | mMousePos = p; |
| 344 | |
| 345 | if (!mEditable) |
| 346 | setCursor(Cursor::Arrow); |
| 347 | else if (mSpinnable && !focused() && spinArea(mMousePos) != SpinArea::None) /* scrolling arrows */ |
| 348 | setCursor(Cursor::Hand); |
| 349 | else |
| 350 | setCursor(Cursor::IBeam); |
| 351 | |
| 352 | if (mEditable && focused()) { |
| 353 | return true; |
| 354 | } |
| 355 | return false; |
| 356 | } |
| 357 | |
| 358 | bool TextBox::mouseDragEvent(const Vector2i &p, const Vector2i &/* rel */, |
| 359 | int /* button */, int /* modifiers */) { |
| 360 | mMousePos = p; |
| 361 | mMouseDragPos = p; |
| 362 | |
| 363 | if (mEditable && focused()) { |
| 364 | return true; |
| 365 | } |
| 366 | return false; |
| 367 | } |
| 368 | |
| 369 | bool TextBox::focusEvent(bool focused) { |
| 370 | Widget::focusEvent(focused); |
| 371 | |
| 372 | std::string backup = mValue; |
| 373 | |
| 374 | if (mEditable) { |
| 375 | if (focused) { |
| 376 | mValueTemp = mValue; |
| 377 | mCommitted = false; |
| 378 | mCursorPos = 0; |
| 379 | } else { |
| 380 | if (mValidFormat) { |
| 381 | if (mValueTemp == "" ) |
| 382 | mValue = mDefaultValue; |
| 383 | else |
| 384 | mValue = mValueTemp; |
| 385 | } |
| 386 | |
| 387 | if (mCallback && !mCallback(mValue)) |
| 388 | mValue = backup; |
| 389 | |
| 390 | mValidFormat = true; |
| 391 | mCommitted = true; |
| 392 | mCursorPos = -1; |
| 393 | mSelectionPos = -1; |
| 394 | mTextOffset = 0; |
| 395 | } |
| 396 | |
| 397 | mValidFormat = (mValueTemp == "" ) || checkFormat(mValueTemp, mFormat); |
| 398 | } |
| 399 | |
| 400 | return true; |
| 401 | } |
| 402 | |
| 403 | bool TextBox::keyboardEvent(int key, int /* scancode */, int action, int modifiers) { |
| 404 | if (mEditable && focused()) { |
| 405 | if (action == GLFW_PRESS || action == GLFW_REPEAT) { |
| 406 | if (key == GLFW_KEY_LEFT) { |
| 407 | if (modifiers == GLFW_MOD_SHIFT) { |
| 408 | if (mSelectionPos == -1) |
| 409 | mSelectionPos = mCursorPos; |
| 410 | } else { |
| 411 | mSelectionPos = -1; |
| 412 | } |
| 413 | |
| 414 | if (mCursorPos > 0) |
| 415 | mCursorPos--; |
| 416 | } else if (key == GLFW_KEY_RIGHT) { |
| 417 | if (modifiers == GLFW_MOD_SHIFT) { |
| 418 | if (mSelectionPos == -1) |
| 419 | mSelectionPos = mCursorPos; |
| 420 | } else { |
| 421 | mSelectionPos = -1; |
| 422 | } |
| 423 | |
| 424 | if (mCursorPos < (int) mValueTemp.length()) |
| 425 | mCursorPos++; |
| 426 | } else if (key == GLFW_KEY_HOME) { |
| 427 | if (modifiers == GLFW_MOD_SHIFT) { |
| 428 | if (mSelectionPos == -1) |
| 429 | mSelectionPos = mCursorPos; |
| 430 | } else { |
| 431 | mSelectionPos = -1; |
| 432 | } |
| 433 | |
| 434 | mCursorPos = 0; |
| 435 | } else if (key == GLFW_KEY_END) { |
| 436 | if (modifiers == GLFW_MOD_SHIFT) { |
| 437 | if (mSelectionPos == -1) |
| 438 | mSelectionPos = mCursorPos; |
| 439 | } else { |
| 440 | mSelectionPos = -1; |
| 441 | } |
| 442 | |
| 443 | mCursorPos = (int) mValueTemp.size(); |
| 444 | } else if (key == GLFW_KEY_BACKSPACE) { |
| 445 | if (!deleteSelection()) { |
| 446 | if (mCursorPos > 0) { |
| 447 | mValueTemp.erase(mValueTemp.begin() + mCursorPos - 1); |
| 448 | mCursorPos--; |
| 449 | } |
| 450 | } |
| 451 | } else if (key == GLFW_KEY_DELETE) { |
| 452 | if (!deleteSelection()) { |
| 453 | if (mCursorPos < (int) mValueTemp.length()) |
| 454 | mValueTemp.erase(mValueTemp.begin() + mCursorPos); |
| 455 | } |
| 456 | } else if (key == GLFW_KEY_ENTER) { |
| 457 | if (!mCommitted) |
| 458 | focusEvent(false); |
| 459 | } else if (key == GLFW_KEY_A && modifiers == SYSTEM_COMMAND_MOD) { |
| 460 | mCursorPos = (int) mValueTemp.length(); |
| 461 | mSelectionPos = 0; |
| 462 | } else if (key == GLFW_KEY_X && modifiers == SYSTEM_COMMAND_MOD) { |
| 463 | copySelection(); |
| 464 | deleteSelection(); |
| 465 | } else if (key == GLFW_KEY_C && modifiers == SYSTEM_COMMAND_MOD) { |
| 466 | copySelection(); |
| 467 | } else if (key == GLFW_KEY_V && modifiers == SYSTEM_COMMAND_MOD) { |
| 468 | deleteSelection(); |
| 469 | pasteFromClipboard(); |
| 470 | } |
| 471 | |
| 472 | mValidFormat = |
| 473 | (mValueTemp == "" ) || checkFormat(mValueTemp, mFormat); |
| 474 | } |
| 475 | |
| 476 | return true; |
| 477 | } |
| 478 | |
| 479 | return false; |
| 480 | } |
| 481 | |
| 482 | bool TextBox::keyboardCharacterEvent(unsigned int codepoint) { |
| 483 | if (mEditable && focused()) { |
| 484 | std::ostringstream convert; |
| 485 | convert << (char) codepoint; |
| 486 | |
| 487 | deleteSelection(); |
| 488 | mValueTemp.insert(mCursorPos, convert.str()); |
| 489 | mCursorPos++; |
| 490 | |
| 491 | mValidFormat = (mValueTemp == "" ) || checkFormat(mValueTemp, mFormat); |
| 492 | |
| 493 | return true; |
| 494 | } |
| 495 | |
| 496 | return false; |
| 497 | } |
| 498 | |
| 499 | bool TextBox::checkFormat(const std::string &input, const std::string &format) { |
| 500 | if (format.empty()) |
| 501 | return true; |
| 502 | try { |
| 503 | std::regex regex(format); |
| 504 | return regex_match(input, regex); |
| 505 | } catch (const std::regex_error &) { |
| 506 | #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 9) |
| 507 | std::cerr << "Warning: cannot validate text field due to lacking regular expression support. please compile with GCC >= 4.9" << std::endl; |
| 508 | return true; |
| 509 | #else |
| 510 | throw; |
| 511 | #endif |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | bool TextBox::copySelection() { |
| 516 | if (mSelectionPos > -1) { |
| 517 | Screen *sc = dynamic_cast<Screen *>(this->window()->parent()); |
| 518 | if (!sc) |
| 519 | return false; |
| 520 | |
| 521 | int begin = mCursorPos; |
| 522 | int end = mSelectionPos; |
| 523 | |
| 524 | if (begin > end) |
| 525 | std::swap(begin, end); |
| 526 | |
| 527 | glfwSetClipboardString(sc->glfwWindow(), |
| 528 | mValueTemp.substr(begin, end).c_str()); |
| 529 | return true; |
| 530 | } |
| 531 | |
| 532 | return false; |
| 533 | } |
| 534 | |
| 535 | void TextBox::pasteFromClipboard() { |
| 536 | Screen *sc = dynamic_cast<Screen *>(this->window()->parent()); |
| 537 | if (!sc) |
| 538 | return; |
| 539 | const char* cbstr = glfwGetClipboardString(sc->glfwWindow()); |
| 540 | if (cbstr) |
| 541 | mValueTemp.insert(mCursorPos, std::string(cbstr)); |
| 542 | } |
| 543 | |
| 544 | bool TextBox::deleteSelection() { |
| 545 | if (mSelectionPos > -1) { |
| 546 | int begin = mCursorPos; |
| 547 | int end = mSelectionPos; |
| 548 | |
| 549 | if (begin > end) |
| 550 | std::swap(begin, end); |
| 551 | |
| 552 | if (begin == end - 1) |
| 553 | mValueTemp.erase(mValueTemp.begin() + begin); |
| 554 | else |
| 555 | mValueTemp.erase(mValueTemp.begin() + begin, |
| 556 | mValueTemp.begin() + end); |
| 557 | |
| 558 | mCursorPos = begin; |
| 559 | mSelectionPos = -1; |
| 560 | return true; |
| 561 | } |
| 562 | |
| 563 | return false; |
| 564 | } |
| 565 | |
| 566 | void TextBox::updateCursor(NVGcontext *, float lastx, |
| 567 | const NVGglyphPosition *glyphs, int size) { |
| 568 | // handle mouse cursor events |
| 569 | if (mMouseDownPos.x() != -1) { |
| 570 | if (mMouseDownModifier == GLFW_MOD_SHIFT) { |
| 571 | if (mSelectionPos == -1) |
| 572 | mSelectionPos = mCursorPos; |
| 573 | } else |
| 574 | mSelectionPos = -1; |
| 575 | |
| 576 | mCursorPos = |
| 577 | position2CursorIndex(mMouseDownPos.x(), lastx, glyphs, size); |
| 578 | |
| 579 | mMouseDownPos = Vector2i(-1, -1); |
| 580 | } else if (mMouseDragPos.x() != -1) { |
| 581 | if (mSelectionPos == -1) |
| 582 | mSelectionPos = mCursorPos; |
| 583 | |
| 584 | mCursorPos = |
| 585 | position2CursorIndex(mMouseDragPos.x(), lastx, glyphs, size); |
| 586 | } else { |
| 587 | // set cursor to last character |
| 588 | if (mCursorPos == -2) |
| 589 | mCursorPos = size; |
| 590 | } |
| 591 | |
| 592 | if (mCursorPos == mSelectionPos) |
| 593 | mSelectionPos = -1; |
| 594 | } |
| 595 | |
| 596 | float TextBox::cursorIndex2Position(int index, float lastx, |
| 597 | const NVGglyphPosition *glyphs, int size) { |
| 598 | float pos = 0; |
| 599 | if (index == size) |
| 600 | pos = lastx; // last character |
| 601 | else |
| 602 | pos = glyphs[index].x; |
| 603 | |
| 604 | return pos; |
| 605 | } |
| 606 | |
| 607 | int TextBox::position2CursorIndex(float posx, float lastx, |
| 608 | const NVGglyphPosition *glyphs, int size) { |
| 609 | int mCursorId = 0; |
| 610 | float caretx = glyphs[mCursorId].x; |
| 611 | for (int j = 1; j < size; j++) { |
| 612 | if (std::abs(caretx - posx) > std::abs(glyphs[j].x - posx)) { |
| 613 | mCursorId = j; |
| 614 | caretx = glyphs[mCursorId].x; |
| 615 | } |
| 616 | } |
| 617 | if (std::abs(caretx - posx) > std::abs(lastx - posx)) |
| 618 | mCursorId = size; |
| 619 | |
| 620 | return mCursorId; |
| 621 | } |
| 622 | |
| 623 | TextBox::SpinArea TextBox::spinArea(const Vector2i & pos) { |
| 624 | if (0 <= pos.x() - mPos.x() && pos.x() - mPos.x() < 14.f) { /* on scrolling arrows */ |
| 625 | if (mSize.y() >= pos.y() - mPos.y() && pos.y() - mPos.y() <= mSize.y() / 2.f) { /* top part */ |
| 626 | return SpinArea::Top; |
| 627 | } else if (0.f <= pos.y() - mPos.y() && pos.y() - mPos.y() > mSize.y() / 2.f) { /* bottom part */ |
| 628 | return SpinArea::Bottom; |
| 629 | } |
| 630 | } |
| 631 | return SpinArea::None; |
| 632 | } |
| 633 | |
| 634 | void TextBox::save(Serializer &s) const { |
| 635 | Widget::save(s); |
| 636 | s.set("editable" , mEditable); |
| 637 | s.set("spinnable" , mSpinnable); |
| 638 | s.set("committed" , mCommitted); |
| 639 | s.set("value" , mValue); |
| 640 | s.set("defaultValue" , mDefaultValue); |
| 641 | s.set("alignment" , (int) mAlignment); |
| 642 | s.set("units" , mUnits); |
| 643 | s.set("format" , mFormat); |
| 644 | s.set("unitsImage" , mUnitsImage); |
| 645 | s.set("validFormat" , mValidFormat); |
| 646 | s.set("valueTemp" , mValueTemp); |
| 647 | s.set("cursorPos" , mCursorPos); |
| 648 | s.set("selectionPos" , mSelectionPos); |
| 649 | } |
| 650 | |
| 651 | bool TextBox::load(Serializer &s) { |
| 652 | if (!Widget::load(s)) return false; |
| 653 | if (!s.get("editable" , mEditable)) return false; |
| 654 | if (!s.get("spinnable" , mSpinnable)) return false; |
| 655 | if (!s.get("committed" , mCommitted)) return false; |
| 656 | if (!s.get("value" , mValue)) return false; |
| 657 | if (!s.get("defaultValue" , mDefaultValue)) return false; |
| 658 | if (!s.get("alignment" , mAlignment)) return false; |
| 659 | if (!s.get("units" , mUnits)) return false; |
| 660 | if (!s.get("format" , mFormat)) return false; |
| 661 | if (!s.get("unitsImage" , mUnitsImage)) return false; |
| 662 | if (!s.get("validFormat" , mValidFormat)) return false; |
| 663 | if (!s.get("valueTemp" , mValueTemp)) return false; |
| 664 | if (!s.get("cursorPos" , mCursorPos)) return false; |
| 665 | if (!s.get("selectionPos" , mSelectionPos)) return false; |
| 666 | mMousePos = mMouseDownPos = mMouseDragPos = Vector2i::Constant(-1); |
| 667 | mMouseDownModifier = mTextOffset = 0; |
| 668 | return true; |
| 669 | } |
| 670 | |
| 671 | NAMESPACE_END(nanogui) |
| 672 | |