diff options
author | Jacek Antonelli | 2008-08-15 23:44:46 -0500 |
---|---|---|
committer | Jacek Antonelli | 2008-08-15 23:44:46 -0500 |
commit | 38d6d37f2d982fa959e9e8a4a3f7e1ccfad7b5d4 (patch) | |
tree | adca584755d22ca041a2dbfc35d4eca01f70b32c /linden/indra/llui/lltexteditor.cpp | |
parent | README.txt (diff) | |
download | meta-impy-38d6d37f2d982fa959e9e8a4a3f7e1ccfad7b5d4.zip meta-impy-38d6d37f2d982fa959e9e8a4a3f7e1ccfad7b5d4.tar.gz meta-impy-38d6d37f2d982fa959e9e8a4a3f7e1ccfad7b5d4.tar.bz2 meta-impy-38d6d37f2d982fa959e9e8a4a3f7e1ccfad7b5d4.tar.xz |
Second Life viewer sources 1.13.2.12
Diffstat (limited to '')
-rw-r--r-- | linden/indra/llui/lltexteditor.cpp | 4163 |
1 files changed, 4163 insertions, 0 deletions
diff --git a/linden/indra/llui/lltexteditor.cpp b/linden/indra/llui/lltexteditor.cpp new file mode 100644 index 0000000..b0f8b5a --- /dev/null +++ b/linden/indra/llui/lltexteditor.cpp | |||
@@ -0,0 +1,4163 @@ | |||
1 | /** | ||
2 | * @file lltexteditor.cpp | ||
3 | * @brief LLTextEditor base class | ||
4 | * | ||
5 | * Copyright (c) 2001-2007, Linden Research, Inc. | ||
6 | * | ||
7 | * The source code in this file ("Source Code") is provided by Linden Lab | ||
8 | * to you under the terms of the GNU General Public License, version 2.0 | ||
9 | * ("GPL"), unless you have obtained a separate licensing agreement | ||
10 | * ("Other License"), formally executed by you and Linden Lab. Terms of | ||
11 | * the GPL can be found in doc/GPL-license.txt in this distribution, or | ||
12 | * online at http://secondlife.com/developers/opensource/gplv2 | ||
13 | * | ||
14 | * There are special exceptions to the terms and conditions of the GPL as | ||
15 | * it is applied to this Source Code. View the full text of the exception | ||
16 | * in the file doc/FLOSS-exception.txt in this software distribution, or | ||
17 | * online at http://secondlife.com/developers/opensource/flossexception | ||
18 | * | ||
19 | * By copying, modifying or distributing this software, you acknowledge | ||
20 | * that you have read and understood your obligations described above, | ||
21 | * and agree to abide by those obligations. | ||
22 | * | ||
23 | * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO | ||
24 | * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, | ||
25 | * COMPLETENESS OR PERFORMANCE. | ||
26 | */ | ||
27 | |||
28 | // Text editor widget to let users enter a a multi-line ASCII document. | ||
29 | |||
30 | #include "linden_common.h" | ||
31 | |||
32 | #include "lltexteditor.h" | ||
33 | |||
34 | #include "llfontgl.h" | ||
35 | #include "llgl.h" | ||
36 | #include "llui.h" | ||
37 | #include "lluictrlfactory.h" | ||
38 | #include "llrect.h" | ||
39 | #include "llfocusmgr.h" | ||
40 | #include "sound_ids.h" | ||
41 | #include "lltimer.h" | ||
42 | #include "llmath.h" | ||
43 | |||
44 | #include "audioengine.h" | ||
45 | #include "llclipboard.h" | ||
46 | #include "llscrollbar.h" | ||
47 | #include "llstl.h" | ||
48 | #include "llkeyboard.h" | ||
49 | #include "llkeywords.h" | ||
50 | #include "llundo.h" | ||
51 | #include "llviewborder.h" | ||
52 | #include "llcontrol.h" | ||
53 | #include "llimagegl.h" | ||
54 | #include "llwindow.h" | ||
55 | #include "llglheaders.h" | ||
56 | #include <queue> | ||
57 | |||
58 | // | ||
59 | // Globals | ||
60 | // | ||
61 | |||
62 | BOOL gDebugTextEditorTips = FALSE; | ||
63 | |||
64 | // | ||
65 | // Constants | ||
66 | // | ||
67 | |||
68 | const S32 UI_TEXTEDITOR_BUFFER_BLOCK_SIZE = 512; | ||
69 | |||
70 | const S32 UI_TEXTEDITOR_BORDER = 1; | ||
71 | const S32 UI_TEXTEDITOR_H_PAD = 4; | ||
72 | const S32 UI_TEXTEDITOR_V_PAD_TOP = 4; | ||
73 | const F32 CURSOR_FLASH_DELAY = 1.0f; // in seconds | ||
74 | const S32 CURSOR_THICKNESS = 2; | ||
75 | const S32 SPACES_PER_TAB = 4; | ||
76 | |||
77 | LLColor4 LLTextEditor::mLinkColor = LLColor4::blue; | ||
78 | void (* LLTextEditor::mURLcallback)(const char*) = NULL; | ||
79 | BOOL (* LLTextEditor::mSecondlifeURLcallback)(LLString) = NULL; | ||
80 | |||
81 | /////////////////////////////////////////////////////////////////// | ||
82 | //virtuals | ||
83 | BOOL LLTextCmd::canExtend(S32 pos) | ||
84 | { | ||
85 | return FALSE; | ||
86 | } | ||
87 | |||
88 | void LLTextCmd::blockExtensions() | ||
89 | { | ||
90 | } | ||
91 | |||
92 | BOOL LLTextCmd::extendAndExecute( LLTextEditor* editor, S32 pos, llwchar c, S32* delta ) | ||
93 | { | ||
94 | llassert(0); | ||
95 | return 0; | ||
96 | } | ||
97 | |||
98 | BOOL LLTextCmd::hasExtCharValue( llwchar value ) | ||
99 | { | ||
100 | return FALSE; | ||
101 | } | ||
102 | |||
103 | // Utility funcs | ||
104 | S32 LLTextCmd::insert(LLTextEditor* editor, S32 pos, const LLWString &utf8str) | ||
105 | { | ||
106 | return editor->insertStringNoUndo( pos, utf8str ); | ||
107 | } | ||
108 | S32 LLTextCmd::remove(LLTextEditor* editor, S32 pos, S32 length) | ||
109 | { | ||
110 | return editor->removeStringNoUndo( pos, length ); | ||
111 | } | ||
112 | S32 LLTextCmd::overwrite(LLTextEditor* editor, S32 pos, llwchar wc) | ||
113 | { | ||
114 | return editor->overwriteCharNoUndo(pos, wc); | ||
115 | } | ||
116 | |||
117 | /////////////////////////////////////////////////////////////////// | ||
118 | |||
119 | class LLTextCmdInsert : public LLTextCmd | ||
120 | { | ||
121 | public: | ||
122 | LLTextCmdInsert(S32 pos, BOOL group_with_next, const LLWString &ws) | ||
123 | : LLTextCmd(pos, group_with_next), mString(ws) | ||
124 | { | ||
125 | } | ||
126 | virtual BOOL execute( LLTextEditor* editor, S32* delta ) | ||
127 | { | ||
128 | *delta = insert(editor, mPos, mString ); | ||
129 | LLWString::truncate(mString, *delta); | ||
130 | //mString = wstring_truncate(mString, *delta); | ||
131 | return (*delta != 0); | ||
132 | } | ||
133 | virtual S32 undo( LLTextEditor* editor ) | ||
134 | { | ||
135 | remove(editor, mPos, mString.length() ); | ||
136 | return mPos; | ||
137 | } | ||
138 | virtual S32 redo( LLTextEditor* editor ) | ||
139 | { | ||
140 | insert(editor, mPos, mString ); | ||
141 | return mPos + mString.length(); | ||
142 | } | ||
143 | |||
144 | private: | ||
145 | LLWString mString; | ||
146 | }; | ||
147 | |||
148 | /////////////////////////////////////////////////////////////////// | ||
149 | |||
150 | class LLTextCmdAddChar : public LLTextCmd | ||
151 | { | ||
152 | public: | ||
153 | LLTextCmdAddChar( S32 pos, BOOL group_with_next, llwchar wc) | ||
154 | : LLTextCmd(pos, group_with_next), mString(1, wc), mBlockExtensions(FALSE) | ||
155 | { | ||
156 | } | ||
157 | virtual void blockExtensions() | ||
158 | { | ||
159 | mBlockExtensions = TRUE; | ||
160 | } | ||
161 | virtual BOOL canExtend(S32 pos) | ||
162 | { | ||
163 | return !mBlockExtensions && (pos == mPos + (S32)mString.length()); | ||
164 | } | ||
165 | virtual BOOL execute( LLTextEditor* editor, S32* delta ) | ||
166 | { | ||
167 | *delta = insert(editor, mPos, mString); | ||
168 | LLWString::truncate(mString, *delta); | ||
169 | //mString = wstring_truncate(mString, *delta); | ||
170 | return (*delta != 0); | ||
171 | } | ||
172 | virtual BOOL extendAndExecute( LLTextEditor* editor, S32 pos, llwchar wc, S32* delta ) | ||
173 | { | ||
174 | LLWString ws; | ||
175 | ws += wc; | ||
176 | |||
177 | *delta = insert(editor, pos, ws); | ||
178 | if( *delta > 0 ) | ||
179 | { | ||
180 | mString += wc; | ||
181 | } | ||
182 | return (*delta != 0); | ||
183 | } | ||
184 | virtual S32 undo( LLTextEditor* editor ) | ||
185 | { | ||
186 | remove(editor, mPos, mString.length() ); | ||
187 | return mPos; | ||
188 | } | ||
189 | virtual S32 redo( LLTextEditor* editor ) | ||
190 | { | ||
191 | insert(editor, mPos, mString ); | ||
192 | return mPos + mString.length(); | ||
193 | } | ||
194 | |||
195 | private: | ||
196 | LLWString mString; | ||
197 | BOOL mBlockExtensions; | ||
198 | |||
199 | }; | ||
200 | |||
201 | /////////////////////////////////////////////////////////////////// | ||
202 | |||
203 | class LLTextCmdOverwriteChar : public LLTextCmd | ||
204 | { | ||
205 | public: | ||
206 | LLTextCmdOverwriteChar( S32 pos, BOOL group_with_next, llwchar wc) | ||
207 | : LLTextCmd(pos, group_with_next), mChar(wc), mOldChar(0) {} | ||
208 | |||
209 | virtual BOOL execute( LLTextEditor* editor, S32* delta ) | ||
210 | { | ||
211 | mOldChar = editor->getWChar(mPos); | ||
212 | overwrite(editor, mPos, mChar); | ||
213 | *delta = 0; | ||
214 | return TRUE; | ||
215 | } | ||
216 | virtual S32 undo( LLTextEditor* editor ) | ||
217 | { | ||
218 | overwrite(editor, mPos, mOldChar); | ||
219 | return mPos; | ||
220 | } | ||
221 | virtual S32 redo( LLTextEditor* editor ) | ||
222 | { | ||
223 | overwrite(editor, mPos, mChar); | ||
224 | return mPos+1; | ||
225 | } | ||
226 | |||
227 | private: | ||
228 | llwchar mChar; | ||
229 | llwchar mOldChar; | ||
230 | }; | ||
231 | |||
232 | /////////////////////////////////////////////////////////////////// | ||
233 | |||
234 | class LLTextCmdRemove : public LLTextCmd | ||
235 | { | ||
236 | public: | ||
237 | LLTextCmdRemove( S32 pos, BOOL group_with_next, S32 len ) : | ||
238 | LLTextCmd(pos, group_with_next), mLen(len) | ||
239 | { | ||
240 | } | ||
241 | virtual BOOL execute( LLTextEditor* editor, S32* delta ) | ||
242 | { | ||
243 | mString = editor->getWSubString(mPos, mLen); | ||
244 | *delta = remove(editor, mPos, mLen ); | ||
245 | return (*delta != 0); | ||
246 | } | ||
247 | virtual S32 undo( LLTextEditor* editor ) | ||
248 | { | ||
249 | insert(editor, mPos, mString ); | ||
250 | return mPos + mString.length(); | ||
251 | } | ||
252 | virtual S32 redo( LLTextEditor* editor ) | ||
253 | { | ||
254 | remove(editor, mPos, mLen ); | ||
255 | return mPos; | ||
256 | } | ||
257 | private: | ||
258 | LLWString mString; | ||
259 | S32 mLen; | ||
260 | }; | ||
261 | |||
262 | /////////////////////////////////////////////////////////////////// | ||
263 | |||
264 | // | ||
265 | // Member functions | ||
266 | // | ||
267 | |||
268 | LLTextEditor::LLTextEditor( | ||
269 | const LLString& name, | ||
270 | const LLRect& rect, | ||
271 | S32 max_length, | ||
272 | const LLString &default_text, | ||
273 | const LLFontGL* font, | ||
274 | BOOL allow_embedded_items) | ||
275 | : | ||
276 | LLUICtrl( name, rect, TRUE, NULL, NULL, FOLLOWS_TOP | FOLLOWS_LEFT ), | ||
277 | mTextIsUpToDate(TRUE), | ||
278 | mMaxTextLength( max_length ), | ||
279 | mBaseDocIsPristine(TRUE), | ||
280 | mPristineCmd( NULL ), | ||
281 | mLastCmd( NULL ), | ||
282 | mCursorPos( 0 ), | ||
283 | mIsSelecting( FALSE ), | ||
284 | mSelectionStart( 0 ), | ||
285 | mSelectionEnd( 0 ), | ||
286 | mOnScrollEndCallback( NULL ), | ||
287 | mOnScrollEndData( NULL ), | ||
288 | mCursorColor( LLUI::sColorsGroup->getColor( "TextCursorColor" ) ), | ||
289 | mFgColor( LLUI::sColorsGroup->getColor( "TextFgColor" ) ), | ||
290 | mReadOnlyFgColor( LLUI::sColorsGroup->getColor( "TextFgReadOnlyColor" ) ), | ||
291 | mWriteableBgColor( LLUI::sColorsGroup->getColor( "TextBgWriteableColor" ) ), | ||
292 | mReadOnlyBgColor( LLUI::sColorsGroup->getColor( "TextBgReadOnlyColor" ) ), | ||
293 | mFocusBgColor( LLUI::sColorsGroup->getColor( "TextBgFocusColor" ) ), | ||
294 | mReadOnly(FALSE), | ||
295 | mWordWrap( FALSE ), | ||
296 | mTabToNextField( TRUE ), | ||
297 | mCommitOnFocusLost( FALSE ), | ||
298 | mTakesFocus( TRUE ), | ||
299 | mHideScrollbarForShortDocs( FALSE ), | ||
300 | mTakesNonScrollClicks( TRUE ), | ||
301 | mAllowEmbeddedItems( allow_embedded_items ), | ||
302 | mAcceptCallingCardNames(FALSE), | ||
303 | mHandleEditKeysDirectly( FALSE ), | ||
304 | mMouseDownX(0), | ||
305 | mMouseDownY(0), | ||
306 | mLastSelectionX(-1), | ||
307 | mLastSelectionY(-1) | ||
308 | { | ||
309 | mSourceID.generate(); | ||
310 | |||
311 | if (font) | ||
312 | { | ||
313 | mGLFont = font; | ||
314 | } | ||
315 | else | ||
316 | { | ||
317 | mGLFont = LLFontGL::sSansSerif; | ||
318 | } | ||
319 | |||
320 | updateTextRect(); | ||
321 | |||
322 | S32 line_height = llround( mGLFont->getLineHeight() ); | ||
323 | S32 page_size = mTextRect.getHeight() / line_height; | ||
324 | |||
325 | // Init the scrollbar | ||
326 | LLRect scroll_rect; | ||
327 | scroll_rect.setOriginAndSize( | ||
328 | mRect.getWidth() - UI_TEXTEDITOR_BORDER - SCROLLBAR_SIZE, | ||
329 | UI_TEXTEDITOR_BORDER, | ||
330 | SCROLLBAR_SIZE, | ||
331 | mRect.getHeight() - 2 * UI_TEXTEDITOR_BORDER ); | ||
332 | S32 lines_in_doc = getLineCount(); | ||
333 | mScrollbar = new LLScrollbar( "Scrollbar", scroll_rect, | ||
334 | LLScrollbar::VERTICAL, | ||
335 | lines_in_doc, | ||
336 | 0, | ||
337 | page_size, | ||
338 | NULL, this ); | ||
339 | mScrollbar->setFollowsRight(); | ||
340 | mScrollbar->setFollowsTop(); | ||
341 | mScrollbar->setFollowsBottom(); | ||
342 | mScrollbar->setEnabled( TRUE ); | ||
343 | mScrollbar->setVisible( TRUE ); | ||
344 | mScrollbar->setOnScrollEndCallback(mOnScrollEndCallback, mOnScrollEndData); | ||
345 | addChild(mScrollbar); | ||
346 | |||
347 | mBorder = new LLViewBorder( "text ed border", LLRect(0, mRect.getHeight(), mRect.getWidth(), 0), LLViewBorder::BEVEL_IN, LLViewBorder::STYLE_LINE, UI_TEXTEDITOR_BORDER ); | ||
348 | addChild( mBorder ); | ||
349 | |||
350 | setText(default_text); | ||
351 | |||
352 | mParseHTML=FALSE; | ||
353 | mHTML=""; | ||
354 | } | ||
355 | |||
356 | |||
357 | LLTextEditor::~LLTextEditor() | ||
358 | { | ||
359 | gFocusMgr.releaseFocusIfNeeded( this ); // calls onCommit() | ||
360 | |||
361 | // Route menu back to the default | ||
362 | if( gEditMenuHandler == this ) | ||
363 | { | ||
364 | gEditMenuHandler = NULL; | ||
365 | } | ||
366 | |||
367 | // Scrollbar is deleted by LLView | ||
368 | mHoverSegment = NULL; | ||
369 | std::for_each(mSegments.begin(), mSegments.end(), DeletePointer()); | ||
370 | |||
371 | std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer()); | ||
372 | } | ||
373 | |||
374 | //virtual | ||
375 | LLString LLTextEditor::getWidgetTag() const | ||
376 | { | ||
377 | return LL_TEXT_EDITOR_TAG; | ||
378 | } | ||
379 | |||
380 | void LLTextEditor::setTrackColor( const LLColor4& color ) | ||
381 | { | ||
382 | mScrollbar->setTrackColor(color); | ||
383 | } | ||
384 | |||
385 | void LLTextEditor::setThumbColor( const LLColor4& color ) | ||
386 | { | ||
387 | mScrollbar->setThumbColor(color); | ||
388 | } | ||
389 | |||
390 | void LLTextEditor::setHighlightColor( const LLColor4& color ) | ||
391 | { | ||
392 | mScrollbar->setHighlightColor(color); | ||
393 | } | ||
394 | |||
395 | void LLTextEditor::setShadowColor( const LLColor4& color ) | ||
396 | { | ||
397 | mScrollbar->setShadowColor(color); | ||
398 | } | ||
399 | |||
400 | void LLTextEditor::updateLineStartList(S32 startpos) | ||
401 | { | ||
402 | updateSegments(); | ||
403 | |||
404 | bindEmbeddedChars( mGLFont ); | ||
405 | |||
406 | S32 seg_num = mSegments.size(); | ||
407 | S32 seg_idx = 0; | ||
408 | S32 seg_offset = 0; | ||
409 | |||
410 | if (!mLineStartList.empty()) | ||
411 | { | ||
412 | getSegmentAndOffset(startpos, &seg_idx, &seg_offset); | ||
413 | line_info t(seg_idx, seg_offset); | ||
414 | line_list_t::iterator iter = std::upper_bound(mLineStartList.begin(), mLineStartList.end(), t, line_info_compare()); | ||
415 | if (iter != mLineStartList.begin()) --iter; | ||
416 | seg_idx = iter->mSegment; | ||
417 | seg_offset = iter->mOffset; | ||
418 | mLineStartList.erase(iter, mLineStartList.end()); | ||
419 | } | ||
420 | |||
421 | while( seg_idx < seg_num ) | ||
422 | { | ||
423 | mLineStartList.push_back(line_info(seg_idx,seg_offset)); | ||
424 | BOOL line_ended = FALSE; | ||
425 | S32 line_width = 0; | ||
426 | while(!line_ended && seg_idx < seg_num) | ||
427 | { | ||
428 | LLTextSegment* segment = mSegments[seg_idx]; | ||
429 | S32 start_idx = segment->getStart() + seg_offset; | ||
430 | S32 end_idx = start_idx; | ||
431 | while (end_idx < segment->getEnd() && mWText[end_idx] != '\n') | ||
432 | { | ||
433 | end_idx++; | ||
434 | } | ||
435 | if (start_idx == end_idx) | ||
436 | { | ||
437 | if (end_idx >= segment->getEnd()) | ||
438 | { | ||
439 | // empty segment | ||
440 | seg_idx++; | ||
441 | seg_offset = 0; | ||
442 | } | ||
443 | else | ||
444 | { | ||
445 | // empty line | ||
446 | line_ended = TRUE; | ||
447 | seg_offset++; | ||
448 | } | ||
449 | } | ||
450 | else | ||
451 | { | ||
452 | const llwchar* str = mWText.c_str() + start_idx; | ||
453 | S32 drawn = mGLFont->maxDrawableChars(str, (F32)mTextRect.getWidth() - line_width, | ||
454 | end_idx - start_idx, mWordWrap, mAllowEmbeddedItems ); | ||
455 | if( 0 == drawn && line_width == 0) | ||
456 | { | ||
457 | // If at the beginning of a line, draw at least one character, even if it doesn't all fit. | ||
458 | drawn = 1; | ||
459 | } | ||
460 | seg_offset += drawn; | ||
461 | line_width += mGLFont->getWidth(str, 0, drawn, mAllowEmbeddedItems); | ||
462 | end_idx = segment->getStart() + seg_offset; | ||
463 | if (end_idx < segment->getEnd()) | ||
464 | { | ||
465 | line_ended = TRUE; | ||
466 | if (mWText[end_idx] == '\n') | ||
467 | { | ||
468 | seg_offset++; // skip newline | ||
469 | } | ||
470 | } | ||
471 | else | ||
472 | { | ||
473 | // finished with segment | ||
474 | seg_idx++; | ||
475 | seg_offset = 0; | ||
476 | } | ||
477 | } | ||
478 | } | ||
479 | } | ||
480 | |||
481 | unbindEmbeddedChars(mGLFont); | ||
482 | |||
483 | mScrollbar->setDocSize( getLineCount() ); | ||
484 | |||
485 | if (mHideScrollbarForShortDocs) | ||
486 | { | ||
487 | BOOL short_doc = (mScrollbar->getDocSize() <= mScrollbar->getPageSize()); | ||
488 | mScrollbar->setVisible(!short_doc); | ||
489 | } | ||
490 | |||
491 | } | ||
492 | |||
493 | //////////////////////////////////////////////////////////// | ||
494 | // LLTextEditor | ||
495 | // Public methods | ||
496 | |||
497 | //static | ||
498 | BOOL LLTextEditor::isPartOfWord(llwchar c) { return (c == '_') || isalnum(c); } | ||
499 | |||
500 | |||
501 | |||
502 | void LLTextEditor::truncate() | ||
503 | { | ||
504 | if (mWText.size() > (size_t)mMaxTextLength) | ||
505 | { | ||
506 | LLWString::truncate(mWText, mMaxTextLength); | ||
507 | mTextIsUpToDate = FALSE; | ||
508 | } | ||
509 | } | ||
510 | |||
511 | void LLTextEditor::setText(const LLString &utf8str) | ||
512 | { | ||
513 | mUTF8Text = utf8str; | ||
514 | mWText = utf8str_to_wstring(utf8str); | ||
515 | mTextIsUpToDate = TRUE; | ||
516 | |||
517 | truncate(); | ||
518 | blockUndo(); | ||
519 | |||
520 | setCursorPos(0); | ||
521 | deselect(); | ||
522 | |||
523 | updateLineStartList(); | ||
524 | updateScrollFromCursor(); | ||
525 | } | ||
526 | |||
527 | void LLTextEditor::setWText(const LLWString &wtext) | ||
528 | { | ||
529 | mWText = wtext; | ||
530 | mUTF8Text.clear(); | ||
531 | mTextIsUpToDate = FALSE; | ||
532 | |||
533 | truncate(); | ||
534 | blockUndo(); | ||
535 | |||
536 | setCursorPos(0); | ||
537 | deselect(); | ||
538 | |||
539 | updateLineStartList(); | ||
540 | updateScrollFromCursor(); | ||
541 | } | ||
542 | |||
543 | void LLTextEditor::setValue(const LLSD& value) | ||
544 | { | ||
545 | setText(value.asString()); | ||
546 | } | ||
547 | |||
548 | const LLString& LLTextEditor::getText() const | ||
549 | { | ||
550 | if (!mTextIsUpToDate) | ||
551 | { | ||
552 | if (mAllowEmbeddedItems) | ||
553 | { | ||
554 | llwarns << "getText() called on text with embedded items (not supported)" << llendl; | ||
555 | } | ||
556 | mUTF8Text = wstring_to_utf8str(mWText); | ||
557 | mTextIsUpToDate = TRUE; | ||
558 | } | ||
559 | return mUTF8Text; | ||
560 | } | ||
561 | |||
562 | LLSD LLTextEditor::getValue() const | ||
563 | { | ||
564 | return LLSD(getText()); | ||
565 | } | ||
566 | |||
567 | void LLTextEditor::setWordWrap(BOOL b) | ||
568 | { | ||
569 | mWordWrap = b; | ||
570 | |||
571 | setCursorPos(0); | ||
572 | deselect(); | ||
573 | |||
574 | updateLineStartList(); | ||
575 | updateScrollFromCursor(); | ||
576 | } | ||
577 | |||
578 | |||
579 | void LLTextEditor::setBorderVisible(BOOL b) | ||
580 | { | ||
581 | mBorder->setVisible(b); | ||
582 | } | ||
583 | |||
584 | |||
585 | |||
586 | void LLTextEditor::setHideScrollbarForShortDocs(BOOL b) | ||
587 | { | ||
588 | mHideScrollbarForShortDocs = b; | ||
589 | |||
590 | if (mHideScrollbarForShortDocs) | ||
591 | { | ||
592 | BOOL short_doc = (mScrollbar->getDocSize() <= mScrollbar->getPageSize()); | ||
593 | mScrollbar->setVisible(!short_doc); | ||
594 | } | ||
595 | } | ||
596 | |||
597 | void LLTextEditor::selectNext(const LLString& search_text_in, BOOL case_insensitive, BOOL wrap) | ||
598 | { | ||
599 | if (search_text_in.empty()) | ||
600 | { | ||
601 | return; | ||
602 | } | ||
603 | |||
604 | LLWString text = getWText(); | ||
605 | LLWString search_text = utf8str_to_wstring(search_text_in); | ||
606 | if (case_insensitive) | ||
607 | { | ||
608 | LLWString::toLower(text); | ||
609 | LLWString::toLower(search_text); | ||
610 | } | ||
611 | |||
612 | if (mIsSelecting) | ||
613 | { | ||
614 | LLWString selected_text = text.substr(mSelectionEnd, mSelectionStart - mSelectionEnd); | ||
615 | |||
616 | if (selected_text == search_text) | ||
617 | { | ||
618 | // We already have this word selected, we are searching for the next. | ||
619 | mCursorPos += search_text.size(); | ||
620 | } | ||
621 | } | ||
622 | |||
623 | S32 loc = text.find(search_text,mCursorPos); | ||
624 | |||
625 | // If Maybe we wrapped, search again | ||
626 | if (wrap && (-1 == loc)) | ||
627 | { | ||
628 | loc = text.find(search_text); | ||
629 | } | ||
630 | |||
631 | // If still -1, then search_text just isn't found. | ||
632 | if (-1 == loc) | ||
633 | { | ||
634 | mIsSelecting = FALSE; | ||
635 | mSelectionEnd = 0; | ||
636 | mSelectionStart = 0; | ||
637 | return; | ||
638 | } | ||
639 | |||
640 | setCursorPos(loc); | ||
641 | |||
642 | mIsSelecting = TRUE; | ||
643 | mSelectionEnd = mCursorPos; | ||
644 | mSelectionStart = llmin((S32)getLength(), (S32)(mCursorPos + search_text.size())); | ||
645 | } | ||
646 | |||
647 | BOOL LLTextEditor::replaceText(const LLString& search_text_in, const LLString& replace_text, | ||
648 | BOOL case_insensitive, BOOL wrap) | ||
649 | { | ||
650 | BOOL replaced = FALSE; | ||
651 | |||
652 | if (search_text_in.empty()) | ||
653 | { | ||
654 | return replaced; | ||
655 | } | ||
656 | |||
657 | LLWString search_text = utf8str_to_wstring(search_text_in); | ||
658 | if (mIsSelecting) | ||
659 | { | ||
660 | LLWString text = getWText(); | ||
661 | LLWString selected_text = text.substr(mSelectionEnd, mSelectionStart - mSelectionEnd); | ||
662 | |||
663 | if (case_insensitive) | ||
664 | { | ||
665 | LLWString::toLower(selected_text); | ||
666 | LLWString::toLower(search_text); | ||
667 | } | ||
668 | |||
669 | if (selected_text == search_text) | ||
670 | { | ||
671 | insertText(replace_text); | ||
672 | replaced = TRUE; | ||
673 | } | ||
674 | } | ||
675 | |||
676 | selectNext(search_text_in, case_insensitive, wrap); | ||
677 | return replaced; | ||
678 | } | ||
679 | |||
680 | void LLTextEditor::replaceTextAll(const LLString& search_text, const LLString& replace_text, BOOL case_insensitive) | ||
681 | { | ||
682 | S32 cur_pos = mScrollbar->getDocPos(); | ||
683 | |||
684 | setCursorPos(0); | ||
685 | selectNext(search_text, case_insensitive, FALSE); | ||
686 | |||
687 | BOOL replaced = TRUE; | ||
688 | while ( replaced ) | ||
689 | { | ||
690 | replaced = replaceText(search_text,replace_text, case_insensitive, FALSE); | ||
691 | } | ||
692 | |||
693 | mScrollbar->setDocPos(cur_pos); | ||
694 | } | ||
695 | |||
696 | void LLTextEditor::setTakesNonScrollClicks(BOOL b) | ||
697 | { | ||
698 | mTakesNonScrollClicks = b; | ||
699 | } | ||
700 | |||
701 | |||
702 | // Picks a new cursor position based on the screen size of text being drawn. | ||
703 | void LLTextEditor::setCursorAtLocalPos( S32 local_x, S32 local_y, BOOL round ) | ||
704 | { | ||
705 | setCursorPos(getCursorPosFromLocalCoord(local_x, local_y, round)); | ||
706 | } | ||
707 | |||
708 | S32 LLTextEditor::prevWordPos(S32 cursorPos) const | ||
709 | { | ||
710 | const LLWString& wtext = mWText; | ||
711 | while( (cursorPos > 0) && (wtext[cursorPos-1] == ' ') ) | ||
712 | { | ||
713 | cursorPos--; | ||
714 | } | ||
715 | while( (cursorPos > 0) && isPartOfWord( wtext[cursorPos-1] ) ) | ||
716 | { | ||
717 | cursorPos--; | ||
718 | } | ||
719 | return cursorPos; | ||
720 | } | ||
721 | |||
722 | S32 LLTextEditor::nextWordPos(S32 cursorPos) const | ||
723 | { | ||
724 | const LLWString& wtext = mWText; | ||
725 | while( (cursorPos < getLength()) && isPartOfWord( wtext[cursorPos+1] ) ) | ||
726 | { | ||
727 | cursorPos++; | ||
728 | } | ||
729 | while( (cursorPos < getLength()) && (wtext[cursorPos+1] == ' ') ) | ||
730 | { | ||
731 | cursorPos++; | ||
732 | } | ||
733 | return cursorPos; | ||
734 | } | ||
735 | |||
736 | S32 LLTextEditor::getLineCount() | ||
737 | { | ||
738 | return mLineStartList.size(); | ||
739 | } | ||
740 | |||
741 | S32 LLTextEditor::getLineStart( S32 line ) | ||
742 | { | ||
743 | S32 num_lines = getLineCount(); | ||
744 | if (num_lines == 0) | ||
745 | { | ||
746 | return 0; | ||
747 | } | ||
748 | line = llclamp(line, 0, num_lines-1); | ||
749 | S32 segidx = mLineStartList[line].mSegment; | ||
750 | S32 segoffset = mLineStartList[line].mOffset; | ||
751 | LLTextSegment* seg = mSegments[segidx]; | ||
752 | S32 res = seg->getStart() + segoffset; | ||
753 | if (res > seg->getEnd()) llerrs << "wtf" << llendl; | ||
754 | return res; | ||
755 | } | ||
756 | |||
757 | // Given an offset into text (pos), find the corresponding line (from the start of the doc) and an offset into the line. | ||
758 | void LLTextEditor::getLineAndOffset( S32 startpos, S32* linep, S32* offsetp ) | ||
759 | { | ||
760 | if (mLineStartList.empty()) | ||
761 | { | ||
762 | *linep = 0; | ||
763 | *offsetp = startpos; | ||
764 | } | ||
765 | else | ||
766 | { | ||
767 | S32 seg_idx, seg_offset; | ||
768 | getSegmentAndOffset( startpos, &seg_idx, &seg_offset ); | ||
769 | |||
770 | line_info tline(seg_idx, seg_offset); | ||
771 | line_list_t::iterator iter = std::upper_bound(mLineStartList.begin(), mLineStartList.end(), tline, line_info_compare()); | ||
772 | if (iter != mLineStartList.begin()) --iter; | ||
773 | *linep = iter - mLineStartList.begin(); | ||
774 | S32 line_start = mSegments[iter->mSegment]->getStart() + iter->mOffset; | ||
775 | *offsetp = startpos - line_start; | ||
776 | } | ||
777 | } | ||
778 | |||
779 | void LLTextEditor::getSegmentAndOffset( S32 startpos, S32* segidxp, S32* offsetp ) | ||
780 | { | ||
781 | if (mSegments.empty()) | ||
782 | { | ||
783 | *segidxp = -1; | ||
784 | *offsetp = startpos; | ||
785 | } | ||
786 | |||
787 | LLTextSegment tseg(startpos); | ||
788 | segment_list_t::iterator seg_iter; | ||
789 | seg_iter = std::upper_bound(mSegments.begin(), mSegments.end(), &tseg, LLTextSegment::compare()); | ||
790 | if (seg_iter != mSegments.begin()) --seg_iter; | ||
791 | *segidxp = seg_iter - mSegments.begin(); | ||
792 | *offsetp = startpos - (*seg_iter)->getStart(); | ||
793 | } | ||
794 | |||
795 | const LLWString& LLTextEditor::getWText() const | ||
796 | { | ||
797 | return mWText; | ||
798 | } | ||
799 | |||
800 | S32 LLTextEditor::getLength() const | ||
801 | { | ||
802 | return mWText.length(); | ||
803 | } | ||
804 | |||
805 | llwchar LLTextEditor::getWChar(S32 pos) | ||
806 | { | ||
807 | return mWText[pos]; | ||
808 | } | ||
809 | |||
810 | LLWString LLTextEditor::getWSubString(S32 pos, S32 len) | ||
811 | { | ||
812 | return mWText.substr(pos, len); | ||
813 | } | ||
814 | |||
815 | S32 LLTextEditor::getCursorPosFromLocalCoord( S32 local_x, S32 local_y, BOOL round ) | ||
816 | { | ||
817 | // If round is true, if the position is on the right half of a character, the cursor | ||
818 | // will be put to its right. If round is false, the cursor will always be put to the | ||
819 | // character's left. | ||
820 | |||
821 | // Figure out which line we're nearest to. | ||
822 | S32 total_lines = getLineCount(); | ||
823 | S32 line_height = llround( mGLFont->getLineHeight() ); | ||
824 | S32 max_visible_lines = mTextRect.getHeight() / line_height; | ||
825 | S32 scroll_lines = mScrollbar->getDocPos(); | ||
826 | S32 visible_lines = llmin( total_lines - scroll_lines, max_visible_lines ); // Lines currently visible | ||
827 | |||
828 | //S32 line = S32( 0.5f + ((mTextRect.mTop - local_y) / mGLFont->getLineHeight()) ); | ||
829 | S32 line = (mTextRect.mTop - 1 - local_y) / line_height; | ||
830 | if (line >= total_lines) | ||
831 | { | ||
832 | return getLength(); // past the end | ||
833 | } | ||
834 | |||
835 | line = llclamp( line, 0, visible_lines ) + scroll_lines; | ||
836 | |||
837 | S32 line_start = getLineStart(line); | ||
838 | S32 next_start = getLineStart(line+1); | ||
839 | S32 line_end = (next_start != line_start) ? next_start - 1 : getLength(); | ||
840 | |||
841 | if(line_start == -1) | ||
842 | { | ||
843 | return 0; | ||
844 | } | ||
845 | else | ||
846 | { | ||
847 | S32 line_len = line_end - line_start; | ||
848 | S32 pos; | ||
849 | |||
850 | if (mAllowEmbeddedItems) | ||
851 | { | ||
852 | // Figure out which character we're nearest to. | ||
853 | bindEmbeddedChars(mGLFont); | ||
854 | pos = mGLFont->charFromPixelOffset(mWText.c_str(), line_start, | ||
855 | (F32)(local_x - mTextRect.mLeft), | ||
856 | (F32)(mTextRect.getWidth()), | ||
857 | line_len, | ||
858 | round, TRUE); | ||
859 | unbindEmbeddedChars(mGLFont); | ||
860 | } | ||
861 | else | ||
862 | { | ||
863 | pos = mGLFont->charFromPixelOffset(mWText.c_str(), line_start, | ||
864 | (F32)(local_x - mTextRect.mLeft), | ||
865 | (F32)mTextRect.getWidth(), | ||
866 | line_len, | ||
867 | round); | ||
868 | } | ||
869 | |||
870 | return line_start + pos; | ||
871 | } | ||
872 | } | ||
873 | |||
874 | void LLTextEditor::setCursor(S32 row, S32 column) | ||
875 | { | ||
876 | const llwchar* doc = mWText.c_str(); | ||
877 | const char CR = 10; | ||
878 | while(row--) | ||
879 | { | ||
880 | while (CR != *doc++); | ||
881 | } | ||
882 | doc += column; | ||
883 | setCursorPos(doc - mWText.c_str()); | ||
884 | updateScrollFromCursor(); | ||
885 | } | ||
886 | |||
887 | void LLTextEditor::setCursorPos(S32 offset) | ||
888 | { | ||
889 | mCursorPos = llclamp(offset, 0, (S32)getLength()); | ||
890 | updateScrollFromCursor(); | ||
891 | } | ||
892 | |||
893 | |||
894 | BOOL LLTextEditor::canDeselect() | ||
895 | { | ||
896 | return hasSelection(); | ||
897 | } | ||
898 | |||
899 | |||
900 | void LLTextEditor::deselect() | ||
901 | { | ||
902 | mSelectionStart = 0; | ||
903 | mSelectionEnd = 0; | ||
904 | mIsSelecting = FALSE; | ||
905 | } | ||
906 | |||
907 | |||
908 | void LLTextEditor::startSelection() | ||
909 | { | ||
910 | if( !mIsSelecting ) | ||
911 | { | ||
912 | mIsSelecting = TRUE; | ||
913 | mSelectionStart = mCursorPos; | ||
914 | mSelectionEnd = mCursorPos; | ||
915 | } | ||
916 | } | ||
917 | |||
918 | void LLTextEditor::endSelection() | ||
919 | { | ||
920 | if( mIsSelecting ) | ||
921 | { | ||
922 | mIsSelecting = FALSE; | ||
923 | mSelectionEnd = mCursorPos; | ||
924 | } | ||
925 | if (mParseHTML && mHTML.length() > 0) | ||
926 | { | ||
927 | //Special handling for slurls | ||
928 | if ( (mSecondlifeURLcallback!=NULL) && !(*mSecondlifeURLcallback)(mHTML) ) | ||
929 | { | ||
930 | if (mURLcallback!=NULL) (*mURLcallback)(mHTML.c_str()); | ||
931 | |||
932 | //load_url(url.c_str()); | ||
933 | } | ||
934 | mHTML=""; | ||
935 | } | ||
936 | } | ||
937 | |||
938 | BOOL LLTextEditor::selectionContainsLineBreaks() | ||
939 | { | ||
940 | if (hasSelection()) | ||
941 | { | ||
942 | S32 left = llmin(mSelectionStart, mSelectionEnd); | ||
943 | S32 right = left + abs(mSelectionStart - mSelectionEnd); | ||
944 | |||
945 | const LLWString &wtext = mWText; | ||
946 | for( S32 i = left; i < right; i++ ) | ||
947 | { | ||
948 | if (wtext[i] == '\n') | ||
949 | { | ||
950 | return TRUE; | ||
951 | } | ||
952 | } | ||
953 | } | ||
954 | return FALSE; | ||
955 | } | ||
956 | |||
957 | |||
958 | S32 LLTextEditor::indentLine( S32 pos, S32 spaces ) | ||
959 | { | ||
960 | // Assumes that pos is at the start of the line | ||
961 | // spaces may be positive (indent) or negative (unindent). | ||
962 | // Returns the actual number of characters added or removed. | ||
963 | |||
964 | llassert(pos >= 0); | ||
965 | llassert(pos <= getLength() ); | ||
966 | |||
967 | S32 delta_spaces = 0; | ||
968 | |||
969 | if (spaces >= 0) | ||
970 | { | ||
971 | // Indent | ||
972 | for(S32 i=0; i < spaces; i++) | ||
973 | { | ||
974 | delta_spaces += addChar(pos, ' '); | ||
975 | } | ||
976 | } | ||
977 | else | ||
978 | { | ||
979 | // Unindent | ||
980 | for(S32 i=0; i < -spaces; i++) | ||
981 | { | ||
982 | const LLWString &wtext = mWText; | ||
983 | if (wtext[pos] == ' ') | ||
984 | { | ||
985 | delta_spaces += remove( pos, 1, FALSE ); | ||
986 | } | ||
987 | } | ||
988 | } | ||
989 | |||
990 | return delta_spaces; | ||
991 | } | ||
992 | |||
993 | void LLTextEditor::indentSelectedLines( S32 spaces ) | ||
994 | { | ||
995 | if( hasSelection() ) | ||
996 | { | ||
997 | const LLWString &text = mWText; | ||
998 | S32 left = llmin( mSelectionStart, mSelectionEnd ); | ||
999 | S32 right = left + abs( mSelectionStart - mSelectionEnd ); | ||
1000 | BOOL cursor_on_right = (mSelectionEnd > mSelectionStart); | ||
1001 | S32 cur = left; | ||
1002 | |||
1003 | // Expand left to start of line | ||
1004 | while( (cur > 0) && (text[cur] != '\n') ) | ||
1005 | { | ||
1006 | cur--; | ||
1007 | } | ||
1008 | left = cur; | ||
1009 | if( cur > 0 ) | ||
1010 | { | ||
1011 | left++; | ||
1012 | } | ||
1013 | |||
1014 | // Expand right to end of line | ||
1015 | if( text[right - 1] == '\n' ) | ||
1016 | { | ||
1017 | right--; | ||
1018 | } | ||
1019 | else | ||
1020 | { | ||
1021 | while( (text[right] != '\n') && (right <= getLength() ) ) | ||
1022 | { | ||
1023 | right++; | ||
1024 | } | ||
1025 | } | ||
1026 | |||
1027 | // Find each start-of-line and indent it | ||
1028 | do | ||
1029 | { | ||
1030 | if( text[cur] == '\n' ) | ||
1031 | { | ||
1032 | cur++; | ||
1033 | } | ||
1034 | |||
1035 | S32 delta_spaces = indentLine( cur, spaces ); | ||
1036 | if( delta_spaces > 0 ) | ||
1037 | { | ||
1038 | cur += delta_spaces; | ||
1039 | } | ||
1040 | right += delta_spaces; | ||
1041 | |||
1042 | //text = mWText; | ||
1043 | |||
1044 | // Find the next new line | ||
1045 | while( (cur < right) && (text[cur] != '\n') ) | ||
1046 | { | ||
1047 | cur++; | ||
1048 | } | ||
1049 | } | ||
1050 | while( cur < right ); | ||
1051 | |||
1052 | if( (right < getLength()) && (text[right] == '\n') ) | ||
1053 | { | ||
1054 | right++; | ||
1055 | } | ||
1056 | |||
1057 | // Set the selection and cursor | ||
1058 | if( cursor_on_right ) | ||
1059 | { | ||
1060 | mSelectionStart = left; | ||
1061 | mSelectionEnd = right; | ||
1062 | } | ||
1063 | else | ||
1064 | { | ||
1065 | mSelectionStart = right; | ||
1066 | mSelectionEnd = left; | ||
1067 | } | ||
1068 | mCursorPos = mSelectionEnd; | ||
1069 | } | ||
1070 | } | ||
1071 | |||
1072 | |||
1073 | BOOL LLTextEditor::canSelectAll() | ||
1074 | { | ||
1075 | return TRUE; | ||
1076 | } | ||
1077 | |||
1078 | void LLTextEditor::selectAll() | ||
1079 | { | ||
1080 | mSelectionStart = getLength(); | ||
1081 | mSelectionEnd = 0; | ||
1082 | mCursorPos = mSelectionEnd; | ||
1083 | } | ||
1084 | |||
1085 | |||
1086 | BOOL LLTextEditor::handleToolTip(S32 x, S32 y, LLString& msg, LLRect* sticky_rect_screen) | ||
1087 | { | ||
1088 | if (pointInView(x, y) && getVisible()) | ||
1089 | { | ||
1090 | for ( child_list_const_iter_t child_it = getChildList()->begin(); | ||
1091 | child_it != getChildList()->end(); ++child_it) | ||
1092 | { | ||
1093 | LLView* viewp = *child_it; | ||
1094 | S32 local_x = x - viewp->getRect().mLeft; | ||
1095 | S32 local_y = y - viewp->getRect().mBottom; | ||
1096 | if( viewp->handleToolTip(local_x, local_y, msg, sticky_rect_screen ) ) | ||
1097 | { | ||
1098 | return TRUE; | ||
1099 | } | ||
1100 | } | ||
1101 | |||
1102 | if( mSegments.empty() ) | ||
1103 | { | ||
1104 | return TRUE; | ||
1105 | } | ||
1106 | |||
1107 | LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); | ||
1108 | if( cur_segment ) | ||
1109 | { | ||
1110 | BOOL has_tool_tip = FALSE; | ||
1111 | has_tool_tip = cur_segment->getToolTip( msg ); | ||
1112 | |||
1113 | if( has_tool_tip ) | ||
1114 | { | ||
1115 | // Just use a slop area around the cursor | ||
1116 | // Convert rect local to screen coordinates | ||
1117 | S32 SLOP = 8; | ||
1118 | localPointToScreen( | ||
1119 | x - SLOP, y - SLOP, | ||
1120 | &(sticky_rect_screen->mLeft), &(sticky_rect_screen->mBottom) ); | ||
1121 | sticky_rect_screen->mRight = sticky_rect_screen->mLeft + 2 * SLOP; | ||
1122 | sticky_rect_screen->mTop = sticky_rect_screen->mBottom + 2 * SLOP; | ||
1123 | } | ||
1124 | } | ||
1125 | return TRUE; | ||
1126 | } | ||
1127 | return FALSE; | ||
1128 | } | ||
1129 | |||
1130 | BOOL LLTextEditor::handleScrollWheel(S32 x, S32 y, S32 clicks) | ||
1131 | { | ||
1132 | // Pretend the mouse is over the scrollbar | ||
1133 | if (getVisible()) | ||
1134 | { | ||
1135 | return mScrollbar->handleScrollWheel( 0, 0, clicks ); | ||
1136 | } | ||
1137 | else | ||
1138 | { | ||
1139 | return FALSE; | ||
1140 | } | ||
1141 | } | ||
1142 | |||
1143 | BOOL LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask) | ||
1144 | { | ||
1145 | BOOL handled = FALSE; | ||
1146 | |||
1147 | // Let scrollbar have first dibs | ||
1148 | handled = LLView::childrenHandleMouseDown(x, y, mask) != NULL; | ||
1149 | |||
1150 | if( !handled && mTakesNonScrollClicks) | ||
1151 | { | ||
1152 | if (!(mask & MASK_SHIFT)) | ||
1153 | { | ||
1154 | deselect(); | ||
1155 | } | ||
1156 | |||
1157 | BOOL start_select = TRUE; | ||
1158 | if( start_select ) | ||
1159 | { | ||
1160 | // If we're not scrolling (handled by child), then we're selecting | ||
1161 | if (mask & MASK_SHIFT) | ||
1162 | { | ||
1163 | S32 old_cursor_pos = mCursorPos; | ||
1164 | setCursorAtLocalPos( x, y, TRUE ); | ||
1165 | |||
1166 | if (hasSelection()) | ||
1167 | { | ||
1168 | /* Mac-like behavior - extend selection towards the cursor | ||
1169 | if (mCursorPos < mSelectionStart | ||
1170 | && mCursorPos < mSelectionEnd) | ||
1171 | { | ||
1172 | // ...left of selection | ||
1173 | mSelectionStart = llmax(mSelectionStart, mSelectionEnd); | ||
1174 | mSelectionEnd = mCursorPos; | ||
1175 | } | ||
1176 | else if (mCursorPos > mSelectionStart | ||
1177 | && mCursorPos > mSelectionEnd) | ||
1178 | { | ||
1179 | // ...right of selection | ||
1180 | mSelectionStart = llmin(mSelectionStart, mSelectionEnd); | ||
1181 | mSelectionEnd = mCursorPos; | ||
1182 | } | ||
1183 | else | ||
1184 | { | ||
1185 | mSelectionEnd = mCursorPos; | ||
1186 | } | ||
1187 | */ | ||
1188 | // Windows behavior | ||
1189 | mSelectionEnd = mCursorPos; | ||
1190 | } | ||
1191 | else | ||
1192 | { | ||
1193 | mSelectionStart = old_cursor_pos; | ||
1194 | mSelectionEnd = mCursorPos; | ||
1195 | } | ||
1196 | // assume we're starting a drag select | ||
1197 | mIsSelecting = TRUE; | ||
1198 | } | ||
1199 | else | ||
1200 | { | ||
1201 | setCursorAtLocalPos( x, y, TRUE ); | ||
1202 | startSelection(); | ||
1203 | } | ||
1204 | gFocusMgr.setMouseCapture( this, &LLTextEditor::onMouseCaptureLost ); | ||
1205 | } | ||
1206 | |||
1207 | handled = TRUE; | ||
1208 | } | ||
1209 | |||
1210 | if (mTakesFocus) | ||
1211 | { | ||
1212 | setFocus( TRUE ); | ||
1213 | handled = TRUE; | ||
1214 | } | ||
1215 | |||
1216 | // Delay cursor flashing | ||
1217 | mKeystrokeTimer.reset(); | ||
1218 | |||
1219 | return handled; | ||
1220 | } | ||
1221 | |||
1222 | |||
1223 | BOOL LLTextEditor::handleHover(S32 x, S32 y, MASK mask) | ||
1224 | { | ||
1225 | BOOL handled = FALSE; | ||
1226 | |||
1227 | mHoverSegment = NULL; | ||
1228 | if( getVisible() ) | ||
1229 | { | ||
1230 | if(gFocusMgr.getMouseCapture() == this ) | ||
1231 | { | ||
1232 | if( mIsSelecting ) | ||
1233 | { | ||
1234 | if (x != mLastSelectionX || y != mLastSelectionY) | ||
1235 | { | ||
1236 | mLastSelectionX = x; | ||
1237 | mLastSelectionY = y; | ||
1238 | } | ||
1239 | |||
1240 | if( y > mTextRect.mTop ) | ||
1241 | { | ||
1242 | mScrollbar->setDocPos( mScrollbar->getDocPos() - 1 ); | ||
1243 | } | ||
1244 | else | ||
1245 | if( y < mTextRect.mBottom ) | ||
1246 | { | ||
1247 | mScrollbar->setDocPos( mScrollbar->getDocPos() + 1 ); | ||
1248 | } | ||
1249 | |||
1250 | setCursorAtLocalPos( x, y, TRUE ); | ||
1251 | mSelectionEnd = mCursorPos; | ||
1252 | |||
1253 | updateScrollFromCursor(); | ||
1254 | } | ||
1255 | |||
1256 | lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (active)" << llendl; | ||
1257 | getWindow()->setCursor(UI_CURSOR_IBEAM); | ||
1258 | handled = TRUE; | ||
1259 | } | ||
1260 | |||
1261 | if( !handled ) | ||
1262 | { | ||
1263 | // Pass to children | ||
1264 | handled = LLView::childrenHandleHover(x, y, mask) != NULL; | ||
1265 | } | ||
1266 | |||
1267 | if( handled ) | ||
1268 | { | ||
1269 | // Delay cursor flashing | ||
1270 | mKeystrokeTimer.reset(); | ||
1271 | } | ||
1272 | |||
1273 | // Opaque | ||
1274 | if( !handled && mTakesNonScrollClicks) | ||
1275 | { | ||
1276 | // Check to see if we're over an HTML-style link | ||
1277 | if( !mSegments.empty() ) | ||
1278 | { | ||
1279 | LLTextSegment* cur_segment = getSegmentAtLocalPos( x, y ); | ||
1280 | if( cur_segment ) | ||
1281 | { | ||
1282 | if(cur_segment->getStyle().isLink()) | ||
1283 | { | ||
1284 | lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over link, inactive)" << llendl; | ||
1285 | getWindow()->setCursor(UI_CURSOR_HAND); | ||
1286 | handled = TRUE; | ||
1287 | } | ||
1288 | else | ||
1289 | if(cur_segment->getStyle().getIsEmbeddedItem()) | ||
1290 | { | ||
1291 | lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (over embedded item, inactive)" << llendl; | ||
1292 | getWindow()->setCursor(UI_CURSOR_HAND); | ||
1293 | //getWindow()->setCursor(UI_CURSOR_ARROW); | ||
1294 | handled = TRUE; | ||
1295 | } | ||
1296 | mHoverSegment = cur_segment; | ||
1297 | } | ||
1298 | } | ||
1299 | |||
1300 | if( !handled ) | ||
1301 | { | ||
1302 | lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (inactive)" << llendl; | ||
1303 | if (!mScrollbar->getVisible() || x < mRect.getWidth() - SCROLLBAR_SIZE) | ||
1304 | { | ||
1305 | getWindow()->setCursor(UI_CURSOR_IBEAM); | ||
1306 | } | ||
1307 | else | ||
1308 | { | ||
1309 | getWindow()->setCursor(UI_CURSOR_ARROW); | ||
1310 | } | ||
1311 | handled = TRUE; | ||
1312 | } | ||
1313 | } | ||
1314 | } | ||
1315 | |||
1316 | if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) | ||
1317 | { | ||
1318 | mOnScrollEndCallback(mOnScrollEndData); | ||
1319 | } | ||
1320 | return handled; | ||
1321 | } | ||
1322 | |||
1323 | |||
1324 | BOOL LLTextEditor::handleMouseUp(S32 x, S32 y, MASK mask) | ||
1325 | { | ||
1326 | BOOL handled = FALSE; | ||
1327 | |||
1328 | // let scrollbar have first dibs | ||
1329 | handled = LLView::childrenHandleMouseUp(x, y, mask) != NULL; | ||
1330 | |||
1331 | if( !handled && mTakesNonScrollClicks) | ||
1332 | { | ||
1333 | if( mIsSelecting ) | ||
1334 | { | ||
1335 | // Finish selection | ||
1336 | if( y > mTextRect.mTop ) | ||
1337 | { | ||
1338 | mScrollbar->setDocPos( mScrollbar->getDocPos() - 1 ); | ||
1339 | } | ||
1340 | else | ||
1341 | if( y < mTextRect.mBottom ) | ||
1342 | { | ||
1343 | mScrollbar->setDocPos( mScrollbar->getDocPos() + 1 ); | ||
1344 | } | ||
1345 | |||
1346 | setCursorAtLocalPos( x, y, TRUE ); | ||
1347 | endSelection(); | ||
1348 | |||
1349 | updateScrollFromCursor(); | ||
1350 | } | ||
1351 | |||
1352 | if( !hasSelection() ) | ||
1353 | { | ||
1354 | handleMouseUpOverSegment( x, y, mask ); | ||
1355 | } | ||
1356 | |||
1357 | handled = TRUE; | ||
1358 | } | ||
1359 | |||
1360 | // Delay cursor flashing | ||
1361 | mKeystrokeTimer.reset(); | ||
1362 | |||
1363 | if( gFocusMgr.getMouseCapture() == this ) | ||
1364 | { | ||
1365 | gFocusMgr.setMouseCapture( NULL, NULL ); | ||
1366 | handled = TRUE; | ||
1367 | } | ||
1368 | |||
1369 | return handled; | ||
1370 | } | ||
1371 | |||
1372 | |||
1373 | BOOL LLTextEditor::handleDoubleClick(S32 x, S32 y, MASK mask) | ||
1374 | { | ||
1375 | BOOL handled = FALSE; | ||
1376 | |||
1377 | // let scrollbar have first dibs | ||
1378 | handled = LLView::childrenHandleDoubleClick(x, y, mask) != NULL; | ||
1379 | |||
1380 | if( !handled && mTakesNonScrollClicks) | ||
1381 | { | ||
1382 | if (mTakesFocus) | ||
1383 | { | ||
1384 | setFocus( TRUE ); | ||
1385 | } | ||
1386 | |||
1387 | setCursorAtLocalPos( x, y, FALSE ); | ||
1388 | deselect(); | ||
1389 | |||
1390 | const LLWString &text = mWText; | ||
1391 | |||
1392 | if( isPartOfWord( text[mCursorPos] ) ) | ||
1393 | { | ||
1394 | // Select word the cursor is over | ||
1395 | while ((mCursorPos > 0) && isPartOfWord(text[mCursorPos-1])) | ||
1396 | { | ||
1397 | mCursorPos--; | ||
1398 | } | ||
1399 | startSelection(); | ||
1400 | |||
1401 | while ((mCursorPos < (S32)text.length()) && isPartOfWord( text[mCursorPos] ) ) | ||
1402 | { | ||
1403 | mCursorPos++; | ||
1404 | } | ||
1405 | |||
1406 | mSelectionEnd = mCursorPos; | ||
1407 | } | ||
1408 | else if ((mCursorPos < (S32)text.length()) && !iswspace( text[mCursorPos]) ) | ||
1409 | { | ||
1410 | // Select the character the cursor is over | ||
1411 | startSelection(); | ||
1412 | mCursorPos++; | ||
1413 | mSelectionEnd = mCursorPos; | ||
1414 | } | ||
1415 | |||
1416 | // We don't want handleMouseUp() to "finish" the selection (and thereby | ||
1417 | // set mSelectionEnd to where the mouse is), so we finish the selection here. | ||
1418 | mIsSelecting = FALSE; | ||
1419 | |||
1420 | // delay cursor flashing | ||
1421 | mKeystrokeTimer.reset(); | ||
1422 | |||
1423 | handled = TRUE; | ||
1424 | } | ||
1425 | return handled; | ||
1426 | } | ||
1427 | |||
1428 | |||
1429 | // Allow calling cards to be dropped onto text fields. Append the name and | ||
1430 | // a carriage return. | ||
1431 | // virtual | ||
1432 | BOOL LLTextEditor::handleDragAndDrop(S32 x, S32 y, MASK mask, | ||
1433 | BOOL drop, EDragAndDropType cargo_type, void *cargo_data, | ||
1434 | EAcceptance *accept, | ||
1435 | LLString& tooltip_msg) | ||
1436 | { | ||
1437 | *accept = ACCEPT_NO; | ||
1438 | |||
1439 | return TRUE; | ||
1440 | } | ||
1441 | |||
1442 | //---------------------------------------------------------------------------- | ||
1443 | // Returns change in number of characters in mText | ||
1444 | |||
1445 | S32 LLTextEditor::execute( LLTextCmd* cmd ) | ||
1446 | { | ||
1447 | S32 delta = 0; | ||
1448 | if( cmd->execute(this, &delta) ) | ||
1449 | { | ||
1450 | // Delete top of undo stack | ||
1451 | undo_stack_t::iterator enditer = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd); | ||
1452 | if (enditer != mUndoStack.begin()) | ||
1453 | { | ||
1454 | --enditer; | ||
1455 | std::for_each(mUndoStack.begin(), enditer, DeletePointer()); | ||
1456 | mUndoStack.erase(mUndoStack.begin(), enditer); | ||
1457 | } | ||
1458 | // Push the new command is now on the top (front) of the undo stack. | ||
1459 | mUndoStack.push_front(cmd); | ||
1460 | mLastCmd = cmd; | ||
1461 | } | ||
1462 | else | ||
1463 | { | ||
1464 | // Operation failed, so don't put it on the undo stack. | ||
1465 | delete cmd; | ||
1466 | } | ||
1467 | |||
1468 | return delta; | ||
1469 | } | ||
1470 | |||
1471 | S32 LLTextEditor::insert(const S32 pos, const LLWString &wstr, const BOOL group_with_next_op) | ||
1472 | { | ||
1473 | return execute( new LLTextCmdInsert( pos, group_with_next_op, wstr ) ); | ||
1474 | } | ||
1475 | |||
1476 | S32 LLTextEditor::remove(const S32 pos, const S32 length, const BOOL group_with_next_op) | ||
1477 | { | ||
1478 | return execute( new LLTextCmdRemove( pos, group_with_next_op, length ) ); | ||
1479 | } | ||
1480 | |||
1481 | S32 LLTextEditor::append(const LLWString &wstr, const BOOL group_with_next_op) | ||
1482 | { | ||
1483 | return insert(mWText.length(), wstr, group_with_next_op); | ||
1484 | } | ||
1485 | |||
1486 | S32 LLTextEditor::overwriteChar(S32 pos, llwchar wc) | ||
1487 | { | ||
1488 | if ((S32)mWText.length() == pos) | ||
1489 | { | ||
1490 | return addChar(pos, wc); | ||
1491 | } | ||
1492 | else | ||
1493 | { | ||
1494 | return execute(new LLTextCmdOverwriteChar(pos, FALSE, wc)); | ||
1495 | } | ||
1496 | } | ||
1497 | |||
1498 | // Remove a single character from the text. Tries to remove | ||
1499 | // a pseudo-tab (up to for spaces in a row) | ||
1500 | void LLTextEditor::removeCharOrTab() | ||
1501 | { | ||
1502 | if( getEnabled() ) | ||
1503 | { | ||
1504 | if( mCursorPos > 0 ) | ||
1505 | { | ||
1506 | S32 chars_to_remove = 1; | ||
1507 | |||
1508 | const LLWString &text = mWText; | ||
1509 | if (text[mCursorPos - 1] == ' ') | ||
1510 | { | ||
1511 | // Try to remove a "tab" | ||
1512 | S32 line, offset; | ||
1513 | getLineAndOffset(mCursorPos, &line, &offset); | ||
1514 | if (offset > 0) | ||
1515 | { | ||
1516 | chars_to_remove = offset % SPACES_PER_TAB; | ||
1517 | if( chars_to_remove == 0 ) | ||
1518 | { | ||
1519 | chars_to_remove = SPACES_PER_TAB; | ||
1520 | } | ||
1521 | |||
1522 | for( S32 i = 0; i < chars_to_remove; i++ ) | ||
1523 | { | ||
1524 | if (text[ mCursorPos - i - 1] != ' ') | ||
1525 | { | ||
1526 | // Fewer than a full tab's worth of spaces, so | ||
1527 | // just delete a single character. | ||
1528 | chars_to_remove = 1; | ||
1529 | break; | ||
1530 | } | ||
1531 | } | ||
1532 | } | ||
1533 | } | ||
1534 | |||
1535 | for (S32 i = 0; i < chars_to_remove; i++) | ||
1536 | { | ||
1537 | setCursorPos(mCursorPos - 1); | ||
1538 | remove( mCursorPos, 1, FALSE ); | ||
1539 | } | ||
1540 | } | ||
1541 | else | ||
1542 | { | ||
1543 | reportBadKeystroke(); | ||
1544 | } | ||
1545 | } | ||
1546 | } | ||
1547 | |||
1548 | // Remove a single character from the text | ||
1549 | S32 LLTextEditor::removeChar(S32 pos) | ||
1550 | { | ||
1551 | return remove( pos, 1, FALSE ); | ||
1552 | } | ||
1553 | |||
1554 | void LLTextEditor::removeChar() | ||
1555 | { | ||
1556 | if (getEnabled()) | ||
1557 | { | ||
1558 | if (mCursorPos > 0) | ||
1559 | { | ||
1560 | setCursorPos(mCursorPos - 1); | ||
1561 | removeChar(mCursorPos); | ||
1562 | } | ||
1563 | else | ||
1564 | { | ||
1565 | reportBadKeystroke(); | ||
1566 | } | ||
1567 | } | ||
1568 | } | ||
1569 | |||
1570 | // Add a single character to the text | ||
1571 | S32 LLTextEditor::addChar(S32 pos, llwchar wc) | ||
1572 | { | ||
1573 | if ((S32)mWText.length() == mMaxTextLength) | ||
1574 | { | ||
1575 | make_ui_sound("UISndBadKeystroke"); | ||
1576 | return 0; | ||
1577 | } | ||
1578 | |||
1579 | if (mLastCmd && mLastCmd->canExtend(pos)) | ||
1580 | { | ||
1581 | S32 delta = 0; | ||
1582 | mLastCmd->extendAndExecute(this, pos, wc, &delta); | ||
1583 | return delta; | ||
1584 | } | ||
1585 | else | ||
1586 | { | ||
1587 | return execute(new LLTextCmdAddChar(pos, FALSE, wc)); | ||
1588 | } | ||
1589 | } | ||
1590 | |||
1591 | void LLTextEditor::addChar(llwchar wc) | ||
1592 | { | ||
1593 | if( getEnabled() ) | ||
1594 | { | ||
1595 | if( hasSelection() ) | ||
1596 | { | ||
1597 | deleteSelection(TRUE); | ||
1598 | } | ||
1599 | else if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) | ||
1600 | { | ||
1601 | removeChar(mCursorPos); | ||
1602 | } | ||
1603 | |||
1604 | setCursorPos(mCursorPos + addChar( mCursorPos, wc )); | ||
1605 | } | ||
1606 | } | ||
1607 | |||
1608 | |||
1609 | BOOL LLTextEditor::handleSelectionKey(const KEY key, const MASK mask) | ||
1610 | { | ||
1611 | BOOL handled = FALSE; | ||
1612 | |||
1613 | if( mask & MASK_SHIFT ) | ||
1614 | { | ||
1615 | handled = TRUE; | ||
1616 | |||
1617 | switch( key ) | ||
1618 | { | ||
1619 | case KEY_LEFT: | ||
1620 | if( 0 < mCursorPos ) | ||
1621 | { | ||
1622 | startSelection(); | ||
1623 | mCursorPos--; | ||
1624 | if( mask & MASK_CONTROL ) | ||
1625 | { | ||
1626 | mCursorPos = prevWordPos(mCursorPos); | ||
1627 | } | ||
1628 | mSelectionEnd = mCursorPos; | ||
1629 | } | ||
1630 | break; | ||
1631 | |||
1632 | case KEY_RIGHT: | ||
1633 | if( mCursorPos < getLength() ) | ||
1634 | { | ||
1635 | startSelection(); | ||
1636 | mCursorPos++; | ||
1637 | if( mask & MASK_CONTROL ) | ||
1638 | { | ||
1639 | mCursorPos = nextWordPos(mCursorPos); | ||
1640 | } | ||
1641 | mSelectionEnd = mCursorPos; | ||
1642 | } | ||
1643 | break; | ||
1644 | |||
1645 | case KEY_UP: | ||
1646 | startSelection(); | ||
1647 | changeLine( -1 ); | ||
1648 | mSelectionEnd = mCursorPos; | ||
1649 | break; | ||
1650 | |||
1651 | case KEY_PAGE_UP: | ||
1652 | startSelection(); | ||
1653 | changePage( -1 ); | ||
1654 | mSelectionEnd = mCursorPos; | ||
1655 | break; | ||
1656 | |||
1657 | case KEY_HOME: | ||
1658 | startSelection(); | ||
1659 | if( mask & MASK_CONTROL ) | ||
1660 | { | ||
1661 | mCursorPos = 0; | ||
1662 | } | ||
1663 | else | ||
1664 | { | ||
1665 | startOfLine(); | ||
1666 | } | ||
1667 | mSelectionEnd = mCursorPos; | ||
1668 | break; | ||
1669 | |||
1670 | case KEY_DOWN: | ||
1671 | startSelection(); | ||
1672 | changeLine( 1 ); | ||
1673 | mSelectionEnd = mCursorPos; | ||
1674 | break; | ||
1675 | |||
1676 | case KEY_PAGE_DOWN: | ||
1677 | startSelection(); | ||
1678 | changePage( 1 ); | ||
1679 | mSelectionEnd = mCursorPos; | ||
1680 | break; | ||
1681 | |||
1682 | case KEY_END: | ||
1683 | startSelection(); | ||
1684 | if( mask & MASK_CONTROL ) | ||
1685 | { | ||
1686 | mCursorPos = getLength(); | ||
1687 | } | ||
1688 | else | ||
1689 | { | ||
1690 | endOfLine(); | ||
1691 | } | ||
1692 | mSelectionEnd = mCursorPos; | ||
1693 | break; | ||
1694 | |||
1695 | default: | ||
1696 | handled = FALSE; | ||
1697 | break; | ||
1698 | } | ||
1699 | } | ||
1700 | |||
1701 | |||
1702 | if( !handled && mHandleEditKeysDirectly ) | ||
1703 | { | ||
1704 | if( (MASK_CONTROL & mask) && ('A' == key) ) | ||
1705 | { | ||
1706 | if( canSelectAll() ) | ||
1707 | { | ||
1708 | selectAll(); | ||
1709 | } | ||
1710 | else | ||
1711 | { | ||
1712 | reportBadKeystroke(); | ||
1713 | } | ||
1714 | handled = TRUE; | ||
1715 | } | ||
1716 | } | ||
1717 | |||
1718 | return handled; | ||
1719 | } | ||
1720 | |||
1721 | BOOL LLTextEditor::handleNavigationKey(const KEY key, const MASK mask) | ||
1722 | { | ||
1723 | BOOL handled = FALSE; | ||
1724 | |||
1725 | // Ignore capslock key | ||
1726 | if( MASK_NONE == mask ) | ||
1727 | { | ||
1728 | handled = TRUE; | ||
1729 | switch( key ) | ||
1730 | { | ||
1731 | case KEY_UP: | ||
1732 | if (mReadOnly) | ||
1733 | { | ||
1734 | mScrollbar->setDocPos(mScrollbar->getDocPos() - 1); | ||
1735 | } | ||
1736 | else | ||
1737 | { | ||
1738 | changeLine( -1 ); | ||
1739 | } | ||
1740 | break; | ||
1741 | |||
1742 | case KEY_PAGE_UP: | ||
1743 | changePage( -1 ); | ||
1744 | break; | ||
1745 | |||
1746 | case KEY_HOME: | ||
1747 | if (mReadOnly) | ||
1748 | { | ||
1749 | mScrollbar->setDocPos(0); | ||
1750 | } | ||
1751 | else | ||
1752 | { | ||
1753 | startOfLine(); | ||
1754 | } | ||
1755 | break; | ||
1756 | |||
1757 | case KEY_DOWN: | ||
1758 | if (mReadOnly) | ||
1759 | { | ||
1760 | mScrollbar->setDocPos(mScrollbar->getDocPos() + 1); | ||
1761 | } | ||
1762 | else | ||
1763 | { | ||
1764 | changeLine( 1 ); | ||
1765 | } | ||
1766 | break; | ||
1767 | |||
1768 | case KEY_PAGE_DOWN: | ||
1769 | changePage( 1 ); | ||
1770 | break; | ||
1771 | |||
1772 | case KEY_END: | ||
1773 | if (mReadOnly) | ||
1774 | { | ||
1775 | mScrollbar->setDocPos(mScrollbar->getDocPosMax()); | ||
1776 | } | ||
1777 | else | ||
1778 | { | ||
1779 | endOfLine(); | ||
1780 | } | ||
1781 | break; | ||
1782 | |||
1783 | case KEY_LEFT: | ||
1784 | if (mReadOnly) | ||
1785 | { | ||
1786 | break; | ||
1787 | } | ||
1788 | if( hasSelection() ) | ||
1789 | { | ||
1790 | setCursorPos(llmin( mCursorPos - 1, mSelectionStart, mSelectionEnd )); | ||
1791 | } | ||
1792 | else | ||
1793 | { | ||
1794 | if( 0 < mCursorPos ) | ||
1795 | { | ||
1796 | setCursorPos(mCursorPos - 1); | ||
1797 | } | ||
1798 | else | ||
1799 | { | ||
1800 | reportBadKeystroke(); | ||
1801 | } | ||
1802 | } | ||
1803 | break; | ||
1804 | |||
1805 | case KEY_RIGHT: | ||
1806 | if (mReadOnly) | ||
1807 | { | ||
1808 | break; | ||
1809 | } | ||
1810 | if( hasSelection() ) | ||
1811 | { | ||
1812 | setCursorPos(llmax( mCursorPos + 1, mSelectionStart, mSelectionEnd )); | ||
1813 | } | ||
1814 | else | ||
1815 | { | ||
1816 | if( mCursorPos < getLength() ) | ||
1817 | { | ||
1818 | setCursorPos(mCursorPos + 1); | ||
1819 | } | ||
1820 | else | ||
1821 | { | ||
1822 | reportBadKeystroke(); | ||
1823 | } | ||
1824 | } | ||
1825 | break; | ||
1826 | |||
1827 | default: | ||
1828 | handled = FALSE; | ||
1829 | break; | ||
1830 | } | ||
1831 | } | ||
1832 | |||
1833 | if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) | ||
1834 | { | ||
1835 | mOnScrollEndCallback(mOnScrollEndData); | ||
1836 | } | ||
1837 | return handled; | ||
1838 | } | ||
1839 | |||
1840 | void LLTextEditor::deleteSelection(BOOL group_with_next_op ) | ||
1841 | { | ||
1842 | if( getEnabled() && hasSelection() ) | ||
1843 | { | ||
1844 | S32 pos = llmin( mSelectionStart, mSelectionEnd ); | ||
1845 | S32 length = abs( mSelectionStart - mSelectionEnd ); | ||
1846 | |||
1847 | remove( pos, length, group_with_next_op ); | ||
1848 | |||
1849 | deselect(); | ||
1850 | setCursorPos(pos); | ||
1851 | } | ||
1852 | } | ||
1853 | |||
1854 | BOOL LLTextEditor::canCut() | ||
1855 | { | ||
1856 | return !mReadOnly && hasSelection(); | ||
1857 | } | ||
1858 | |||
1859 | // cut selection to clipboard | ||
1860 | void LLTextEditor::cut() | ||
1861 | { | ||
1862 | if( canCut() ) | ||
1863 | { | ||
1864 | S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); | ||
1865 | S32 length = abs( mSelectionStart - mSelectionEnd ); | ||
1866 | gClipboard.copyFromSubstring( mWText, left_pos, length, mSourceID ); | ||
1867 | deleteSelection( FALSE ); | ||
1868 | |||
1869 | updateLineStartList(); | ||
1870 | updateScrollFromCursor(); | ||
1871 | } | ||
1872 | } | ||
1873 | |||
1874 | BOOL LLTextEditor::canCopy() | ||
1875 | { | ||
1876 | return hasSelection(); | ||
1877 | } | ||
1878 | |||
1879 | |||
1880 | // copy selection to clipboard | ||
1881 | void LLTextEditor::copy() | ||
1882 | { | ||
1883 | if( canCopy() ) | ||
1884 | { | ||
1885 | S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); | ||
1886 | S32 length = abs( mSelectionStart - mSelectionEnd ); | ||
1887 | gClipboard.copyFromSubstring(mWText, left_pos, length, mSourceID); | ||
1888 | } | ||
1889 | } | ||
1890 | |||
1891 | BOOL LLTextEditor::canPaste() | ||
1892 | { | ||
1893 | return !mReadOnly && gClipboard.canPasteString(); | ||
1894 | } | ||
1895 | |||
1896 | |||
1897 | // paste from clipboard | ||
1898 | void LLTextEditor::paste() | ||
1899 | { | ||
1900 | if (canPaste()) | ||
1901 | { | ||
1902 | LLUUID source_id; | ||
1903 | LLWString paste = gClipboard.getPasteWString(&source_id); | ||
1904 | if (!paste.empty()) | ||
1905 | { | ||
1906 | // Delete any selected characters (the paste replaces them) | ||
1907 | if( hasSelection() ) | ||
1908 | { | ||
1909 | deleteSelection(TRUE); | ||
1910 | } | ||
1911 | |||
1912 | // Clean up string (replace tabs and remove characters that our fonts don't support). | ||
1913 | LLWString clean_string(paste); | ||
1914 | LLWString::replaceTabsWithSpaces(clean_string, SPACES_PER_TAB); | ||
1915 | if( mAllowEmbeddedItems ) | ||
1916 | { | ||
1917 | const llwchar LF = 10; | ||
1918 | S32 len = clean_string.length(); | ||
1919 | for( S32 i = 0; i < len; i++ ) | ||
1920 | { | ||
1921 | llwchar wc = clean_string[i]; | ||
1922 | if( (wc < LLFont::FIRST_CHAR) && (wc != LF) ) | ||
1923 | { | ||
1924 | clean_string[i] = LL_UNKNOWN_CHAR; | ||
1925 | } | ||
1926 | else if (wc >= FIRST_EMBEDDED_CHAR && wc <= LAST_EMBEDDED_CHAR) | ||
1927 | { | ||
1928 | clean_string[i] = pasteEmbeddedItem(wc); | ||
1929 | } | ||
1930 | } | ||
1931 | } | ||
1932 | |||
1933 | // Insert the new text into the existing text. | ||
1934 | setCursorPos(mCursorPos + insert(mCursorPos, clean_string, FALSE)); | ||
1935 | deselect(); | ||
1936 | |||
1937 | updateLineStartList(); | ||
1938 | updateScrollFromCursor(); | ||
1939 | } | ||
1940 | } | ||
1941 | } | ||
1942 | |||
1943 | |||
1944 | BOOL LLTextEditor::handleControlKey(const KEY key, const MASK mask) | ||
1945 | { | ||
1946 | BOOL handled = FALSE; | ||
1947 | |||
1948 | if( mask & MASK_CONTROL ) | ||
1949 | { | ||
1950 | handled = TRUE; | ||
1951 | |||
1952 | switch( key ) | ||
1953 | { | ||
1954 | case KEY_HOME: | ||
1955 | if( mask & MASK_SHIFT ) | ||
1956 | { | ||
1957 | startSelection(); | ||
1958 | mCursorPos = 0; | ||
1959 | mSelectionEnd = mCursorPos; | ||
1960 | } | ||
1961 | else | ||
1962 | { | ||
1963 | // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down | ||
1964 | // all move the cursor as if clicking, so should deselect. | ||
1965 | deselect(); | ||
1966 | setCursorPos(0); | ||
1967 | } | ||
1968 | break; | ||
1969 | |||
1970 | case KEY_END: | ||
1971 | { | ||
1972 | if( mask & MASK_SHIFT ) | ||
1973 | { | ||
1974 | startSelection(); | ||
1975 | } | ||
1976 | else | ||
1977 | { | ||
1978 | // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down | ||
1979 | // all move the cursor as if clicking, so should deselect. | ||
1980 | deselect(); | ||
1981 | } | ||
1982 | endOfDoc(); | ||
1983 | if( mask & MASK_SHIFT ) | ||
1984 | { | ||
1985 | mSelectionEnd = mCursorPos; | ||
1986 | } | ||
1987 | break; | ||
1988 | } | ||
1989 | |||
1990 | case KEY_RIGHT: | ||
1991 | if( mCursorPos < getLength() ) | ||
1992 | { | ||
1993 | // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down | ||
1994 | // all move the cursor as if clicking, so should deselect. | ||
1995 | deselect(); | ||
1996 | |||
1997 | setCursorPos(nextWordPos(mCursorPos + 1)); | ||
1998 | } | ||
1999 | break; | ||
2000 | |||
2001 | |||
2002 | case KEY_LEFT: | ||
2003 | if( mCursorPos > 0 ) | ||
2004 | { | ||
2005 | // Ctrl-Home, Ctrl-Left, Ctrl-Right, Ctrl-Down | ||
2006 | // all move the cursor as if clicking, so should deselect. | ||
2007 | deselect(); | ||
2008 | |||
2009 | setCursorPos(prevWordPos(mCursorPos - 1)); | ||
2010 | } | ||
2011 | break; | ||
2012 | |||
2013 | default: | ||
2014 | handled = FALSE; | ||
2015 | break; | ||
2016 | } | ||
2017 | } | ||
2018 | |||
2019 | return handled; | ||
2020 | } | ||
2021 | |||
2022 | BOOL LLTextEditor::handleEditKey(const KEY key, const MASK mask) | ||
2023 | { | ||
2024 | BOOL handled = FALSE; | ||
2025 | |||
2026 | // Standard edit keys (Ctrl-X, Delete, etc,) are handled here instead of routed by the menu system. | ||
2027 | if( KEY_DELETE == key ) | ||
2028 | { | ||
2029 | if( canDoDelete() ) | ||
2030 | { | ||
2031 | doDelete(); | ||
2032 | } | ||
2033 | else | ||
2034 | { | ||
2035 | reportBadKeystroke(); | ||
2036 | } | ||
2037 | handled = TRUE; | ||
2038 | } | ||
2039 | else | ||
2040 | if( MASK_CONTROL & mask ) | ||
2041 | { | ||
2042 | if( 'C' == key ) | ||
2043 | { | ||
2044 | if( canCopy() ) | ||
2045 | { | ||
2046 | copy(); | ||
2047 | } | ||
2048 | else | ||
2049 | { | ||
2050 | reportBadKeystroke(); | ||
2051 | } | ||
2052 | handled = TRUE; | ||
2053 | } | ||
2054 | else | ||
2055 | if( 'V' == key ) | ||
2056 | { | ||
2057 | if( canPaste() ) | ||
2058 | { | ||
2059 | paste(); | ||
2060 | } | ||
2061 | else | ||
2062 | { | ||
2063 | reportBadKeystroke(); | ||
2064 | } | ||
2065 | handled = TRUE; | ||
2066 | } | ||
2067 | else | ||
2068 | if( 'X' == key ) | ||
2069 | { | ||
2070 | if( canCut() ) | ||
2071 | { | ||
2072 | cut(); | ||
2073 | } | ||
2074 | else | ||
2075 | { | ||
2076 | reportBadKeystroke(); | ||
2077 | } | ||
2078 | handled = TRUE; | ||
2079 | } | ||
2080 | } | ||
2081 | |||
2082 | return handled; | ||
2083 | } | ||
2084 | |||
2085 | |||
2086 | BOOL LLTextEditor::handleSpecialKey(const KEY key, const MASK mask, BOOL* return_key_hit) | ||
2087 | { | ||
2088 | *return_key_hit = FALSE; | ||
2089 | BOOL handled = TRUE; | ||
2090 | |||
2091 | switch( key ) | ||
2092 | { | ||
2093 | case KEY_INSERT: | ||
2094 | if (mask == MASK_NONE) | ||
2095 | { | ||
2096 | gKeyboard->toggleInsertMode(); | ||
2097 | } | ||
2098 | break; | ||
2099 | |||
2100 | case KEY_BACKSPACE: | ||
2101 | if( hasSelection() ) | ||
2102 | { | ||
2103 | deleteSelection(FALSE); | ||
2104 | } | ||
2105 | else | ||
2106 | if( 0 < mCursorPos ) | ||
2107 | { | ||
2108 | removeCharOrTab(); | ||
2109 | } | ||
2110 | else | ||
2111 | { | ||
2112 | reportBadKeystroke(); | ||
2113 | } | ||
2114 | break; | ||
2115 | |||
2116 | |||
2117 | case KEY_RETURN: | ||
2118 | if (mask == MASK_NONE) | ||
2119 | { | ||
2120 | if( hasSelection() ) | ||
2121 | { | ||
2122 | deleteSelection(FALSE); | ||
2123 | } | ||
2124 | autoIndent(); // TODO: make this optional | ||
2125 | } | ||
2126 | else | ||
2127 | { | ||
2128 | handled = FALSE; | ||
2129 | break; | ||
2130 | } | ||
2131 | break; | ||
2132 | |||
2133 | case KEY_TAB: | ||
2134 | if (mask & MASK_CONTROL) | ||
2135 | { | ||
2136 | handled = FALSE; | ||
2137 | break; | ||
2138 | } | ||
2139 | if( hasSelection() && selectionContainsLineBreaks() ) | ||
2140 | { | ||
2141 | indentSelectedLines( (mask & MASK_SHIFT) ? -SPACES_PER_TAB : SPACES_PER_TAB ); | ||
2142 | } | ||
2143 | else | ||
2144 | { | ||
2145 | if( hasSelection() ) | ||
2146 | { | ||
2147 | deleteSelection(FALSE); | ||
2148 | } | ||
2149 | |||
2150 | S32 line, offset; | ||
2151 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
2152 | |||
2153 | S32 spaces_needed = SPACES_PER_TAB - (offset % SPACES_PER_TAB); | ||
2154 | for( S32 i=0; i < spaces_needed; i++ ) | ||
2155 | { | ||
2156 | addChar( ' ' ); | ||
2157 | } | ||
2158 | } | ||
2159 | break; | ||
2160 | |||
2161 | default: | ||
2162 | handled = FALSE; | ||
2163 | break; | ||
2164 | } | ||
2165 | |||
2166 | return handled; | ||
2167 | } | ||
2168 | |||
2169 | |||
2170 | void LLTextEditor::unindentLineBeforeCloseBrace() | ||
2171 | { | ||
2172 | if( mCursorPos >= 1 ) | ||
2173 | { | ||
2174 | const LLWString &text = mWText; | ||
2175 | if( ' ' == text[ mCursorPos - 1 ] ) | ||
2176 | { | ||
2177 | removeCharOrTab(); | ||
2178 | } | ||
2179 | } | ||
2180 | } | ||
2181 | |||
2182 | |||
2183 | BOOL LLTextEditor::handleKeyHere(KEY key, MASK mask, BOOL called_from_parent ) | ||
2184 | { | ||
2185 | BOOL handled = FALSE; | ||
2186 | BOOL selection_modified = FALSE; | ||
2187 | BOOL return_key_hit = FALSE; | ||
2188 | BOOL text_may_have_changed = TRUE; | ||
2189 | |||
2190 | if ( (gFocusMgr.getKeyboardFocus() == this) && getVisible()) | ||
2191 | { | ||
2192 | // Special case for TAB. If want to move to next field, report | ||
2193 | // not handled and let the parent take care of field movement. | ||
2194 | if (KEY_TAB == key && mTabToNextField) | ||
2195 | { | ||
2196 | return FALSE; | ||
2197 | } | ||
2198 | |||
2199 | handled = handleNavigationKey( key, mask ); | ||
2200 | if( handled ) | ||
2201 | { | ||
2202 | text_may_have_changed = FALSE; | ||
2203 | } | ||
2204 | |||
2205 | if( !handled ) | ||
2206 | { | ||
2207 | handled = handleSelectionKey( key, mask ); | ||
2208 | if( handled ) | ||
2209 | { | ||
2210 | selection_modified = TRUE; | ||
2211 | } | ||
2212 | } | ||
2213 | |||
2214 | if( !handled ) | ||
2215 | { | ||
2216 | handled = handleControlKey( key, mask ); | ||
2217 | if( handled ) | ||
2218 | { | ||
2219 | selection_modified = TRUE; | ||
2220 | } | ||
2221 | } | ||
2222 | |||
2223 | if( !handled && mHandleEditKeysDirectly ) | ||
2224 | { | ||
2225 | handled = handleEditKey( key, mask ); | ||
2226 | if( handled ) | ||
2227 | { | ||
2228 | selection_modified = TRUE; | ||
2229 | text_may_have_changed = TRUE; | ||
2230 | } | ||
2231 | } | ||
2232 | |||
2233 | // Handle most keys only if the text editor is writeable. | ||
2234 | if( !mReadOnly ) | ||
2235 | { | ||
2236 | if( !handled ) | ||
2237 | { | ||
2238 | handled = handleSpecialKey( key, mask, &return_key_hit ); | ||
2239 | if( handled ) | ||
2240 | { | ||
2241 | selection_modified = TRUE; | ||
2242 | text_may_have_changed = TRUE; | ||
2243 | } | ||
2244 | } | ||
2245 | |||
2246 | } | ||
2247 | |||
2248 | if( handled ) | ||
2249 | { | ||
2250 | mKeystrokeTimer.reset(); | ||
2251 | |||
2252 | // Most keystrokes will make the selection box go away, but not all will. | ||
2253 | if( !selection_modified && | ||
2254 | KEY_SHIFT != key && | ||
2255 | KEY_CONTROL != key && | ||
2256 | KEY_ALT != key && | ||
2257 | KEY_CAPSLOCK ) | ||
2258 | { | ||
2259 | deselect(); | ||
2260 | } | ||
2261 | |||
2262 | if(text_may_have_changed) | ||
2263 | { | ||
2264 | updateLineStartList(); | ||
2265 | } | ||
2266 | updateScrollFromCursor(); | ||
2267 | } | ||
2268 | } | ||
2269 | |||
2270 | return handled; | ||
2271 | } | ||
2272 | |||
2273 | |||
2274 | BOOL LLTextEditor::handleUnicodeCharHere(llwchar uni_char, BOOL called_from_parent) | ||
2275 | { | ||
2276 | if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL | ||
2277 | { | ||
2278 | return FALSE; | ||
2279 | } | ||
2280 | |||
2281 | BOOL handled = FALSE; | ||
2282 | |||
2283 | if ( (gFocusMgr.getKeyboardFocus() == this) && getVisible()) | ||
2284 | { | ||
2285 | // Handle most keys only if the text editor is writeable. | ||
2286 | if( !mReadOnly ) | ||
2287 | { | ||
2288 | if( '}' == uni_char ) | ||
2289 | { | ||
2290 | unindentLineBeforeCloseBrace(); | ||
2291 | } | ||
2292 | |||
2293 | // TODO: KLW Add auto show of tool tip on ( | ||
2294 | addChar( uni_char ); | ||
2295 | |||
2296 | // Keys that add characters temporarily hide the cursor | ||
2297 | getWindow()->hideCursorUntilMouseMove(); | ||
2298 | |||
2299 | handled = TRUE; | ||
2300 | } | ||
2301 | |||
2302 | if( handled ) | ||
2303 | { | ||
2304 | mKeystrokeTimer.reset(); | ||
2305 | |||
2306 | // Most keystrokes will make the selection box go away, but not all will. | ||
2307 | deselect(); | ||
2308 | |||
2309 | updateLineStartList(); | ||
2310 | updateScrollFromCursor(); | ||
2311 | } | ||
2312 | } | ||
2313 | |||
2314 | return handled; | ||
2315 | } | ||
2316 | |||
2317 | |||
2318 | |||
2319 | BOOL LLTextEditor::canDoDelete() | ||
2320 | { | ||
2321 | return !mReadOnly && ( hasSelection() || (mCursorPos < getLength()) ); | ||
2322 | } | ||
2323 | |||
2324 | void LLTextEditor::doDelete() | ||
2325 | { | ||
2326 | if( canDoDelete() ) | ||
2327 | { | ||
2328 | if( hasSelection() ) | ||
2329 | { | ||
2330 | deleteSelection(FALSE); | ||
2331 | } | ||
2332 | else | ||
2333 | if( mCursorPos < getLength() ) | ||
2334 | { | ||
2335 | S32 i; | ||
2336 | S32 chars_to_remove = 1; | ||
2337 | const LLWString &text = mWText; | ||
2338 | if( (text[ mCursorPos ] == ' ') && (mCursorPos + SPACES_PER_TAB < getLength()) ) | ||
2339 | { | ||
2340 | // Try to remove a full tab's worth of spaces | ||
2341 | S32 line, offset; | ||
2342 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
2343 | chars_to_remove = SPACES_PER_TAB - (offset % SPACES_PER_TAB); | ||
2344 | if( chars_to_remove == 0 ) | ||
2345 | { | ||
2346 | chars_to_remove = SPACES_PER_TAB; | ||
2347 | } | ||
2348 | |||
2349 | for( i = 0; i < chars_to_remove; i++ ) | ||
2350 | { | ||
2351 | if( text[mCursorPos + i] != ' ' ) | ||
2352 | { | ||
2353 | chars_to_remove = 1; | ||
2354 | break; | ||
2355 | } | ||
2356 | } | ||
2357 | } | ||
2358 | |||
2359 | |||
2360 | for( i = 0; i < chars_to_remove; i++ ) | ||
2361 | { | ||
2362 | setCursorPos(mCursorPos + 1); | ||
2363 | removeChar(); | ||
2364 | } | ||
2365 | } | ||
2366 | |||
2367 | updateLineStartList(); | ||
2368 | updateScrollFromCursor(); | ||
2369 | } | ||
2370 | } | ||
2371 | |||
2372 | //---------------------------------------------------------------------------- | ||
2373 | |||
2374 | |||
2375 | void LLTextEditor::blockUndo() | ||
2376 | { | ||
2377 | mBaseDocIsPristine = FALSE; | ||
2378 | mLastCmd = NULL; | ||
2379 | std::for_each(mUndoStack.begin(), mUndoStack.end(), DeletePointer()); | ||
2380 | mUndoStack.clear(); | ||
2381 | } | ||
2382 | |||
2383 | |||
2384 | BOOL LLTextEditor::canUndo() | ||
2385 | { | ||
2386 | return !mReadOnly && mLastCmd != NULL; | ||
2387 | } | ||
2388 | |||
2389 | void LLTextEditor::undo() | ||
2390 | { | ||
2391 | if( canUndo() ) | ||
2392 | { | ||
2393 | deselect(); | ||
2394 | |||
2395 | S32 pos = 0; | ||
2396 | do | ||
2397 | { | ||
2398 | pos = mLastCmd->undo(this); | ||
2399 | undo_stack_t::iterator iter = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd); | ||
2400 | if (iter != mUndoStack.end()) | ||
2401 | ++iter; | ||
2402 | if (iter != mUndoStack.end()) | ||
2403 | mLastCmd = *iter; | ||
2404 | else | ||
2405 | mLastCmd = NULL; | ||
2406 | |||
2407 | } while( mLastCmd && mLastCmd->groupWithNext() ); | ||
2408 | |||
2409 | setCursorPos(pos); | ||
2410 | |||
2411 | updateLineStartList(); | ||
2412 | updateScrollFromCursor(); | ||
2413 | } | ||
2414 | } | ||
2415 | |||
2416 | BOOL LLTextEditor::canRedo() | ||
2417 | { | ||
2418 | return !mReadOnly && (mUndoStack.size() > 0) && (mLastCmd != mUndoStack.front()); | ||
2419 | } | ||
2420 | |||
2421 | void LLTextEditor::redo() | ||
2422 | { | ||
2423 | if( canRedo() ) | ||
2424 | { | ||
2425 | deselect(); | ||
2426 | |||
2427 | S32 pos = 0; | ||
2428 | do | ||
2429 | { | ||
2430 | if( !mLastCmd ) | ||
2431 | { | ||
2432 | mLastCmd = mUndoStack.back(); | ||
2433 | } | ||
2434 | else | ||
2435 | { | ||
2436 | undo_stack_t::iterator iter = std::find(mUndoStack.begin(), mUndoStack.end(), mLastCmd); | ||
2437 | if (iter != mUndoStack.begin()) | ||
2438 | mLastCmd = *(--iter); | ||
2439 | else | ||
2440 | mLastCmd = NULL; | ||
2441 | } | ||
2442 | |||
2443 | if( mLastCmd ) | ||
2444 | { | ||
2445 | pos = mLastCmd->redo(this); | ||
2446 | } | ||
2447 | } while( | ||
2448 | mLastCmd && | ||
2449 | mLastCmd->groupWithNext() && | ||
2450 | (mLastCmd != mUndoStack.front()) ); | ||
2451 | |||
2452 | setCursorPos(pos); | ||
2453 | |||
2454 | updateLineStartList(); | ||
2455 | updateScrollFromCursor(); | ||
2456 | } | ||
2457 | } | ||
2458 | |||
2459 | |||
2460 | // virtual, from LLView | ||
2461 | void LLTextEditor::onFocusLost() | ||
2462 | { | ||
2463 | // Route menu back to the default | ||
2464 | if( gEditMenuHandler == this ) | ||
2465 | { | ||
2466 | gEditMenuHandler = NULL; | ||
2467 | } | ||
2468 | |||
2469 | if (mCommitOnFocusLost) | ||
2470 | { | ||
2471 | onCommit(); | ||
2472 | } | ||
2473 | |||
2474 | // Make sure cursor is shown again | ||
2475 | getWindow()->showCursorFromMouseMove(); | ||
2476 | } | ||
2477 | |||
2478 | void LLTextEditor::setEnabled(BOOL enabled) | ||
2479 | { | ||
2480 | // just treat enabled as read-only flag | ||
2481 | BOOL read_only = !enabled; | ||
2482 | if (read_only != mReadOnly) | ||
2483 | { | ||
2484 | mReadOnly = read_only; | ||
2485 | updateSegments(); | ||
2486 | } | ||
2487 | } | ||
2488 | |||
2489 | void LLTextEditor::drawBackground() | ||
2490 | { | ||
2491 | S32 left = 0; | ||
2492 | S32 top = mRect.getHeight(); | ||
2493 | S32 right = mRect.getWidth(); | ||
2494 | S32 bottom = 0; | ||
2495 | |||
2496 | LLColor4 bg_color = mReadOnlyBgColor; | ||
2497 | |||
2498 | if( !mReadOnly ) | ||
2499 | { | ||
2500 | if (gFocusMgr.getKeyboardFocus() == this) | ||
2501 | { | ||
2502 | bg_color = mFocusBgColor; | ||
2503 | } | ||
2504 | else | ||
2505 | { | ||
2506 | bg_color = mWriteableBgColor; | ||
2507 | } | ||
2508 | } | ||
2509 | gl_rect_2d(left, top, right, bottom, bg_color); | ||
2510 | |||
2511 | LLView::draw(); | ||
2512 | } | ||
2513 | |||
2514 | // Draws the black box behind the selected text | ||
2515 | void LLTextEditor::drawSelectionBackground() | ||
2516 | { | ||
2517 | // Draw selection even if we don't have keyboard focus for search/replace | ||
2518 | if( hasSelection() ) | ||
2519 | { | ||
2520 | const LLWString &text = mWText; | ||
2521 | const S32 text_len = getLength(); | ||
2522 | std::queue<S32> line_endings; | ||
2523 | |||
2524 | S32 line_height = llround( mGLFont->getLineHeight() ); | ||
2525 | |||
2526 | S32 selection_left = llmin( mSelectionStart, mSelectionEnd ); | ||
2527 | S32 selection_right = llmax( mSelectionStart, mSelectionEnd ); | ||
2528 | S32 selection_left_x = mTextRect.mLeft; | ||
2529 | S32 selection_left_y = mTextRect.mTop - line_height; | ||
2530 | S32 selection_right_x = mTextRect.mRight; | ||
2531 | S32 selection_right_y = mTextRect.mBottom; | ||
2532 | |||
2533 | BOOL selection_left_visible = FALSE; | ||
2534 | BOOL selection_right_visible = FALSE; | ||
2535 | |||
2536 | // Skip through the lines we aren't drawing. | ||
2537 | S32 cur_line = mScrollbar->getDocPos(); | ||
2538 | |||
2539 | S32 left_line_num = cur_line; | ||
2540 | S32 num_lines = getLineCount(); | ||
2541 | S32 right_line_num = num_lines - 1; | ||
2542 | |||
2543 | S32 line_start = -1; | ||
2544 | if (cur_line >= num_lines) | ||
2545 | { | ||
2546 | return; | ||
2547 | } | ||
2548 | |||
2549 | line_start = getLineStart(cur_line); | ||
2550 | |||
2551 | S32 left_visible_pos = line_start; | ||
2552 | S32 right_visible_pos = line_start; | ||
2553 | |||
2554 | S32 text_y = mTextRect.mTop - line_height; | ||
2555 | |||
2556 | // Find the coordinates of the selected area | ||
2557 | while((cur_line < num_lines)) | ||
2558 | { | ||
2559 | S32 next_line = -1; | ||
2560 | S32 line_end = text_len; | ||
2561 | |||
2562 | if ((cur_line + 1) < num_lines) | ||
2563 | { | ||
2564 | next_line = getLineStart(cur_line + 1); | ||
2565 | line_end = next_line; | ||
2566 | |||
2567 | line_end = ( (line_end - line_start)==0 || text[next_line-1] == '\n' || text[next_line-1] == '\0' || text[next_line-1] == ' ' || text[next_line-1] == '\t' ) ? next_line-1 : next_line; | ||
2568 | } | ||
2569 | |||
2570 | const llwchar* line = text.c_str() + line_start; | ||
2571 | |||
2572 | if( line_start <= selection_left && selection_left <= line_end ) | ||
2573 | { | ||
2574 | left_line_num = cur_line; | ||
2575 | selection_left_visible = TRUE; | ||
2576 | selection_left_x = mTextRect.mLeft + mGLFont->getWidth(line, 0, selection_left - line_start, mAllowEmbeddedItems); | ||
2577 | selection_left_y = text_y; | ||
2578 | } | ||
2579 | if( line_start <= selection_right && selection_right <= line_end ) | ||
2580 | { | ||
2581 | right_line_num = cur_line; | ||
2582 | selection_right_visible = TRUE; | ||
2583 | selection_right_x = mTextRect.mLeft + mGLFont->getWidth(line, 0, selection_right - line_start, mAllowEmbeddedItems); | ||
2584 | if (selection_right == line_end) | ||
2585 | { | ||
2586 | // add empty space for "newline" | ||
2587 | //selection_right_x += mGLFont->getWidth("n"); | ||
2588 | } | ||
2589 | selection_right_y = text_y; | ||
2590 | } | ||
2591 | |||
2592 | // if selection spans end of current line... | ||
2593 | if (selection_left <= line_end && line_end < selection_right && selection_left != selection_right) | ||
2594 | { | ||
2595 | // extend selection slightly beyond end of line | ||
2596 | // to indicate selection of newline character (use "n" character to determine width) | ||
2597 | const LLWString nstr(utf8str_to_wstring(LLString("n"))); | ||
2598 | line_endings.push(mTextRect.mLeft + mGLFont->getWidth(line, 0, line_end - line_start, mAllowEmbeddedItems) + mGLFont->getWidth(nstr.c_str())); | ||
2599 | } | ||
2600 | |||
2601 | // move down one line | ||
2602 | text_y -= line_height; | ||
2603 | |||
2604 | right_visible_pos = line_end; | ||
2605 | line_start = next_line; | ||
2606 | cur_line++; | ||
2607 | |||
2608 | if (selection_right_visible) | ||
2609 | { | ||
2610 | break; | ||
2611 | } | ||
2612 | } | ||
2613 | |||
2614 | // Draw the selection box (we're using a box instead of reversing the colors on the selected text). | ||
2615 | BOOL selection_visible = (left_visible_pos <= selection_right) && (selection_left <= right_visible_pos); | ||
2616 | if( selection_visible ) | ||
2617 | { | ||
2618 | LLGLSNoTexture no_texture; | ||
2619 | const LLColor4& color = mReadOnly ? mReadOnlyBgColor : mWriteableBgColor; | ||
2620 | glColor3f( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2] ); | ||
2621 | |||
2622 | if( selection_left_y == selection_right_y ) | ||
2623 | { | ||
2624 | // Draw from selection start to selection end | ||
2625 | gl_rect_2d( selection_left_x, selection_left_y + line_height + 1, | ||
2626 | selection_right_x, selection_right_y); | ||
2627 | } | ||
2628 | else | ||
2629 | { | ||
2630 | // Draw from selection start to the end of the first line | ||
2631 | if( mTextRect.mRight == selection_left_x ) | ||
2632 | { | ||
2633 | selection_left_x -= CURSOR_THICKNESS; | ||
2634 | } | ||
2635 | |||
2636 | S32 line_end = line_endings.front(); | ||
2637 | line_endings.pop(); | ||
2638 | gl_rect_2d( selection_left_x, selection_left_y + line_height + 1, | ||
2639 | line_end, selection_left_y ); | ||
2640 | |||
2641 | S32 line_num = left_line_num + 1; | ||
2642 | while(line_endings.size()) | ||
2643 | { | ||
2644 | S32 vert_offset = -(line_num - left_line_num) * line_height; | ||
2645 | // Draw the block between the two lines | ||
2646 | gl_rect_2d( mTextRect.mLeft, selection_left_y + vert_offset + line_height + 1, | ||
2647 | line_endings.front(), selection_left_y + vert_offset); | ||
2648 | line_endings.pop(); | ||
2649 | line_num++; | ||
2650 | } | ||
2651 | |||
2652 | // Draw from the start of the last line to selection end | ||
2653 | if( mTextRect.mLeft == selection_right_x ) | ||
2654 | { | ||
2655 | selection_right_x += CURSOR_THICKNESS; | ||
2656 | } | ||
2657 | gl_rect_2d( mTextRect.mLeft, selection_right_y + line_height + 1, | ||
2658 | selection_right_x, selection_right_y ); | ||
2659 | } | ||
2660 | } | ||
2661 | } | ||
2662 | } | ||
2663 | |||
2664 | void LLTextEditor::drawCursor() | ||
2665 | { | ||
2666 | if( gFocusMgr.getKeyboardFocus() == this | ||
2667 | && gShowTextEditCursor && !mReadOnly) | ||
2668 | { | ||
2669 | const LLWString &text = mWText; | ||
2670 | const S32 text_len = getLength(); | ||
2671 | |||
2672 | // Skip through the lines we aren't drawing. | ||
2673 | S32 cur_pos = mScrollbar->getDocPos(); | ||
2674 | |||
2675 | S32 num_lines = getLineCount(); | ||
2676 | if (cur_pos >= num_lines) | ||
2677 | { | ||
2678 | return; | ||
2679 | } | ||
2680 | S32 line_start = getLineStart(cur_pos); | ||
2681 | |||
2682 | F32 line_height = mGLFont->getLineHeight(); | ||
2683 | F32 text_y = (F32)(mTextRect.mTop) - line_height; | ||
2684 | |||
2685 | F32 cursor_left = 0.f; | ||
2686 | F32 next_char_left = 0.f; | ||
2687 | F32 cursor_bottom = 0.f; | ||
2688 | BOOL cursor_visible = FALSE; | ||
2689 | |||
2690 | S32 line_end = 0; | ||
2691 | // Determine if the cursor is visible and if so what its coordinates are. | ||
2692 | while( (mTextRect.mBottom <= llround(text_y)) && (cur_pos < num_lines)) | ||
2693 | { | ||
2694 | line_end = text_len + 1; | ||
2695 | S32 next_line = -1; | ||
2696 | |||
2697 | if ((cur_pos + 1) < num_lines) | ||
2698 | { | ||
2699 | next_line = getLineStart(cur_pos + 1); | ||
2700 | line_end = next_line - 1; | ||
2701 | } | ||
2702 | |||
2703 | const llwchar* line = text.c_str() + line_start; | ||
2704 | |||
2705 | // Find the cursor and selection bounds | ||
2706 | if( line_start <= mCursorPos && mCursorPos <= line_end ) | ||
2707 | { | ||
2708 | cursor_visible = TRUE; | ||
2709 | next_char_left = (F32)mTextRect.mLeft + mGLFont->getWidthF32(line, 0, mCursorPos - line_start, mAllowEmbeddedItems ); | ||
2710 | cursor_left = next_char_left - 1.f; | ||
2711 | cursor_bottom = text_y; | ||
2712 | break; | ||
2713 | } | ||
2714 | |||
2715 | // move down one line | ||
2716 | text_y -= line_height; | ||
2717 | line_start = next_line; | ||
2718 | cur_pos++; | ||
2719 | } | ||
2720 | |||
2721 | // Draw the cursor | ||
2722 | if( cursor_visible ) | ||
2723 | { | ||
2724 | // (Flash the cursor every half second starting a fixed time after the last keystroke) | ||
2725 | F32 elapsed = mKeystrokeTimer.getElapsedTimeF32(); | ||
2726 | if( (elapsed < CURSOR_FLASH_DELAY ) || (S32(elapsed * 2) & 1) ) | ||
2727 | { | ||
2728 | F32 cursor_top = cursor_bottom + line_height + 1.f; | ||
2729 | F32 cursor_right = cursor_left + (F32)CURSOR_THICKNESS; | ||
2730 | if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection()) | ||
2731 | { | ||
2732 | cursor_left += CURSOR_THICKNESS; | ||
2733 | const LLWString space(utf8str_to_wstring(LLString(" "))); | ||
2734 | F32 spacew = mGLFont->getWidthF32(space.c_str()); | ||
2735 | if (mCursorPos == line_end) | ||
2736 | { | ||
2737 | cursor_right = cursor_left + spacew; | ||
2738 | } | ||
2739 | else | ||
2740 | { | ||
2741 | F32 width = mGLFont->getWidthF32(text.c_str(), mCursorPos, 1, mAllowEmbeddedItems); | ||
2742 | cursor_right = cursor_left + llmax(spacew, width); | ||
2743 | } | ||
2744 | } | ||
2745 | |||
2746 | LLGLSNoTexture no_texture; | ||
2747 | |||
2748 | glColor4fv( mCursorColor.mV ); | ||
2749 | |||
2750 | gl_rect_2d(llfloor(cursor_left), llfloor(cursor_top), | ||
2751 | llfloor(cursor_right), llfloor(cursor_bottom)); | ||
2752 | |||
2753 | if (LL_KIM_OVERWRITE == gKeyboard->getInsertMode() && !hasSelection() && text[mCursorPos] != '\n') | ||
2754 | { | ||
2755 | LLTextSegment* segmentp = getSegmentAtOffset(mCursorPos); | ||
2756 | LLColor4 text_color; | ||
2757 | if (segmentp) | ||
2758 | { | ||
2759 | text_color = segmentp->getColor(); | ||
2760 | } | ||
2761 | else if (mReadOnly) | ||
2762 | { | ||
2763 | text_color = mReadOnlyFgColor; | ||
2764 | } | ||
2765 | else | ||
2766 | { | ||
2767 | text_color = mFgColor; | ||
2768 | } | ||
2769 | LLGLSTexture texture; | ||
2770 | mGLFont->render(text, mCursorPos, next_char_left, cursor_bottom + line_height, | ||
2771 | LLColor4(1.f - text_color.mV[VRED], 1.f - text_color.mV[VGREEN], 1.f - text_color.mV[VBLUE], 1.f), | ||
2772 | LLFontGL::LEFT, LLFontGL::TOP, | ||
2773 | LLFontGL::NORMAL, | ||
2774 | 1); | ||
2775 | } | ||
2776 | |||
2777 | |||
2778 | } | ||
2779 | } | ||
2780 | } | ||
2781 | } | ||
2782 | |||
2783 | |||
2784 | void LLTextEditor::drawText() | ||
2785 | { | ||
2786 | const LLWString &text = mWText; | ||
2787 | const S32 text_len = getLength(); | ||
2788 | |||
2789 | if( text_len > 0 ) | ||
2790 | { | ||
2791 | S32 selection_left = -1; | ||
2792 | S32 selection_right = -1; | ||
2793 | // Draw selection even if we don't have keyboard focus for search/replace | ||
2794 | if( hasSelection()) | ||
2795 | { | ||
2796 | selection_left = llmin( mSelectionStart, mSelectionEnd ); | ||
2797 | selection_right = llmax( mSelectionStart, mSelectionEnd ); | ||
2798 | } | ||
2799 | |||
2800 | LLGLSUIDefault gls_ui; | ||
2801 | |||
2802 | S32 cur_line = mScrollbar->getDocPos(); | ||
2803 | S32 num_lines = getLineCount(); | ||
2804 | if (cur_line >= num_lines) | ||
2805 | { | ||
2806 | return; | ||
2807 | } | ||
2808 | |||
2809 | S32 line_start = getLineStart(cur_line); | ||
2810 | LLTextSegment t(line_start); | ||
2811 | segment_list_t::iterator seg_iter; | ||
2812 | seg_iter = std::upper_bound(mSegments.begin(), mSegments.end(), &t, LLTextSegment::compare()); | ||
2813 | if (seg_iter == mSegments.end() || (*seg_iter)->getStart() > line_start) --seg_iter; | ||
2814 | LLTextSegment* cur_segment = *seg_iter; | ||
2815 | |||
2816 | S32 line_height = llround( mGLFont->getLineHeight() ); | ||
2817 | F32 text_y = (F32)(mTextRect.mTop - line_height); | ||
2818 | while((mTextRect.mBottom <= text_y) && (cur_line < num_lines)) | ||
2819 | { | ||
2820 | S32 next_start = -1; | ||
2821 | S32 line_end = text_len; | ||
2822 | |||
2823 | if ((cur_line + 1) < num_lines) | ||
2824 | { | ||
2825 | next_start = getLineStart(cur_line + 1); | ||
2826 | line_end = next_start; | ||
2827 | } | ||
2828 | if ( text[line_end-1] == '\n' ) | ||
2829 | { | ||
2830 | --line_end; | ||
2831 | } | ||
2832 | |||
2833 | F32 text_x = (F32)mTextRect.mLeft; | ||
2834 | |||
2835 | S32 seg_start = line_start; | ||
2836 | while( seg_start < line_end ) | ||
2837 | { | ||
2838 | while( cur_segment->getEnd() <= seg_start ) | ||
2839 | { | ||
2840 | seg_iter++; | ||
2841 | if (seg_iter == mSegments.end()) | ||
2842 | { | ||
2843 | llwarns << "Ran off the segmentation end!" << llendl; | ||
2844 | return; | ||
2845 | } | ||
2846 | cur_segment = *seg_iter; | ||
2847 | } | ||
2848 | |||
2849 | // Draw a segment within the line | ||
2850 | S32 clipped_end = llmin( line_end, cur_segment->getEnd() ); | ||
2851 | S32 clipped_len = clipped_end - seg_start; | ||
2852 | if( clipped_len > 0 ) | ||
2853 | { | ||
2854 | LLStyle style = cur_segment->getStyle(); | ||
2855 | if ( style.isImage() && (cur_segment->getStart() >= seg_start) && (cur_segment->getStart() <= clipped_end)) | ||
2856 | { | ||
2857 | LLImageGL *image = style.getImage(); | ||
2858 | |||
2859 | gl_draw_scaled_image( llround(text_x), llround(text_y)+line_height-style.mImageHeight, style.mImageWidth, style.mImageHeight, image, LLColor4::white ); | ||
2860 | |||
2861 | } | ||
2862 | |||
2863 | if (cur_segment == mHoverSegment && style.getIsEmbeddedItem()) | ||
2864 | { | ||
2865 | style.mUnderline = TRUE; | ||
2866 | } | ||
2867 | |||
2868 | S32 left_pos = llmin( mSelectionStart, mSelectionEnd ); | ||
2869 | |||
2870 | if ( (mParseHTML) && (left_pos > seg_start) && (left_pos < clipped_end) && mIsSelecting && (mSelectionStart == mSelectionEnd) ) | ||
2871 | { | ||
2872 | mHTML = style.getLinkHREF(); | ||
2873 | } | ||
2874 | |||
2875 | drawClippedSegment( text, seg_start, clipped_end, text_x, text_y, selection_left, selection_right, style, &text_x ); | ||
2876 | |||
2877 | // Note: text_x is incremented by drawClippedSegment() | ||
2878 | seg_start += clipped_len; | ||
2879 | } | ||
2880 | } | ||
2881 | |||
2882 | // move down one line | ||
2883 | text_y -= (F32)line_height; | ||
2884 | |||
2885 | line_start = next_start; | ||
2886 | cur_line++; | ||
2887 | } | ||
2888 | } | ||
2889 | } | ||
2890 | |||
2891 | // Draws a single text segment, reversing the color for selection if needed. | ||
2892 | void LLTextEditor::drawClippedSegment(const LLWString &text, S32 seg_start, S32 seg_end, F32 x, F32 y, S32 selection_left, S32 selection_right, const LLStyle& style, F32* right_x ) | ||
2893 | { | ||
2894 | const LLFontGL* font = mGLFont; | ||
2895 | |||
2896 | LLColor4 color; | ||
2897 | |||
2898 | if (!style.isVisible()) | ||
2899 | { | ||
2900 | return; | ||
2901 | } | ||
2902 | |||
2903 | color = style.getColor(); | ||
2904 | |||
2905 | if ( style.getFontString()[0] ) | ||
2906 | { | ||
2907 | font = gResMgr->getRes(style.getFontID()); | ||
2908 | } | ||
2909 | |||
2910 | U8 font_flags = LLFontGL::NORMAL; | ||
2911 | |||
2912 | if (style.mBold) | ||
2913 | { | ||
2914 | font_flags |= LLFontGL::BOLD; | ||
2915 | } | ||
2916 | if (style.mItalic) | ||
2917 | { | ||
2918 | font_flags |= LLFontGL::ITALIC; | ||
2919 | } | ||
2920 | if (style.mUnderline) | ||
2921 | { | ||
2922 | font_flags |= LLFontGL::UNDERLINE; | ||
2923 | } | ||
2924 | |||
2925 | if (style.getIsEmbeddedItem()) | ||
2926 | { | ||
2927 | if (mReadOnly) | ||
2928 | { | ||
2929 | color = LLUI::sColorsGroup->getColor("TextEmbeddedItemReadOnlyColor"); | ||
2930 | } | ||
2931 | else | ||
2932 | { | ||
2933 | color = LLUI::sColorsGroup->getColor("TextEmbeddedItemColor"); | ||
2934 | } | ||
2935 | } | ||
2936 | |||
2937 | F32 y_top = y + (F32)llround(font->getLineHeight()); | ||
2938 | |||
2939 | if( selection_left > seg_start ) | ||
2940 | { | ||
2941 | // Draw normally | ||
2942 | S32 start = seg_start; | ||
2943 | S32 end = llmin( selection_left, seg_end ); | ||
2944 | S32 length = end - start; | ||
2945 | font->render(text, start, x, y_top, color, LLFontGL::LEFT, LLFontGL::TOP, font_flags, length, S32_MAX, right_x, mAllowEmbeddedItems); | ||
2946 | } | ||
2947 | x = *right_x; | ||
2948 | |||
2949 | if( (selection_left < seg_end) && (selection_right > seg_start) ) | ||
2950 | { | ||
2951 | // Draw reversed | ||
2952 | S32 start = llmax( selection_left, seg_start ); | ||
2953 | S32 end = llmin( selection_right, seg_end ); | ||
2954 | S32 length = end - start; | ||
2955 | |||
2956 | font->render(text, start, x, y_top, | ||
2957 | LLColor4( 1.f - color.mV[0], 1.f - color.mV[1], 1.f - color.mV[2], 1.f ), | ||
2958 | LLFontGL::LEFT, LLFontGL::TOP, font_flags, length, S32_MAX, right_x, mAllowEmbeddedItems); | ||
2959 | } | ||
2960 | x = *right_x; | ||
2961 | if( selection_right < seg_end ) | ||
2962 | { | ||
2963 | // Draw normally | ||
2964 | S32 start = llmax( selection_right, seg_start ); | ||
2965 | S32 end = seg_end; | ||
2966 | S32 length = end - start; | ||
2967 | font->render(text, start, x, y_top, color, LLFontGL::LEFT, LLFontGL::TOP, font_flags, length, S32_MAX, right_x, mAllowEmbeddedItems); | ||
2968 | } | ||
2969 | } | ||
2970 | |||
2971 | |||
2972 | void LLTextEditor::draw() | ||
2973 | { | ||
2974 | if( getVisible() ) | ||
2975 | { | ||
2976 | { | ||
2977 | LLGLEnable scissor_test(GL_SCISSOR_TEST); | ||
2978 | LLUI::setScissorRegionLocal(LLRect(0, mRect.getHeight(), mRect.getWidth() - (mScrollbar->getVisible() ? SCROLLBAR_SIZE : 0), 0)); | ||
2979 | |||
2980 | bindEmbeddedChars( mGLFont ); | ||
2981 | |||
2982 | drawBackground(); | ||
2983 | drawSelectionBackground(); | ||
2984 | drawText(); | ||
2985 | drawCursor(); | ||
2986 | |||
2987 | unbindEmbeddedChars( mGLFont ); | ||
2988 | |||
2989 | //RN: the decision was made to always show the orange border for keyboard focus but do not put an insertion caret | ||
2990 | // when in readonly mode | ||
2991 | mBorder->setKeyboardFocusHighlight( gFocusMgr.getKeyboardFocus() == this);// && !mReadOnly); | ||
2992 | } | ||
2993 | LLView::draw(); // Draw children (scrollbar and border) | ||
2994 | } | ||
2995 | } | ||
2996 | |||
2997 | void LLTextEditor::reportBadKeystroke() | ||
2998 | { | ||
2999 | make_ui_sound("UISndBadKeystroke"); | ||
3000 | } | ||
3001 | |||
3002 | |||
3003 | void LLTextEditor::onTabInto() | ||
3004 | { | ||
3005 | // selecting all on tabInto causes users to hit tab twice and replace their text with a tab character | ||
3006 | // theoretically, one could selectAll if mTabToNextField is true, but we couldn't think of a use case | ||
3007 | // where you'd want to select all anyway | ||
3008 | // preserve insertion point when returning to the editor | ||
3009 | //selectAll(); | ||
3010 | } | ||
3011 | |||
3012 | void LLTextEditor::clear() | ||
3013 | { | ||
3014 | setText(""); | ||
3015 | } | ||
3016 | |||
3017 | // Start or stop the editor from accepting text-editing keystrokes | ||
3018 | // see also LLLineEditor | ||
3019 | void LLTextEditor::setFocus( BOOL new_state ) | ||
3020 | { | ||
3021 | BOOL old_state = hasFocus(); | ||
3022 | |||
3023 | // Don't change anything if the focus state didn't change | ||
3024 | if (new_state == old_state) return; | ||
3025 | |||
3026 | LLUICtrl::setFocus( new_state ); | ||
3027 | |||
3028 | if( new_state ) | ||
3029 | { | ||
3030 | // Route menu to this class | ||
3031 | gEditMenuHandler = this; | ||
3032 | |||
3033 | // Don't start the cursor flashing right away | ||
3034 | mKeystrokeTimer.reset(); | ||
3035 | } | ||
3036 | else | ||
3037 | { | ||
3038 | // Route menu back to the default | ||
3039 | if( gEditMenuHandler == this ) | ||
3040 | { | ||
3041 | gEditMenuHandler = NULL; | ||
3042 | } | ||
3043 | |||
3044 | endSelection(); | ||
3045 | } | ||
3046 | } | ||
3047 | |||
3048 | BOOL LLTextEditor::acceptsTextInput() const | ||
3049 | { | ||
3050 | return !mReadOnly; | ||
3051 | } | ||
3052 | |||
3053 | // Given a line (from the start of the doc) and an offset into the line, find the offset (pos) into text. | ||
3054 | S32 LLTextEditor::getPos( S32 line, S32 offset ) | ||
3055 | { | ||
3056 | S32 line_start = getLineStart(line); | ||
3057 | S32 next_start = getLineStart(line+1); | ||
3058 | if (next_start == line_start) | ||
3059 | { | ||
3060 | next_start = getLength() + 1; | ||
3061 | } | ||
3062 | S32 line_length = next_start - line_start - 1; | ||
3063 | line_length = llmax(line_length, 0); | ||
3064 | return line_start + llmin( offset, line_length ); | ||
3065 | } | ||
3066 | |||
3067 | |||
3068 | void LLTextEditor::changePage( S32 delta ) | ||
3069 | { | ||
3070 | S32 line, offset; | ||
3071 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
3072 | |||
3073 | // allow one line overlap | ||
3074 | S32 page_size = mScrollbar->getPageSize() - 1; | ||
3075 | if( delta == -1 ) | ||
3076 | { | ||
3077 | line = llmax( line - page_size, 0); | ||
3078 | setCursorPos(getPos( line, offset )); | ||
3079 | mScrollbar->setDocPos( mScrollbar->getDocPos() - page_size ); | ||
3080 | } | ||
3081 | else | ||
3082 | if( delta == 1 ) | ||
3083 | { | ||
3084 | setCursorPos(getPos( line + page_size, offset )); | ||
3085 | mScrollbar->setDocPos( mScrollbar->getDocPos() + page_size ); | ||
3086 | } | ||
3087 | if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) | ||
3088 | { | ||
3089 | mOnScrollEndCallback(mOnScrollEndData); | ||
3090 | } | ||
3091 | } | ||
3092 | |||
3093 | void LLTextEditor::changeLine( S32 delta ) | ||
3094 | { | ||
3095 | bindEmbeddedChars( mGLFont ); | ||
3096 | |||
3097 | S32 line, offset; | ||
3098 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
3099 | |||
3100 | S32 line_start = getLineStart(line); | ||
3101 | |||
3102 | S32 desired_x_pixel; | ||
3103 | |||
3104 | desired_x_pixel = mGLFont->getWidth(mWText.c_str(), line_start, offset, mAllowEmbeddedItems ); | ||
3105 | |||
3106 | S32 new_line = 0; | ||
3107 | if( (delta < 0) && (line > 0 ) ) | ||
3108 | { | ||
3109 | new_line = line - 1; | ||
3110 | } | ||
3111 | else | ||
3112 | if( (delta > 0) && (line < (getLineCount() - 1)) ) | ||
3113 | { | ||
3114 | new_line = line + 1; | ||
3115 | } | ||
3116 | else | ||
3117 | { | ||
3118 | unbindEmbeddedChars( mGLFont ); | ||
3119 | return; | ||
3120 | } | ||
3121 | |||
3122 | S32 num_lines = getLineCount(); | ||
3123 | S32 new_line_start = getLineStart(new_line); | ||
3124 | S32 new_line_end = getLength(); | ||
3125 | if (new_line + 1 < num_lines) | ||
3126 | { | ||
3127 | new_line_end = getLineStart(new_line + 1) - 1; | ||
3128 | } | ||
3129 | |||
3130 | S32 new_line_len = new_line_end - new_line_start; | ||
3131 | |||
3132 | S32 new_offset; | ||
3133 | new_offset = mGLFont->charFromPixelOffset(mWText.c_str(), new_line_start, | ||
3134 | (F32)desired_x_pixel, | ||
3135 | (F32)mTextRect.getWidth(), | ||
3136 | new_line_len, | ||
3137 | mAllowEmbeddedItems); | ||
3138 | |||
3139 | setCursorPos (getPos( new_line, new_offset )); | ||
3140 | unbindEmbeddedChars( mGLFont ); | ||
3141 | } | ||
3142 | |||
3143 | void LLTextEditor::startOfLine() | ||
3144 | { | ||
3145 | S32 line, offset; | ||
3146 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
3147 | setCursorPos(mCursorPos - offset); | ||
3148 | } | ||
3149 | |||
3150 | |||
3151 | // public | ||
3152 | void LLTextEditor::setCursorAndScrollToEnd() | ||
3153 | { | ||
3154 | deselect(); | ||
3155 | endOfDoc(); | ||
3156 | updateScrollFromCursor(); | ||
3157 | } | ||
3158 | |||
3159 | |||
3160 | void LLTextEditor::getCurrentLineAndColumn( S32* line, S32* col, BOOL include_wordwrap ) | ||
3161 | { | ||
3162 | if( include_wordwrap ) | ||
3163 | { | ||
3164 | getLineAndOffset( mCursorPos, line, col ); | ||
3165 | } | ||
3166 | else | ||
3167 | { | ||
3168 | const LLWString &text = mWText; | ||
3169 | S32 line_count = 0; | ||
3170 | S32 line_start = 0; | ||
3171 | S32 i; | ||
3172 | for( i = 0; text[i] && (i < mCursorPos); i++ ) | ||
3173 | { | ||
3174 | if( '\n' == text[i] ) | ||
3175 | { | ||
3176 | line_start = i + 1; | ||
3177 | line_count++; | ||
3178 | } | ||
3179 | } | ||
3180 | *line = line_count; | ||
3181 | *col = i - line_start; | ||
3182 | } | ||
3183 | } | ||
3184 | |||
3185 | |||
3186 | void LLTextEditor::endOfLine() | ||
3187 | { | ||
3188 | S32 line, offset; | ||
3189 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
3190 | S32 num_lines = getLineCount(); | ||
3191 | if (line + 1 >= num_lines) | ||
3192 | { | ||
3193 | setCursorPos(getLength()); | ||
3194 | } | ||
3195 | else | ||
3196 | { | ||
3197 | setCursorPos( getLineStart(line + 1) - 1 ); | ||
3198 | } | ||
3199 | } | ||
3200 | |||
3201 | void LLTextEditor::endOfDoc() | ||
3202 | { | ||
3203 | mScrollbar->setDocPos( mScrollbar->getDocPosMax() ); | ||
3204 | S32 len = getLength(); | ||
3205 | if( len ) | ||
3206 | { | ||
3207 | setCursorPos(len); | ||
3208 | } | ||
3209 | if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) | ||
3210 | { | ||
3211 | mOnScrollEndCallback(mOnScrollEndData); | ||
3212 | } | ||
3213 | } | ||
3214 | |||
3215 | // Sets the scrollbar from the cursor position | ||
3216 | void LLTextEditor::updateScrollFromCursor() | ||
3217 | { | ||
3218 | mScrollbar->setDocSize( getLineCount() ); | ||
3219 | |||
3220 | if (mReadOnly) | ||
3221 | { | ||
3222 | // no cursor in read only mode | ||
3223 | return; | ||
3224 | } | ||
3225 | |||
3226 | S32 line, offset; | ||
3227 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
3228 | |||
3229 | S32 page_size = mScrollbar->getPageSize(); | ||
3230 | |||
3231 | if( line < mScrollbar->getDocPos() ) | ||
3232 | { | ||
3233 | // scroll so that the cursor is at the top of the page | ||
3234 | mScrollbar->setDocPos( line ); | ||
3235 | } | ||
3236 | else if( line >= mScrollbar->getDocPos() + page_size - 1 ) | ||
3237 | { | ||
3238 | S32 new_pos = 0; | ||
3239 | if( line < mScrollbar->getDocSize() - 1 ) | ||
3240 | { | ||
3241 | // scroll so that the cursor is one line above the bottom of the page, | ||
3242 | new_pos = line - page_size + 1; | ||
3243 | } | ||
3244 | else | ||
3245 | { | ||
3246 | // if there is less than a page of text remaining, scroll so that the cursor is at the bottom | ||
3247 | new_pos = mScrollbar->getDocPosMax(); | ||
3248 | } | ||
3249 | mScrollbar->setDocPos( new_pos ); | ||
3250 | } | ||
3251 | |||
3252 | // Check if we've scrolled to bottom for callback if asked for callback | ||
3253 | if (mOnScrollEndCallback && mOnScrollEndData && (mScrollbar->getDocPos() == mScrollbar->getDocPosMax())) | ||
3254 | { | ||
3255 | mOnScrollEndCallback(mOnScrollEndData); | ||
3256 | } | ||
3257 | } | ||
3258 | |||
3259 | void LLTextEditor::reshape(S32 width, S32 height, BOOL called_from_parent) | ||
3260 | { | ||
3261 | LLView::reshape( width, height, called_from_parent ); | ||
3262 | |||
3263 | updateTextRect(); | ||
3264 | |||
3265 | S32 line_height = llround( mGLFont->getLineHeight() ); | ||
3266 | S32 page_lines = mTextRect.getHeight() / line_height; | ||
3267 | mScrollbar->setPageSize( page_lines ); | ||
3268 | |||
3269 | updateLineStartList(); | ||
3270 | } | ||
3271 | |||
3272 | void LLTextEditor::autoIndent() | ||
3273 | { | ||
3274 | // Count the number of spaces in the current line | ||
3275 | S32 line, offset; | ||
3276 | getLineAndOffset( mCursorPos, &line, &offset ); | ||
3277 | S32 line_start = getLineStart(line); | ||
3278 | S32 space_count = 0; | ||
3279 | S32 i; | ||
3280 | |||
3281 | const LLWString &text = mWText; | ||
3282 | while( ' ' == text[line_start] ) | ||
3283 | { | ||
3284 | space_count++; | ||
3285 | line_start++; | ||
3286 | } | ||
3287 | |||
3288 | // If we're starting a braced section, indent one level. | ||
3289 | if( (mCursorPos > 0) && (text[mCursorPos -1] == '{') ) | ||
3290 | { | ||
3291 | space_count += SPACES_PER_TAB; | ||
3292 | } | ||
3293 | |||
3294 | // Insert that number of spaces on the new line | ||
3295 | addChar( '\n' ); | ||
3296 | for( i = 0; i < space_count; i++ ) | ||
3297 | { | ||
3298 | addChar( ' ' ); | ||
3299 | } | ||
3300 | } | ||
3301 | |||
3302 | // Inserts new text at the cursor position | ||
3303 | void LLTextEditor::insertText(const LLString &new_text) | ||
3304 | { | ||
3305 | BOOL enabled = getEnabled(); | ||
3306 | setEnabled( TRUE ); | ||
3307 | |||
3308 | // Delete any selected characters (the insertion replaces them) | ||
3309 | if( hasSelection() ) | ||
3310 | { | ||
3311 | deleteSelection(TRUE); | ||
3312 | } | ||
3313 | |||
3314 | setCursorPos(mCursorPos + insert( mCursorPos, utf8str_to_wstring(new_text), FALSE )); | ||
3315 | |||
3316 | updateLineStartList(); | ||
3317 | updateScrollFromCursor(); | ||
3318 | |||
3319 | setEnabled( enabled ); | ||
3320 | } | ||
3321 | |||
3322 | |||
3323 | void LLTextEditor::appendColoredText(const LLString &new_text, | ||
3324 | bool allow_undo, | ||
3325 | bool prepend_newline, | ||
3326 | const LLColor4 &color, | ||
3327 | const LLString& font_name) | ||
3328 | { | ||
3329 | LLStyle style; | ||
3330 | style.setVisible(true); | ||
3331 | style.setColor(color); | ||
3332 | style.setFontName(font_name); | ||
3333 | if(mParseHTML) | ||
3334 | { | ||
3335 | |||
3336 | S32 start=0,end=0; | ||
3337 | LLString text = new_text; | ||
3338 | while ( findHTML(text, &start, &end) ) | ||
3339 | { | ||
3340 | LLStyle html; | ||
3341 | html.setVisible(true); | ||
3342 | html.setColor(mLinkColor); | ||
3343 | html.setFontName(font_name); | ||
3344 | html.mUnderline = TRUE; | ||
3345 | |||
3346 | if (start > 0) appendText(text.substr(0,start),allow_undo, prepend_newline, &style); | ||
3347 | html.setLinkHREF(text.substr(start,end-start)); | ||
3348 | appendText(text.substr(start, end-start),allow_undo, prepend_newline, &html); | ||
3349 | if (end < (S32)text.length()) | ||
3350 | { | ||
3351 | text = text.substr(end,text.length() - end); | ||
3352 | end=0; | ||
3353 | } | ||
3354 | else | ||
3355 | { | ||
3356 | break; | ||
3357 | } | ||
3358 | } | ||
3359 | if (end < (S32)text.length()) appendText(text,allow_undo, prepend_newline, &style); | ||
3360 | } | ||
3361 | else | ||
3362 | { | ||
3363 | appendText(new_text, allow_undo, prepend_newline, &style); | ||
3364 | } | ||
3365 | } | ||
3366 | |||
3367 | void LLTextEditor::appendStyledText(const LLString &new_text, | ||
3368 | bool allow_undo, | ||
3369 | bool prepend_newline, | ||
3370 | const LLStyle &style) | ||
3371 | { | ||
3372 | appendText(new_text, allow_undo, prepend_newline, &style); | ||
3373 | } | ||
3374 | |||
3375 | // Appends new text to end of document | ||
3376 | void LLTextEditor::appendText(const LLString &new_text, bool allow_undo, bool prepend_newline, | ||
3377 | const LLStyle* segment_style) | ||
3378 | { | ||
3379 | // Save old state | ||
3380 | BOOL was_scrolled_to_bottom = (mScrollbar->getDocPos() == mScrollbar->getDocPosMax()); | ||
3381 | S32 selection_start = mSelectionStart; | ||
3382 | S32 selection_end = mSelectionEnd; | ||
3383 | S32 cursor_pos = mCursorPos; | ||
3384 | S32 old_length = getLength(); | ||
3385 | BOOL cursor_was_at_end = (mCursorPos == old_length); | ||
3386 | |||
3387 | deselect(); | ||
3388 | |||
3389 | setCursorPos(old_length); | ||
3390 | |||
3391 | // Add carriage return if not first line | ||
3392 | if (getLength() != 0 | ||
3393 | && prepend_newline) | ||
3394 | { | ||
3395 | LLString final_text = "\n"; | ||
3396 | final_text += new_text; | ||
3397 | append(utf8str_to_wstring(final_text), TRUE); | ||
3398 | } | ||
3399 | else | ||
3400 | { | ||
3401 | append(utf8str_to_wstring(new_text), TRUE ); | ||
3402 | } | ||
3403 | |||
3404 | if (segment_style) | ||
3405 | { | ||
3406 | S32 segment_start = old_length; | ||
3407 | S32 segment_end = getLength(); | ||
3408 | LLTextSegment* segment = new LLTextSegment(*segment_style, segment_start, segment_end ); | ||
3409 | mSegments.push_back(segment); | ||
3410 | } | ||
3411 | |||
3412 | updateLineStartList(old_length); | ||
3413 | |||
3414 | // Set the cursor and scroll position | ||
3415 | // Maintain the scroll position unless the scroll was at the end of the doc | ||
3416 | // (in which case, move it to the new end of the doc) | ||
3417 | if( was_scrolled_to_bottom ) | ||
3418 | { | ||
3419 | endOfDoc(); | ||
3420 | } | ||
3421 | else if( selection_start != selection_end ) | ||
3422 | { | ||
3423 | mSelectionStart = selection_start; | ||
3424 | |||
3425 | mSelectionEnd = selection_end; | ||
3426 | setCursorPos(cursor_pos); | ||
3427 | } | ||
3428 | else if( cursor_was_at_end ) | ||
3429 | { | ||
3430 | setCursorPos(getLength()); | ||
3431 | } | ||
3432 | else | ||
3433 | { | ||
3434 | setCursorPos(cursor_pos); | ||
3435 | } | ||
3436 | |||
3437 | if( !allow_undo ) | ||
3438 | { | ||
3439 | blockUndo(); | ||
3440 | } | ||
3441 | } | ||
3442 | |||
3443 | void LLTextEditor::removeTextFromEnd(S32 num_chars) | ||
3444 | { | ||
3445 | if (num_chars <= 0) return; | ||
3446 | |||
3447 | remove(getLength() - num_chars, num_chars, FALSE); | ||
3448 | |||
3449 | S32 len = getLength(); | ||
3450 | mCursorPos = llclamp(mCursorPos, 0, len); | ||
3451 | mSelectionStart = llclamp(mSelectionStart, 0, len); | ||
3452 | mSelectionEnd = llclamp(mSelectionEnd, 0, len); | ||
3453 | |||
3454 | pruneSegments(); | ||
3455 | updateLineStartList(); | ||
3456 | } | ||
3457 | |||
3458 | /////////////////////////////////////////////////////////////////// | ||
3459 | // Returns change in number of characters in mWText | ||
3460 | |||
3461 | S32 LLTextEditor::insertStringNoUndo(const S32 pos, const LLWString &wstr) | ||
3462 | { | ||
3463 | S32 len = mWText.length(); | ||
3464 | S32 s_len = wstr.length(); | ||
3465 | S32 new_len = len + s_len; | ||
3466 | if( new_len > mMaxTextLength ) | ||
3467 | { | ||
3468 | new_len = mMaxTextLength; | ||
3469 | |||
3470 | // The user's not getting everything he's hoping for | ||
3471 | make_ui_sound("UISndBadKeystroke"); | ||
3472 | } | ||
3473 | |||
3474 | mWText.insert(pos, wstr); | ||
3475 | mTextIsUpToDate = FALSE; | ||
3476 | truncate(); | ||
3477 | |||
3478 | return new_len - len; | ||
3479 | } | ||
3480 | |||
3481 | S32 LLTextEditor::removeStringNoUndo(S32 pos, S32 length) | ||
3482 | { | ||
3483 | mWText.erase(pos, length); | ||
3484 | mTextIsUpToDate = FALSE; | ||
3485 | return -length; // This will be wrong if someone calls removeStringNoUndo with an excessive length | ||
3486 | } | ||
3487 | |||
3488 | S32 LLTextEditor::overwriteCharNoUndo(S32 pos, llwchar wc) | ||
3489 | { | ||
3490 | if (pos > (S32)mWText.length()) | ||
3491 | { | ||
3492 | return 0; | ||
3493 | } | ||
3494 | mWText[pos] = wc; | ||
3495 | mTextIsUpToDate = FALSE; | ||
3496 | return 1; | ||
3497 | } | ||
3498 | |||
3499 | //---------------------------------------------------------------------------- | ||
3500 | |||
3501 | void LLTextEditor::makePristine() | ||
3502 | { | ||
3503 | mPristineCmd = mLastCmd; | ||
3504 | mBaseDocIsPristine = !mLastCmd; | ||
3505 | |||
3506 | // Create a clean partition in the undo stack. We don't want a single command to extend from | ||
3507 | // the "pre-pristine" state to the "post-pristine" state. | ||
3508 | if( mLastCmd ) | ||
3509 | { | ||
3510 | mLastCmd->blockExtensions(); | ||
3511 | } | ||
3512 | } | ||
3513 | |||
3514 | BOOL LLTextEditor::isPristine() const | ||
3515 | { | ||
3516 | if( mPristineCmd ) | ||
3517 | { | ||
3518 | return (mPristineCmd == mLastCmd); | ||
3519 | } | ||
3520 | else | ||
3521 | { | ||
3522 | // No undo stack, so check if the version before and commands were done was the original version | ||
3523 | return !mLastCmd && mBaseDocIsPristine; | ||
3524 | } | ||
3525 | } | ||
3526 | |||
3527 | BOOL LLTextEditor::tryToRevertToPristineState() | ||
3528 | { | ||
3529 | if( !isPristine() ) | ||
3530 | { | ||
3531 | deselect(); | ||
3532 | S32 i = 0; | ||
3533 | while( !isPristine() && canUndo() ) | ||
3534 | { | ||
3535 | undo(); | ||
3536 | i--; | ||
3537 | } | ||
3538 | |||
3539 | while( !isPristine() && canRedo() ) | ||
3540 | { | ||
3541 | redo(); | ||
3542 | i++; | ||
3543 | } | ||
3544 | |||
3545 | if( !isPristine() ) | ||
3546 | { | ||
3547 | // failed, so go back to where we started | ||
3548 | while( i > 0 ) | ||
3549 | { | ||
3550 | undo(); | ||
3551 | i--; | ||
3552 | } | ||
3553 | } | ||
3554 | |||
3555 | updateLineStartList(); | ||
3556 | updateScrollFromCursor(); | ||
3557 | } | ||
3558 | |||
3559 | return isPristine(); // TRUE => success | ||
3560 | } | ||
3561 | |||
3562 | |||
3563 | |||
3564 | void LLTextEditor::updateTextRect() | ||
3565 | { | ||
3566 | mTextRect.setOriginAndSize( | ||
3567 | UI_TEXTEDITOR_BORDER + UI_TEXTEDITOR_H_PAD, | ||
3568 | UI_TEXTEDITOR_BORDER, | ||
3569 | mRect.getWidth() - SCROLLBAR_SIZE - 2 * (UI_TEXTEDITOR_BORDER + UI_TEXTEDITOR_H_PAD), | ||
3570 | mRect.getHeight() - 2 * UI_TEXTEDITOR_BORDER - UI_TEXTEDITOR_V_PAD_TOP ); | ||
3571 | } | ||
3572 | |||
3573 | void LLTextEditor::loadKeywords(const LLString& filename, | ||
3574 | const LLDynamicArray<const char*>& funcs, | ||
3575 | const LLDynamicArray<const char*>& tooltips, | ||
3576 | const LLColor3& color) | ||
3577 | { | ||
3578 | if(mKeywords.loadFromFile(filename)) | ||
3579 | { | ||
3580 | S32 count = funcs.count(); | ||
3581 | LLString name; | ||
3582 | for(S32 i = 0; i < count; i++) | ||
3583 | { | ||
3584 | name = funcs.get(i); | ||
3585 | name = utf8str_trim(name); | ||
3586 | mKeywords.addToken(LLKeywordToken::WORD, name.c_str(), color, tooltips.get(i) ); | ||
3587 | } | ||
3588 | |||
3589 | mKeywords.findSegments( &mSegments, mWText ); | ||
3590 | |||
3591 | llassert( mSegments.front()->getStart() == 0 ); | ||
3592 | llassert( mSegments.back()->getEnd() == getLength() ); | ||
3593 | } | ||
3594 | } | ||
3595 | |||
3596 | void LLTextEditor::updateSegments() | ||
3597 | { | ||
3598 | if (mKeywords.isLoaded()) | ||
3599 | { | ||
3600 | // HACK: No non-ascii keywords for now | ||
3601 | mKeywords.findSegments(&mSegments, mWText); | ||
3602 | } | ||
3603 | else if (mAllowEmbeddedItems) | ||
3604 | { | ||
3605 | findEmbeddedItemSegments(); | ||
3606 | } | ||
3607 | // Make sure we have at least one segment | ||
3608 | if (mSegments.size() == 1 && mSegments[0]->getIsDefault()) | ||
3609 | { | ||
3610 | delete mSegments[0]; | ||
3611 | mSegments.clear(); // create default segment | ||
3612 | } | ||
3613 | if (mSegments.empty()) | ||
3614 | { | ||
3615 | LLColor4& text_color = ( mReadOnly ? mReadOnlyFgColor : mFgColor ); | ||
3616 | LLTextSegment* default_segment = new LLTextSegment( text_color, 0, mWText.length() ); | ||
3617 | default_segment->setIsDefault(TRUE); | ||
3618 | mSegments.push_back(default_segment); | ||
3619 | } | ||
3620 | } | ||
3621 | |||
3622 | // Only effective if text was removed from the end of the editor | ||
3623 | void LLTextEditor::pruneSegments() | ||
3624 | { | ||
3625 | S32 len = mWText.length(); | ||
3626 | // Find and update the first valid segment | ||
3627 | segment_list_t::iterator iter = mSegments.end(); | ||
3628 | while(iter != mSegments.begin()) | ||
3629 | { | ||
3630 | --iter; | ||
3631 | LLTextSegment* seg = *iter; | ||
3632 | if (seg->getStart() < len) | ||
3633 | { | ||
3634 | // valid segment | ||
3635 | if (seg->getEnd() > len) | ||
3636 | { | ||
3637 | seg->setEnd(len); | ||
3638 | } | ||
3639 | break; // done | ||
3640 | } | ||
3641 | } | ||
3642 | // erase invalid segments | ||
3643 | ++iter; | ||
3644 | std::for_each(iter, mSegments.end(), DeletePointer()); | ||
3645 | mSegments.erase(iter, mSegments.end()); | ||
3646 | } | ||
3647 | |||
3648 | void LLTextEditor::findEmbeddedItemSegments() | ||
3649 | { | ||
3650 | mHoverSegment = NULL; | ||
3651 | std::for_each(mSegments.begin(), mSegments.end(), DeletePointer()); | ||
3652 | mSegments.clear(); | ||
3653 | |||
3654 | BOOL found_embedded_items = FALSE; | ||
3655 | const LLWString &text = mWText; | ||
3656 | S32 idx = 0; | ||
3657 | while( text[idx] ) | ||
3658 | { | ||
3659 | if( text[idx] >= FIRST_EMBEDDED_CHAR && text[idx] <= LAST_EMBEDDED_CHAR ) | ||
3660 | { | ||
3661 | found_embedded_items = TRUE; | ||
3662 | break; | ||
3663 | } | ||
3664 | ++idx; | ||
3665 | } | ||
3666 | |||
3667 | if( !found_embedded_items ) | ||
3668 | { | ||
3669 | return; | ||
3670 | } | ||
3671 | |||
3672 | S32 text_len = text.length(); | ||
3673 | |||
3674 | BOOL in_text = FALSE; | ||
3675 | |||
3676 | LLColor4& text_color = ( mReadOnly ? mReadOnlyFgColor : mFgColor ); | ||
3677 | |||
3678 | if( idx > 0 ) | ||
3679 | { | ||
3680 | mSegments.push_back( new LLTextSegment( text_color, 0, text_len ) ); // text | ||
3681 | in_text = TRUE; | ||
3682 | } | ||
3683 | |||
3684 | LLStyle embedded_style; | ||
3685 | embedded_style.setIsEmbeddedItem( TRUE ); | ||
3686 | |||
3687 | // Start with i just after the first embedded item | ||
3688 | while ( text[idx] ) | ||
3689 | { | ||
3690 | if( text[idx] >= FIRST_EMBEDDED_CHAR && text[idx] <= LAST_EMBEDDED_CHAR ) | ||
3691 | { | ||
3692 | if( in_text ) | ||
3693 | { | ||
3694 | mSegments.back()->setEnd( idx ); | ||
3695 | } | ||
3696 | mSegments.push_back( new LLTextSegment( embedded_style, idx, idx + 1 ) ); // item | ||
3697 | in_text = FALSE; | ||
3698 | } | ||
3699 | else | ||
3700 | if( !in_text ) | ||
3701 | { | ||
3702 | mSegments.push_back( new LLTextSegment( text_color, idx, text_len ) ); // text | ||
3703 | in_text = TRUE; | ||
3704 | } | ||
3705 | ++idx; | ||
3706 | } | ||
3707 | } | ||
3708 | |||
3709 | BOOL LLTextEditor::handleMouseUpOverSegment(S32 x, S32 y, MASK mask) | ||
3710 | { | ||
3711 | return FALSE; | ||
3712 | } | ||
3713 | |||
3714 | llwchar LLTextEditor::pasteEmbeddedItem(llwchar ext_char) | ||
3715 | { | ||
3716 | return ext_char; | ||
3717 | } | ||
3718 | |||
3719 | void LLTextEditor::bindEmbeddedChars(const LLFontGL* font) | ||
3720 | { | ||
3721 | } | ||
3722 | |||
3723 | void LLTextEditor::unbindEmbeddedChars(const LLFontGL* font) | ||
3724 | { | ||
3725 | } | ||
3726 | |||
3727 | // Finds the text segment (if any) at the give local screen position | ||
3728 | LLTextSegment* LLTextEditor::getSegmentAtLocalPos( S32 x, S32 y ) | ||
3729 | { | ||
3730 | // Find the cursor position at the requested local screen position | ||
3731 | S32 offset = getCursorPosFromLocalCoord( x, y, FALSE ); | ||
3732 | S32 idx = getSegmentIdxAtOffset(offset); | ||
3733 | return idx >= 0 ? mSegments[idx] : NULL; | ||
3734 | } | ||
3735 | |||
3736 | LLTextSegment* LLTextEditor::getSegmentAtOffset(S32 offset) | ||
3737 | { | ||
3738 | S32 idx = getSegmentIdxAtOffset(offset); | ||
3739 | return idx >= 0 ? mSegments[idx] : NULL; | ||
3740 | } | ||
3741 | |||
3742 | S32 LLTextEditor::getSegmentIdxAtOffset(S32 offset) | ||
3743 | { | ||
3744 | if (mSegments.empty() || offset < 0 || offset >= getLength()) | ||
3745 | { | ||
3746 | return -1; | ||
3747 | } | ||
3748 | else | ||
3749 | { | ||
3750 | S32 segidx, segoff; | ||
3751 | getSegmentAndOffset(offset, &segidx, &segoff); | ||
3752 | return segidx; | ||
3753 | } | ||
3754 | } | ||
3755 | |||
3756 | //static | ||
3757 | void LLTextEditor::onMouseCaptureLost( LLMouseHandler* old_captor ) | ||
3758 | { | ||
3759 | LLTextEditor* self = (LLTextEditor*) old_captor; | ||
3760 | self->endSelection(); | ||
3761 | } | ||
3762 | |||
3763 | void LLTextEditor::setOnScrollEndCallback(void (*callback)(void*), void* userdata) | ||
3764 | { | ||
3765 | mOnScrollEndCallback = callback; | ||
3766 | mOnScrollEndData = userdata; | ||
3767 | mScrollbar->setOnScrollEndCallback(callback, userdata); | ||
3768 | } | ||
3769 | |||
3770 | /////////////////////////////////////////////////////////////////// | ||
3771 | // Hack for Notecards | ||
3772 | |||
3773 | BOOL LLTextEditor::importBuffer(const LLString& buffer ) | ||
3774 | { | ||
3775 | std::istringstream instream(buffer); | ||
3776 | |||
3777 | // Version 1 format: | ||
3778 | // Linden text version 1\n | ||
3779 | // {\n | ||
3780 | // <EmbeddedItemList chunk> | ||
3781 | // Text length <bytes without \0>\n | ||
3782 | // <text without \0> (text may contain ext_char_values) | ||
3783 | // }\n | ||
3784 | |||
3785 | char tbuf[MAX_STRING]; | ||
3786 | |||
3787 | S32 version = 0; | ||
3788 | instream.getline(tbuf, MAX_STRING); | ||
3789 | if( 1 != sscanf(tbuf, "Linden text version %d", &version) ) | ||
3790 | { | ||
3791 | llwarns << "Invalid Linden text file header " << llendl; | ||
3792 | return FALSE; | ||
3793 | } | ||
3794 | |||
3795 | if( 1 != version ) | ||
3796 | { | ||
3797 | llwarns << "Invalid Linden text file version: " << version << llendl; | ||
3798 | return FALSE; | ||
3799 | } | ||
3800 | |||
3801 | instream.getline(tbuf, MAX_STRING); | ||
3802 | if( 0 != sscanf(tbuf, "{") ) | ||
3803 | { | ||
3804 | llwarns << "Invalid Linden text file format" << llendl; | ||
3805 | return FALSE; | ||
3806 | } | ||
3807 | |||
3808 | S32 text_len = 0; | ||
3809 | instream.getline(tbuf, MAX_STRING); | ||
3810 | if( 1 != sscanf(tbuf, "Text length %d", &text_len) ) | ||
3811 | { | ||
3812 | llwarns << "Invalid Linden text length field" << llendl; | ||
3813 | return FALSE; | ||
3814 | } | ||
3815 | |||
3816 | if( text_len > mMaxTextLength ) | ||
3817 | { | ||
3818 | llwarns << "Invalid Linden text length: " << text_len << llendl; | ||
3819 | return FALSE; | ||
3820 | } | ||
3821 | |||
3822 | BOOL success = TRUE; | ||
3823 | |||
3824 | char* text = new char[ text_len + 1]; | ||
3825 | instream.get(text, text_len + 1, '\0'); | ||
3826 | text[text_len] = '\0'; | ||
3827 | if( text_len != (S32)strlen(text) ) | ||
3828 | { | ||
3829 | llwarns << llformat("Invalid text length: %d != %d ",strlen(text),text_len) << llendl; | ||
3830 | success = FALSE; | ||
3831 | } | ||
3832 | |||
3833 | instream.getline(tbuf, MAX_STRING); | ||
3834 | if( success && (0 != sscanf(tbuf, "}")) ) | ||
3835 | { | ||
3836 | llwarns << "Invalid Linden text file format: missing terminal }" << llendl; | ||
3837 | success = FALSE; | ||
3838 | } | ||
3839 | |||
3840 | if( success ) | ||
3841 | { | ||
3842 | // Actually set the text | ||
3843 | setText( text ); | ||
3844 | } | ||
3845 | |||
3846 | delete[] text; | ||
3847 | |||
3848 | setCursorPos(0); | ||
3849 | deselect(); | ||
3850 | |||
3851 | updateLineStartList(); | ||
3852 | updateScrollFromCursor(); | ||
3853 | |||
3854 | return success; | ||
3855 | } | ||
3856 | |||
3857 | BOOL LLTextEditor::exportBuffer(LLString &buffer ) | ||
3858 | { | ||
3859 | std::ostringstream outstream(buffer); | ||
3860 | |||
3861 | outstream << "Linden text version 1\n"; | ||
3862 | outstream << "{\n"; | ||
3863 | |||
3864 | outstream << llformat("Text length %d\n", mWText.length() ); | ||
3865 | outstream << getText(); | ||
3866 | outstream << "}\n"; | ||
3867 | |||
3868 | return TRUE; | ||
3869 | } | ||
3870 | |||
3871 | ////////////////////////////////////////////////////////////////////////// | ||
3872 | // LLTextSegment | ||
3873 | |||
3874 | LLTextSegment::LLTextSegment(S32 start) : mStart(start) | ||
3875 | { | ||
3876 | } | ||
3877 | LLTextSegment::LLTextSegment( const LLStyle& style, S32 start, S32 end ) : | ||
3878 | mStyle( style ), | ||
3879 | mStart( start), | ||
3880 | mEnd( end ), | ||
3881 | mToken(NULL), | ||
3882 | mIsDefault(FALSE) | ||
3883 | { | ||
3884 | } | ||
3885 | LLTextSegment::LLTextSegment( | ||
3886 | const LLColor4& color, S32 start, S32 end, BOOL is_visible) : | ||
3887 | mStyle( is_visible, color,"" ), | ||
3888 | mStart( start), | ||
3889 | mEnd( end ), | ||
3890 | mToken(NULL), | ||
3891 | mIsDefault(FALSE) | ||
3892 | { | ||
3893 | } | ||
3894 | LLTextSegment::LLTextSegment( const LLColor4& color, S32 start, S32 end ) : | ||
3895 | mStyle( TRUE, color,"" ), | ||
3896 | mStart( start), | ||
3897 | mEnd( end ), | ||
3898 | mToken(NULL), | ||
3899 | mIsDefault(FALSE) | ||
3900 | { | ||
3901 | } | ||
3902 | LLTextSegment::LLTextSegment( const LLColor3& color, S32 start, S32 end ) : | ||
3903 | mStyle( TRUE, color,"" ), | ||
3904 | mStart( start), | ||
3905 | mEnd( end ), | ||
3906 | mToken(NULL), | ||
3907 | mIsDefault(FALSE) | ||
3908 | { | ||
3909 | } | ||
3910 | |||
3911 | BOOL LLTextSegment::getToolTip(LLString& msg) | ||
3912 | { | ||
3913 | if (mToken && !mToken->getToolTip().empty()) | ||
3914 | { | ||
3915 | const LLWString& wmsg = mToken->getToolTip(); | ||
3916 | msg = wstring_to_utf8str(wmsg); | ||
3917 | return TRUE; | ||
3918 | } | ||
3919 | return FALSE; | ||
3920 | } | ||
3921 | |||
3922 | |||
3923 | |||
3924 | void LLTextSegment::dump() | ||
3925 | { | ||
3926 | llinfos << "Segment [" << | ||
3927 | // mColor.mV[VX] << ", " << | ||
3928 | // mColor.mV[VY] << ", " << | ||
3929 | // mColor.mV[VZ] << "]\t[" << | ||
3930 | mStart << ", " << | ||
3931 | getEnd() << "]" << | ||
3932 | llendl; | ||
3933 | |||
3934 | } | ||
3935 | |||
3936 | // virtual | ||
3937 | LLXMLNodePtr LLTextEditor::getXML(bool save_children) const | ||
3938 | { | ||
3939 | LLXMLNodePtr node = LLUICtrl::getXML(); | ||
3940 | |||
3941 | // Attributes | ||
3942 | |||
3943 | node->createChild("max_length", TRUE)->setIntValue(getMaxLength()); | ||
3944 | |||
3945 | node->createChild("embedded_items", TRUE)->setBoolValue(mAllowEmbeddedItems); | ||
3946 | |||
3947 | node->createChild("font", TRUE)->setStringValue(LLFontGL::nameFromFont(mGLFont)); | ||
3948 | |||
3949 | node->createChild("word_wrap", TRUE)->setBoolValue(mWordWrap); | ||
3950 | |||
3951 | addColorXML(node, mCursorColor, "cursor_color", "TextCursorColor"); | ||
3952 | addColorXML(node, mFgColor, "text_color", "TextFgColor"); | ||
3953 | addColorXML(node, mReadOnlyFgColor, "text_readonly_color", "TextFgReadOnlyColor"); | ||
3954 | addColorXML(node, mReadOnlyBgColor, "bg_readonly_color", "TextBgReadOnlyColor"); | ||
3955 | addColorXML(node, mWriteableBgColor, "bg_writeable_color", "TextBgWriteableColor"); | ||
3956 | addColorXML(node, mFocusBgColor, "bg_focus_color", "TextBgFocusColor"); | ||
3957 | |||
3958 | // Contents | ||
3959 | node->setStringValue(getText()); | ||
3960 | |||
3961 | return node; | ||
3962 | } | ||
3963 | |||
3964 | // static | ||
3965 | LLView* LLTextEditor::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory) | ||
3966 | { | ||
3967 | LLString name("text_editor"); | ||
3968 | node->getAttributeString("name", name); | ||
3969 | |||
3970 | LLRect rect; | ||
3971 | createRect(node, rect, parent, LLRect()); | ||
3972 | |||
3973 | U32 max_text_length = 255; | ||
3974 | node->getAttributeU32("max_length", max_text_length); | ||
3975 | |||
3976 | BOOL allow_embedded_items; | ||
3977 | node->getAttributeBOOL("embedded_items", allow_embedded_items); | ||
3978 | |||
3979 | LLFontGL* font = LLView::selectFont(node); | ||
3980 | |||
3981 | LLString text = node->getTextContents().substr(0, max_text_length - 1); | ||
3982 | |||
3983 | LLTextEditor* text_editor = new LLTextEditor(name, | ||
3984 | rect, | ||
3985 | max_text_length, | ||
3986 | text, | ||
3987 | font, | ||
3988 | allow_embedded_items); | ||
3989 | |||
3990 | text_editor->setTextEditorParameters(node); | ||
3991 | |||
3992 | BOOL hide_scrollbar = FALSE; | ||
3993 | node->getAttributeBOOL("hide_scrollbar",hide_scrollbar); | ||
3994 | text_editor->setHideScrollbarForShortDocs(hide_scrollbar); | ||
3995 | |||
3996 | text_editor->initFromXML(node, parent); | ||
3997 | |||
3998 | return text_editor; | ||
3999 | } | ||
4000 | |||
4001 | void LLTextEditor::setTextEditorParameters(LLXMLNodePtr node) | ||
4002 | { | ||
4003 | BOOL word_wrap = FALSE; | ||
4004 | node->getAttributeBOOL("word_wrap", word_wrap); | ||
4005 | setWordWrap(word_wrap); | ||
4006 | |||
4007 | LLColor4 color; | ||
4008 | if (LLUICtrlFactory::getAttributeColor(node,"cursor_color", color)) | ||
4009 | { | ||
4010 | setCursorColor(color); | ||
4011 | } | ||
4012 | if(LLUICtrlFactory::getAttributeColor(node,"text_color", color)) | ||
4013 | { | ||
4014 | setFgColor(color); | ||
4015 | } | ||
4016 | if(LLUICtrlFactory::getAttributeColor(node,"text_readonly_color", color)) | ||
4017 | { | ||
4018 | setReadOnlyFgColor(color); | ||
4019 | } | ||
4020 | if(LLUICtrlFactory::getAttributeColor(node,"bg_readonly_color", color)) | ||
4021 | { | ||
4022 | setReadOnlyBgColor(color); | ||
4023 | } | ||
4024 | if(LLUICtrlFactory::getAttributeColor(node,"bg_writeable_color", color)) | ||
4025 | { | ||
4026 | setWriteableBgColor(color); | ||
4027 | } | ||
4028 | } | ||
4029 | |||
4030 | /////////////////////////////////////////////////////////////////// | ||
4031 | S32 LLTextEditor::findHTMLToken(const LLString &line, S32 pos, BOOL reverse) | ||
4032 | { | ||
4033 | LLString openers=" \t('\"[{<>"; | ||
4034 | LLString closers=" \t)'\"]}><;"; | ||
4035 | |||
4036 | S32 m2; | ||
4037 | S32 retval; | ||
4038 | |||
4039 | if (reverse) | ||
4040 | { | ||
4041 | |||
4042 | for (retval=pos; retval>0; retval--) | ||
4043 | { | ||
4044 | m2 = openers.find(line.substr(retval,1)); | ||
4045 | if (m2 >= 0) | ||
4046 | { | ||
4047 | retval++; | ||
4048 | break; | ||
4049 | } | ||
4050 | } | ||
4051 | } | ||
4052 | else | ||
4053 | { | ||
4054 | |||
4055 | for (retval=pos; retval<(S32)line.length(); retval++) | ||
4056 | { | ||
4057 | m2 = closers.find(line.substr(retval,1)); | ||
4058 | if (m2 >= 0) | ||
4059 | { | ||
4060 | break; | ||
4061 | } | ||
4062 | } | ||
4063 | } | ||
4064 | |||
4065 | return retval; | ||
4066 | } | ||
4067 | |||
4068 | BOOL LLTextEditor::findHTML(const LLString &line, S32 *begin, S32 *end) | ||
4069 | { | ||
4070 | |||
4071 | S32 m1,m2,m3; | ||
4072 | BOOL matched = FALSE; | ||
4073 | |||
4074 | m1=line.find("://",*end); | ||
4075 | |||
4076 | if (m1 >= 0) //Easy match. | ||
4077 | { | ||
4078 | *begin = findHTMLToken(line, m1, TRUE); | ||
4079 | *end = findHTMLToken(line, m1, FALSE); | ||
4080 | |||
4081 | //Load_url only handles http and https so don't hilite ftp, smb, etc. | ||
4082 | m2 = line.substr(*begin,(m1 - *begin)).find("http"); | ||
4083 | m3 = line.substr(*begin,(m1 - *begin)).find("secondlife"); | ||
4084 | |||
4085 | LLString badneighbors=".,<>/?';\"][}{=-+_)(*&^%$#@!~`\t\r\n\\"; | ||
4086 | |||
4087 | if (m2 >= 0 || m3>=0) | ||
4088 | { | ||
4089 | S32 bn = badneighbors.find(line.substr(m1+3,1)); | ||
4090 | |||
4091 | if (bn < 0) | ||
4092 | { | ||
4093 | matched = TRUE; | ||
4094 | } | ||
4095 | } | ||
4096 | } | ||
4097 | /* matches things like secondlife.com (no http://) needs a whitelist to really be effective. | ||
4098 | else //Harder match. | ||
4099 | { | ||
4100 | m1 = line.find(".",*end); | ||
4101 | |||
4102 | if (m1 >= 0) | ||
4103 | { | ||
4104 | *end = findHTMLToken(line, m1, FALSE); | ||
4105 | *begin = findHTMLToken(line, m1, TRUE); | ||
4106 | |||
4107 | m1 = line.rfind(".",*end); | ||
4108 | |||
4109 | if ( ( *end - m1 ) > 2 && m1 > *begin) | ||
4110 | { | ||
4111 | LLString badneighbors=".,<>/?';\"][}{=-+_)(*&^%$#@!~`"; | ||
4112 | m2 = badneighbors.find(line.substr(m1+1,1)); | ||
4113 | m3 = badneighbors.find(line.substr(m1-1,1)); | ||
4114 | if (m3<0 && m2<0) | ||
4115 | { | ||
4116 | matched = TRUE; | ||
4117 | } | ||
4118 | } | ||
4119 | } | ||
4120 | } | ||
4121 | */ | ||
4122 | |||
4123 | if (matched) | ||
4124 | { | ||
4125 | S32 strpos, strpos2; | ||
4126 | |||
4127 | LLString url = line.substr(*begin,*end - *begin); | ||
4128 | LLString slurlID = "slurl.com/secondlife/"; | ||
4129 | strpos = url.find(slurlID); | ||
4130 | |||
4131 | if (strpos < 0) | ||
4132 | { | ||
4133 | slurlID="secondlife://"; | ||
4134 | strpos = url.find(slurlID); | ||
4135 | } | ||
4136 | |||
4137 | if (strpos >= 0) | ||
4138 | { | ||
4139 | strpos+=slurlID.length(); | ||
4140 | |||
4141 | while ( ( strpos2=url.find("/",strpos) ) == -1 ) | ||
4142 | { | ||
4143 | if ((*end+2) >= (S32)line.length() || line.substr(*end,1) != " " ) | ||
4144 | { | ||
4145 | matched=FALSE; | ||
4146 | break; | ||
4147 | } | ||
4148 | |||
4149 | strpos = (*end + 1) - *begin; | ||
4150 | |||
4151 | *end = findHTMLToken(line,(*begin + strpos),FALSE); | ||
4152 | url = line.substr(*begin,*end - *begin); | ||
4153 | } | ||
4154 | } | ||
4155 | |||
4156 | } | ||
4157 | |||
4158 | if (!matched) | ||
4159 | { | ||
4160 | *begin=*end=0; | ||
4161 | } | ||
4162 | return matched; | ||
4163 | } | ||