btrfs: Attempt to fix GCC2 build.
[haiku.git] / src / kits / interface / TextView.cpp
blob765bc5ffcb59e691f4bbb627ef12fe9848d8b400
1 /*
2 * Copyright 2001-2015 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
5 * Authors:
6 * Stephan Aßmus, superstippi@gmx.de
7 * Stefano Ceccherini, stefano.ceccherini@gmail.com
8 * Marc Flerackers, mflerackers@androme.be
9 * Hiroshi Lockheimer (BTextView is based on his STEEngine)
10 * John Scipione, jscipione@gmail.com
11 * Oliver Tappe, zooey@hirschkaefer.de
15 // TODOs:
16 // - Consider using BObjectList instead of BList
17 // for disallowed characters (it would remove a lot of reinterpret_casts)
18 // - Check for correctness and possible optimizations the calls to _Refresh(),
19 // to refresh only changed parts of text (currently we often redraw the whole
20 // text)
22 // Known Bugs:
23 // - Double buffering doesn't work well (disabled by default)
26 #include <TextView.h>
28 #include <new>
30 #include <stdio.h>
31 #include <stdlib.h>
33 #include <Application.h>
34 #include <Beep.h>
35 #include <Bitmap.h>
36 #include <Clipboard.h>
37 #include <Debug.h>
38 #include <Entry.h>
39 #include <Input.h>
40 #include <LayoutBuilder.h>
41 #include <LayoutUtils.h>
42 #include <MessageRunner.h>
43 #include <Path.h>
44 #include <PopUpMenu.h>
45 #include <PropertyInfo.h>
46 #include <Region.h>
47 #include <ScrollBar.h>
48 #include <SystemCatalog.h>
49 #include <Window.h>
51 #include <binary_compatibility/Interface.h>
53 #include "InlineInput.h"
54 #include "LineBuffer.h"
55 #include "StyleBuffer.h"
56 #include "TextGapBuffer.h"
57 #include "UndoBuffer.h"
58 #include "WidthBuffer.h"
61 using namespace std;
62 using BPrivate::gSystemCatalog;
65 #undef B_TRANSLATION_CONTEXT
66 #define B_TRANSLATION_CONTEXT "TextView"
69 #define TRANSLATE(str) \
70 gSystemCatalog.GetString(B_TRANSLATE_MARK(str), "TextView")
72 #undef TRACE
73 #undef CALLED
74 //#define TRACE_TEXT_VIEW
75 #ifdef TRACE_TEXT_VIEW
76 # include <FunctionTracer.h>
77 static int32 sFunctionDepth = -1;
78 # define CALLED(x...) FunctionTracer _ft("BTextView", __FUNCTION__, \
79 sFunctionDepth)
80 # define TRACE(x...) { BString _to; \
81 _to.Append(' ', (sFunctionDepth + 1) * 2); \
82 printf("%s", _to.String()); printf(x); }
83 #else
84 # define CALLED(x...)
85 # define TRACE(x...)
86 #endif
89 #define USE_WIDTHBUFFER 1
90 #define USE_DOUBLEBUFFERING 0
93 struct flattened_text_run {
94 int32 offset;
95 font_family family;
96 font_style style;
97 float size;
98 float shear; // typically 90.0
99 uint16 face; // typically 0
100 uint8 red;
101 uint8 green;
102 uint8 blue;
103 uint8 alpha; // 255 == opaque
104 uint16 _reserved_; // 0
107 struct flattened_text_run_array {
108 uint32 magic;
109 uint32 version;
110 int32 count;
111 flattened_text_run styles[1];
114 static const uint32 kFlattenedTextRunArrayMagic = 'Ali!';
115 static const uint32 kFlattenedTextRunArrayVersion = 0;
118 enum {
119 CHAR_CLASS_DEFAULT,
120 CHAR_CLASS_WHITESPACE,
121 CHAR_CLASS_GRAPHICAL,
122 CHAR_CLASS_QUOTE,
123 CHAR_CLASS_PUNCTUATION,
124 CHAR_CLASS_PARENS_OPEN,
125 CHAR_CLASS_PARENS_CLOSE,
126 CHAR_CLASS_END_OF_TEXT
130 class BTextView::TextTrackState {
131 public:
132 TextTrackState(BMessenger messenger);
133 ~TextTrackState();
135 void SimulateMouseMovement(BTextView* view);
137 public:
138 int32 clickOffset;
139 bool shiftDown;
140 BRect selectionRect;
141 BPoint where;
143 int32 anchor;
144 int32 selStart;
145 int32 selEnd;
147 private:
148 BMessageRunner* fRunner;
152 struct BTextView::LayoutData {
153 LayoutData()
154 : leftInset(0),
155 topInset(0),
156 rightInset(0),
157 bottomInset(0),
158 valid(false)
162 void UpdateInsets(const BRect& bounds, const BRect& textRect)
164 // we disallow negative insets, as they would cause parts of the
165 // text to be hidden
166 leftInset = textRect.left >= bounds.left
167 ? textRect.left - bounds.left
168 : 0;
169 topInset = textRect.top >= bounds.top
170 ? textRect.top - bounds.top
171 : 0;
172 rightInset = bounds.right >= textRect.right
173 ? bounds.right - textRect.right
174 : leftInset;
175 bottomInset = bounds.bottom >= textRect.bottom
176 ? bounds.bottom - textRect.bottom
177 : topInset;
180 float leftInset;
181 float topInset;
182 float rightInset;
183 float bottomInset;
185 BSize min;
186 BSize preferred;
187 bool valid;
191 static const rgb_color kBlackColor = { 0, 0, 0, 255 };
192 static const rgb_color kBlueInputColor = { 152, 203, 255, 255 };
193 static const rgb_color kRedInputColor = { 255, 152, 152, 255 };
195 static const float kHorizontalScrollBarStep = 10.0;
196 static const float kVerticalScrollBarStep = 12.0;
198 static const int32 kMsgNavigateArrow = '_NvA';
199 static const int32 kMsgNavigatePage = '_NvP';
202 static property_info sPropertyList[] = {
204 "selection",
205 { B_GET_PROPERTY, 0 },
206 { B_DIRECT_SPECIFIER, 0 },
207 "Returns the current selection.", 0,
208 { B_INT32_TYPE, 0 }
211 "selection",
212 { B_SET_PROPERTY, 0 },
213 { B_DIRECT_SPECIFIER, 0 },
214 "Sets the current selection.", 0,
215 { B_INT32_TYPE, 0 }
218 "Text",
219 { B_COUNT_PROPERTIES, 0 },
220 { B_DIRECT_SPECIFIER, 0 },
221 "Returns the length of the text in bytes.", 0,
222 { B_INT32_TYPE, 0 }
225 "Text",
226 { B_GET_PROPERTY, 0 },
227 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 },
228 "Returns the text in the specified range in the BTextView.", 0,
229 { B_STRING_TYPE, 0 }
232 "Text",
233 { B_SET_PROPERTY, 0 },
234 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 },
235 "Removes or inserts text into the specified range in the BTextView.", 0,
236 { B_STRING_TYPE, 0 }
239 "text_run_array",
240 { B_GET_PROPERTY, 0 },
241 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 },
242 "Returns the style information for the text in the specified range in "
243 "the BTextView.", 0,
244 { B_RAW_TYPE, 0 },
247 "text_run_array",
248 { B_SET_PROPERTY, 0 },
249 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 },
250 "Sets the style information for the text in the specified range in the "
251 "BTextView.", 0,
252 { B_RAW_TYPE, 0 },
255 { 0 }
259 BTextView::BTextView(BRect frame, const char* name, BRect textRect,
260 uint32 resizeMask, uint32 flags)
262 BView(frame, name, resizeMask,
263 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
265 _InitObject(textRect, NULL, NULL);
269 BTextView::BTextView(BRect frame, const char* name, BRect textRect,
270 const BFont* initialFont, const rgb_color* initialColor,
271 uint32 resizeMask, uint32 flags)
273 BView(frame, name, resizeMask,
274 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
276 _InitObject(textRect, initialFont, initialColor);
280 BTextView::BTextView(const char* name, uint32 flags)
282 BView(name,
283 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
285 _InitObject(Bounds(), NULL, NULL);
289 BTextView::BTextView(const char* name, const BFont* initialFont,
290 const rgb_color* initialColor, uint32 flags)
292 BView(name,
293 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
295 _InitObject(Bounds(), initialFont, initialColor);
299 BTextView::BTextView(BMessage* archive)
301 BView(archive)
303 CALLED();
304 BRect rect;
306 if (archive->FindRect("_trect", &rect) != B_OK)
307 rect.Set(0, 0, 0, 0);
309 _InitObject(rect, NULL, NULL);
311 const char* text = NULL;
312 if (archive->FindString("_text", &text) == B_OK)
313 SetText(text);
315 int32 flag, flag2;
316 if (archive->FindInt32("_align", &flag) == B_OK)
317 SetAlignment((alignment)flag);
319 float value;
321 if (archive->FindFloat("_tab", &value) == B_OK)
322 SetTabWidth(value);
324 if (archive->FindInt32("_col_sp", &flag) == B_OK)
325 SetColorSpace((color_space)flag);
327 if (archive->FindInt32("_max", &flag) == B_OK)
328 SetMaxBytes(flag);
330 if (archive->FindInt32("_sel", &flag) == B_OK &&
331 archive->FindInt32("_sel", &flag2) == B_OK)
332 Select(flag, flag2);
334 bool toggle;
336 if (archive->FindBool("_stylable", &toggle) == B_OK)
337 SetStylable(toggle);
339 if (archive->FindBool("_auto_in", &toggle) == B_OK)
340 SetAutoindent(toggle);
342 if (archive->FindBool("_wrap", &toggle) == B_OK)
343 SetWordWrap(toggle);
345 if (archive->FindBool("_nsel", &toggle) == B_OK)
346 MakeSelectable(!toggle);
348 if (archive->FindBool("_nedit", &toggle) == B_OK)
349 MakeEditable(!toggle);
351 ssize_t disallowedCount = 0;
352 const int32* disallowedChars = NULL;
353 if (archive->FindData("_dis_ch", B_RAW_TYPE,
354 (const void**)&disallowedChars, &disallowedCount) == B_OK) {
356 fDisallowedChars = new BList;
357 disallowedCount /= sizeof(int32);
358 for (int32 x = 0; x < disallowedCount; x++) {
359 fDisallowedChars->AddItem(
360 reinterpret_cast<void*>(disallowedChars[x]));
364 ssize_t runSize = 0;
365 const void* flattenedRun = NULL;
367 if (archive->FindData("_runs", B_RAW_TYPE, &flattenedRun, &runSize)
368 == B_OK) {
369 text_run_array* runArray = UnflattenRunArray(flattenedRun,
370 (int32*)&runSize);
371 if (runArray) {
372 SetRunArray(0, TextLength(), runArray);
373 FreeRunArray(runArray);
379 BTextView::~BTextView()
381 _CancelInputMethod();
382 _StopMouseTracking();
383 _DeleteOffscreen();
385 delete fText;
386 delete fLines;
387 delete fStyles;
388 delete fDisallowedChars;
389 delete fUndo;
390 delete fClickRunner;
391 delete fDragRunner;
392 delete fLayoutData;
396 BArchivable*
397 BTextView::Instantiate(BMessage* archive)
399 CALLED();
400 if (validate_instantiation(archive, "BTextView"))
401 return new BTextView(archive);
402 return NULL;
406 status_t
407 BTextView::Archive(BMessage* data, bool deep) const
409 CALLED();
410 status_t err = BView::Archive(data, deep);
411 if (err == B_OK)
412 err = data->AddString("_text", Text());
413 if (err == B_OK)
414 err = data->AddInt32("_align", fAlignment);
415 if (err == B_OK)
416 err = data->AddFloat("_tab", fTabWidth);
417 if (err == B_OK)
418 err = data->AddInt32("_col_sp", fColorSpace);
419 if (err == B_OK)
420 err = data->AddRect("_trect", fTextRect);
421 if (err == B_OK)
422 err = data->AddInt32("_max", fMaxBytes);
423 if (err == B_OK)
424 err = data->AddInt32("_sel", fSelStart);
425 if (err == B_OK)
426 err = data->AddInt32("_sel", fSelEnd);
427 if (err == B_OK)
428 err = data->AddBool("_stylable", fStylable);
429 if (err == B_OK)
430 err = data->AddBool("_auto_in", fAutoindent);
431 if (err == B_OK)
432 err = data->AddBool("_wrap", fWrap);
433 if (err == B_OK)
434 err = data->AddBool("_nsel", !fSelectable);
435 if (err == B_OK)
436 err = data->AddBool("_nedit", !fEditable);
438 if (err == B_OK && fDisallowedChars != NULL && fDisallowedChars->CountItems() > 0) {
439 err = data->AddData("_dis_ch", B_RAW_TYPE, fDisallowedChars->Items(),
440 fDisallowedChars->CountItems() * sizeof(int32));
443 if (err == B_OK) {
444 int32 runSize = 0;
445 text_run_array* runArray = RunArray(0, TextLength());
447 void* flattened = FlattenRunArray(runArray, &runSize);
448 if (flattened != NULL) {
449 data->AddData("_runs", B_RAW_TYPE, flattened, runSize);
450 free(flattened);
451 } else
452 err = B_NO_MEMORY;
454 FreeRunArray(runArray);
457 return err;
461 void
462 BTextView::AttachedToWindow()
464 BView::AttachedToWindow();
466 SetDrawingMode(B_OP_COPY);
468 Window()->SetPulseRate(500000);
470 fCaretVisible = false;
471 fCaretTime = 0;
472 fClickCount = 0;
473 fClickTime = 0;
474 fDragOffset = -1;
475 fActive = false;
477 _AutoResize(true);
479 _UpdateScrollbars();
481 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
485 void
486 BTextView::DetachedFromWindow()
488 BView::DetachedFromWindow();
492 void
493 BTextView::Draw(BRect updateRect)
495 // what lines need to be drawn?
496 int32 startLine = _LineAt(BPoint(0.0, updateRect.top));
497 int32 endLine = _LineAt(BPoint(0.0, updateRect.bottom));
499 _DrawLines(startLine, endLine, -1, true);
503 void
504 BTextView::MouseDown(BPoint where)
506 // should we even bother?
507 if (!fEditable && !fSelectable)
508 return;
510 _CancelInputMethod();
512 if (!IsFocus())
513 MakeFocus();
515 _HideCaret();
517 _StopMouseTracking();
519 int32 modifiers = 0;
520 uint32 buttons = 0;
521 BMessage* currentMessage = Window()->CurrentMessage();
522 if (currentMessage != NULL) {
523 currentMessage->FindInt32("modifiers", &modifiers);
524 currentMessage->FindInt32("buttons", (int32*)&buttons);
527 if (buttons == B_SECONDARY_MOUSE_BUTTON) {
528 _ShowContextMenu(where);
529 return;
532 BMessenger messenger(this);
533 fTrackingMouse = new (nothrow) TextTrackState(messenger);
534 if (fTrackingMouse == NULL)
535 return;
537 fTrackingMouse->clickOffset = OffsetAt(where);
538 fTrackingMouse->shiftDown = modifiers & B_SHIFT_KEY;
539 fTrackingMouse->where = where;
541 bigtime_t clickTime = system_time();
542 bigtime_t clickSpeed = 0;
543 get_click_speed(&clickSpeed);
544 bool multipleClick
545 = clickTime - fClickTime < clickSpeed
546 && fLastClickOffset == fTrackingMouse->clickOffset;
548 fWhere = where;
550 SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS,
551 B_LOCK_WINDOW_FOCUS | B_NO_POINTER_HISTORY);
553 if (fSelStart != fSelEnd && !fTrackingMouse->shiftDown && !multipleClick) {
554 BRegion region;
555 GetTextRegion(fSelStart, fSelEnd, &region);
556 if (region.Contains(where)) {
557 // Setup things for dragging
558 fTrackingMouse->selectionRect = region.Frame();
559 fClickCount = 1;
560 fClickTime = clickTime;
561 fLastClickOffset = OffsetAt(where);
562 return;
566 if (multipleClick) {
567 if (fClickCount > 3) {
568 fClickCount = 0;
569 fClickTime = 0;
570 } else {
571 fClickCount++;
572 fClickTime = clickTime;
574 } else if (!fTrackingMouse->shiftDown) {
575 // If no multiple click yet and shift is not pressed, this is an
576 // independent first click somewhere into the textview - we initialize
577 // the corresponding members for handling potential multiple clicks:
578 fLastClickOffset = fCaretOffset = fTrackingMouse->clickOffset;
579 fClickCount = 1;
580 fClickTime = clickTime;
582 // Deselect any previously selected text
583 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset);
586 if (fClickTime == clickTime) {
587 BMessage message(_PING_);
588 message.AddInt64("clickTime", clickTime);
589 delete fClickRunner;
591 BMessenger messenger(this);
592 fClickRunner = new (nothrow) BMessageRunner(messenger, &message,
593 clickSpeed, 1);
596 if (!fSelectable) {
597 _StopMouseTracking();
598 return;
601 int32 offset = fSelStart;
602 if (fTrackingMouse->clickOffset > fSelStart)
603 offset = fSelEnd;
605 fTrackingMouse->anchor = offset;
607 MouseMoved(where, B_INSIDE_VIEW, NULL);
611 void
612 BTextView::MouseUp(BPoint where)
614 BView::MouseUp(where);
615 _PerformMouseUp(where);
617 delete fDragRunner;
618 fDragRunner = NULL;
622 void
623 BTextView::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage)
625 // check if it's a "click'n'move"
626 if (_PerformMouseMoved(where, code))
627 return;
629 switch (code) {
630 case B_ENTERED_VIEW:
631 case B_INSIDE_VIEW:
632 _TrackMouse(where, dragMessage, true);
633 break;
635 case B_EXITED_VIEW:
636 _DragCaret(-1);
637 if (Window()->IsActive() && dragMessage == NULL)
638 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
639 break;
641 default:
642 BView::MouseMoved(where, code, dragMessage);
647 void
648 BTextView::WindowActivated(bool active)
650 BView::WindowActivated(active);
652 if (active && IsFocus()) {
653 if (!fActive)
654 _Activate();
655 } else {
656 if (fActive)
657 _Deactivate();
660 BPoint where;
661 uint32 buttons;
662 GetMouse(&where, &buttons, false);
664 if (Bounds().Contains(where))
665 _TrackMouse(where, NULL);
669 void
670 BTextView::KeyDown(const char* bytes, int32 numBytes)
672 const char keyPressed = bytes[0];
674 if (!fEditable) {
675 // only arrow and page keys are allowed
676 // (no need to hide the cursor)
677 switch (keyPressed) {
678 case B_LEFT_ARROW:
679 case B_RIGHT_ARROW:
680 case B_UP_ARROW:
681 case B_DOWN_ARROW:
682 _HandleArrowKey(keyPressed);
683 break;
685 case B_HOME:
686 case B_END:
687 case B_PAGE_UP:
688 case B_PAGE_DOWN:
689 _HandlePageKey(keyPressed);
690 break;
692 default:
693 BView::KeyDown(bytes, numBytes);
694 break;
697 return;
700 // hide the cursor and caret
701 if (IsFocus())
702 be_app->ObscureCursor();
703 _HideCaret();
705 switch (keyPressed) {
706 case B_BACKSPACE:
707 _HandleBackspace();
708 break;
710 case B_LEFT_ARROW:
711 case B_RIGHT_ARROW:
712 case B_UP_ARROW:
713 case B_DOWN_ARROW:
714 _HandleArrowKey(keyPressed);
715 break;
717 case B_DELETE:
718 _HandleDelete();
719 break;
721 case B_HOME:
722 case B_END:
723 case B_PAGE_UP:
724 case B_PAGE_DOWN:
725 _HandlePageKey(keyPressed);
726 break;
728 case B_ESCAPE:
729 case B_INSERT:
730 case B_FUNCTION_KEY:
731 // ignore, pass it up to superclass
732 BView::KeyDown(bytes, numBytes);
733 break;
735 default:
736 // bail out if the character is not allowed
737 if (fDisallowedChars
738 && fDisallowedChars->HasItem(
739 reinterpret_cast<void*>((uint32)keyPressed))) {
740 beep();
741 return;
744 _HandleAlphaKey(bytes, numBytes);
745 break;
748 // draw the caret
749 if (fSelStart == fSelEnd)
750 _ShowCaret();
754 void
755 BTextView::Pulse()
757 if (fActive && fEditable && fSelStart == fSelEnd) {
758 if (system_time() > (fCaretTime + 500000.0))
759 _InvertCaret();
764 void
765 BTextView::FrameResized(float newWidth, float newHeight)
767 BView::FrameResized(newWidth, newHeight);
768 _UpdateScrollbars();
772 void
773 BTextView::MakeFocus(bool focus)
775 BView::MakeFocus(focus);
777 if (focus && Window() != NULL && Window()->IsActive()) {
778 if (!fActive)
779 _Activate();
780 } else {
781 if (fActive)
782 _Deactivate();
787 void
788 BTextView::MessageReceived(BMessage* message)
790 // ToDo: block input if not editable (Andrew)
792 // was this message dropped?
793 if (message->WasDropped()) {
794 BPoint dropOffset;
795 BPoint dropPoint = message->DropPoint(&dropOffset);
796 ConvertFromScreen(&dropPoint);
797 ConvertFromScreen(&dropOffset);
798 if (!_MessageDropped(message, dropPoint, dropOffset))
799 BView::MessageReceived(message);
801 return;
804 switch (message->what) {
805 case B_CUT:
806 if (!IsTypingHidden())
807 Cut(be_clipboard);
808 else
809 beep();
810 break;
812 case B_COPY:
813 if (!IsTypingHidden())
814 Copy(be_clipboard);
815 else
816 beep();
817 break;
819 case B_PASTE:
820 Paste(be_clipboard);
821 break;
823 case B_UNDO:
824 Undo(be_clipboard);
825 break;
827 case B_SELECT_ALL:
828 SelectAll();
829 break;
831 case B_INPUT_METHOD_EVENT:
833 int32 opcode;
834 if (message->FindInt32("be:opcode", &opcode) == B_OK) {
835 switch (opcode) {
836 case B_INPUT_METHOD_STARTED:
838 BMessenger messenger;
839 if (message->FindMessenger("be:reply_to", &messenger)
840 == B_OK) {
841 ASSERT(fInline == NULL);
842 fInline = new InlineInput(messenger);
844 break;
847 case B_INPUT_METHOD_STOPPED:
848 delete fInline;
849 fInline = NULL;
850 break;
852 case B_INPUT_METHOD_CHANGED:
853 if (fInline != NULL)
854 _HandleInputMethodChanged(message);
855 break;
857 case B_INPUT_METHOD_LOCATION_REQUEST:
858 if (fInline != NULL)
859 _HandleInputMethodLocationRequest();
860 break;
862 default:
863 break;
866 break;
869 case B_SET_PROPERTY:
870 case B_GET_PROPERTY:
871 case B_COUNT_PROPERTIES:
873 BPropertyInfo propInfo(sPropertyList);
874 BMessage specifier;
875 const char* property;
877 if (message->GetCurrentSpecifier(NULL, &specifier) < B_OK
878 || specifier.FindString("property", &property) < B_OK)
879 return;
881 if (propInfo.FindMatch(message, 0, &specifier, specifier.what,
882 property) < B_OK) {
883 BView::MessageReceived(message);
884 break;
887 BMessage reply;
888 bool handled = false;
889 switch(message->what) {
890 case B_GET_PROPERTY:
891 handled = _GetProperty(&specifier, specifier.what, property,
892 &reply);
893 break;
895 case B_SET_PROPERTY:
896 handled = _SetProperty(&specifier, specifier.what, property,
897 &reply);
898 break;
900 case B_COUNT_PROPERTIES:
901 handled = _CountProperties(&specifier, specifier.what,
902 property, &reply);
903 break;
905 default:
906 break;
908 if (handled)
909 message->SendReply(&reply);
910 else
911 BView::MessageReceived(message);
912 break;
915 case _PING_:
917 if (message->HasInt64("clickTime")) {
918 bigtime_t clickTime;
919 message->FindInt64("clickTime", &clickTime);
920 if (clickTime == fClickTime) {
921 if (fSelStart != fSelEnd && fSelectable) {
922 BRegion region;
923 GetTextRegion(fSelStart, fSelEnd, &region);
924 if (region.Contains(fWhere))
925 _TrackMouse(fWhere, NULL);
927 delete fClickRunner;
928 fClickRunner = NULL;
930 } else if (fTrackingMouse) {
931 fTrackingMouse->SimulateMouseMovement(this);
932 _PerformAutoScrolling();
934 break;
937 case _DISPOSE_DRAG_:
938 if (fEditable)
939 _TrackDrag(fWhere);
940 break;
942 case kMsgNavigateArrow:
944 int32 key = message->GetInt32("key", 0);
945 int32 modifiers = message->GetInt32("modifiers", 0);
946 _HandleArrowKey(key, modifiers);
947 break;
950 case kMsgNavigatePage:
952 int32 key = message->GetInt32("key", 0);
953 int32 modifiers = message->GetInt32("modifiers", 0);
954 _HandlePageKey(key, modifiers);
955 break;
958 default:
959 BView::MessageReceived(message);
960 break;
965 BHandler*
966 BTextView::ResolveSpecifier(BMessage* message, int32 index, BMessage* specifier,
967 int32 what, const char* property)
969 BPropertyInfo propInfo(sPropertyList);
970 BHandler* target = this;
972 if (propInfo.FindMatch(message, index, specifier, what, property) < B_OK) {
973 target = BView::ResolveSpecifier(message, index, specifier, what,
974 property);
977 return target;
981 status_t
982 BTextView::GetSupportedSuites(BMessage* data)
984 if (data == NULL)
985 return B_BAD_VALUE;
987 status_t err = data->AddString("suites", "suite/vnd.Be-text-view");
988 if (err != B_OK)
989 return err;
991 BPropertyInfo prop_info(sPropertyList);
992 err = data->AddFlat("messages", &prop_info);
994 if (err != B_OK)
995 return err;
996 return BView::GetSupportedSuites(data);
1000 status_t
1001 BTextView::Perform(perform_code code, void* _data)
1003 switch (code) {
1004 case PERFORM_CODE_MIN_SIZE:
1005 ((perform_data_min_size*)_data)->return_value
1006 = BTextView::MinSize();
1007 return B_OK;
1008 case PERFORM_CODE_MAX_SIZE:
1009 ((perform_data_max_size*)_data)->return_value
1010 = BTextView::MaxSize();
1011 return B_OK;
1012 case PERFORM_CODE_PREFERRED_SIZE:
1013 ((perform_data_preferred_size*)_data)->return_value
1014 = BTextView::PreferredSize();
1015 return B_OK;
1016 case PERFORM_CODE_LAYOUT_ALIGNMENT:
1017 ((perform_data_layout_alignment*)_data)->return_value
1018 = BTextView::LayoutAlignment();
1019 return B_OK;
1020 case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
1021 ((perform_data_has_height_for_width*)_data)->return_value
1022 = BTextView::HasHeightForWidth();
1023 return B_OK;
1024 case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
1026 perform_data_get_height_for_width* data
1027 = (perform_data_get_height_for_width*)_data;
1028 BTextView::GetHeightForWidth(data->width, &data->min, &data->max,
1029 &data->preferred);
1030 return B_OK;
1032 case PERFORM_CODE_SET_LAYOUT:
1034 perform_data_set_layout* data = (perform_data_set_layout*)_data;
1035 BTextView::SetLayout(data->layout);
1036 return B_OK;
1038 case PERFORM_CODE_LAYOUT_INVALIDATED:
1040 perform_data_layout_invalidated* data
1041 = (perform_data_layout_invalidated*)_data;
1042 BTextView::LayoutInvalidated(data->descendants);
1043 return B_OK;
1045 case PERFORM_CODE_DO_LAYOUT:
1047 BTextView::DoLayout();
1048 return B_OK;
1052 return BView::Perform(code, _data);
1056 void
1057 BTextView::SetText(const char* text, const text_run_array* runs)
1059 SetText(text, text ? strlen(text) : 0, runs);
1063 void
1064 BTextView::SetText(const char* text, int32 length, const text_run_array* runs)
1066 _CancelInputMethod();
1068 // hide the caret/unhighlight the selection
1069 if (fActive) {
1070 if (fSelStart != fSelEnd) {
1071 if (fSelectable)
1072 Highlight(fSelStart, fSelEnd);
1073 } else
1074 _HideCaret();
1077 // remove data from buffer
1078 if (fText->Length() > 0)
1079 DeleteText(0, fText->Length());
1081 if (text != NULL && length > 0)
1082 InsertText(text, length, 0, runs);
1084 // recalculate line breaks and draw the text
1085 _Refresh(0, length, false);
1086 fCaretOffset = fSelStart = fSelEnd = 0;
1087 ScrollTo(B_ORIGIN);
1089 // draw the caret
1090 _ShowCaret();
1094 void
1095 BTextView::SetText(BFile* file, int32 offset, int32 length,
1096 const text_run_array* runs)
1098 CALLED();
1100 _CancelInputMethod();
1102 if (file == NULL)
1103 return;
1105 if (fText->Length() > 0)
1106 DeleteText(0, fText->Length());
1108 if (!fText->InsertText(file, offset, length, 0))
1109 return;
1111 // update the start offsets of each line below offset
1112 fLines->BumpOffset(length, _LineAt(offset) + 1);
1114 // update the style runs
1115 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1);
1117 if (fStylable && runs != NULL)
1118 SetRunArray(offset, offset + length, runs);
1119 else {
1120 // apply null-style to inserted text
1121 _ApplyStyleRange(offset, offset + length);
1124 // recalculate line breaks and draw the text
1125 _Refresh(0, length, false);
1126 fCaretOffset = fSelStart = fSelEnd = 0;
1127 ScrollToOffset(fSelStart);
1129 // draw the caret
1130 _ShowCaret();
1134 void
1135 BTextView::Insert(const char* text, const text_run_array* runs)
1137 if (text != NULL)
1138 _DoInsertText(text, strlen(text), fSelStart, runs);
1142 void
1143 BTextView::Insert(const char* text, int32 length, const text_run_array* runs)
1145 if (text != NULL && length > 0)
1146 _DoInsertText(text, strnlen(text, length), fSelStart, runs);
1150 void
1151 BTextView::Insert(int32 offset, const char* text, int32 length,
1152 const text_run_array* runs)
1154 // pin offset at reasonable values
1155 if (offset < 0)
1156 offset = 0;
1157 else if (offset > fText->Length())
1158 offset = fText->Length();
1160 if (text != NULL && length > 0)
1161 _DoInsertText(text, strnlen(text, length), offset, runs);
1165 void
1166 BTextView::Delete()
1168 Delete(fSelStart, fSelEnd);
1172 void
1173 BTextView::Delete(int32 startOffset, int32 endOffset)
1175 CALLED();
1177 // pin offsets at reasonable values
1178 if (startOffset < 0)
1179 startOffset = 0;
1180 else if (startOffset > fText->Length())
1181 startOffset = fText->Length();
1182 if (endOffset < 0)
1183 endOffset = 0;
1184 else if (endOffset > fText->Length())
1185 endOffset = fText->Length();
1187 // anything to delete?
1188 if (startOffset == endOffset)
1189 return;
1191 // hide the caret/unhighlight the selection
1192 if (fActive) {
1193 if (fSelStart != fSelEnd) {
1194 if (fSelectable)
1195 Highlight(fSelStart, fSelEnd);
1196 } else
1197 _HideCaret();
1199 // remove data from buffer
1200 DeleteText(startOffset, endOffset);
1202 // check if the caret needs to be moved
1203 if (fCaretOffset >= endOffset)
1204 fCaretOffset -= (endOffset - startOffset);
1205 else if (fCaretOffset >= startOffset && fCaretOffset < endOffset)
1206 fCaretOffset = startOffset;
1208 fSelEnd = fSelStart = fCaretOffset;
1210 // recalculate line breaks and draw what's left
1211 _Refresh(startOffset, endOffset, false);
1213 // draw the caret
1214 _ShowCaret();
1218 const char*
1219 BTextView::Text() const
1221 return fText->RealText();
1225 int32
1226 BTextView::TextLength() const
1228 return fText->Length();
1232 void
1233 BTextView::GetText(int32 offset, int32 length, char* buffer) const
1235 if (buffer != NULL)
1236 fText->GetString(offset, length, buffer);
1240 uchar
1241 BTextView::ByteAt(int32 offset) const
1243 if (offset < 0 || offset >= fText->Length())
1244 return '\0';
1246 return fText->RealCharAt(offset);
1250 int32
1251 BTextView::CountLines() const
1253 return fLines->NumLines();
1257 int32
1258 BTextView::CurrentLine() const
1260 return LineAt(fSelStart);
1264 void
1265 BTextView::GoToLine(int32 index)
1267 _CancelInputMethod();
1268 _HideCaret();
1269 fSelStart = fSelEnd = fCaretOffset = OffsetAt(index);
1270 _ShowCaret();
1274 void
1275 BTextView::Cut(BClipboard* clipboard)
1277 _CancelInputMethod();
1278 if (!fEditable)
1279 return;
1280 if (fUndo) {
1281 delete fUndo;
1282 fUndo = new CutUndoBuffer(this);
1284 Copy(clipboard);
1285 Delete();
1289 void
1290 BTextView::Copy(BClipboard* clipboard)
1292 _CancelInputMethod();
1294 if (clipboard->Lock()) {
1295 clipboard->Clear();
1297 BMessage* clip = clipboard->Data();
1298 if (clip != NULL) {
1299 int32 numBytes = fSelEnd - fSelStart;
1300 const char* text = fText->GetString(fSelStart, &numBytes);
1301 clip->AddData("text/plain", B_MIME_TYPE, text, numBytes);
1303 int32 size;
1304 if (fStylable) {
1305 text_run_array* runArray = RunArray(fSelStart, fSelEnd, &size);
1306 clip->AddData("application/x-vnd.Be-text_run_array",
1307 B_MIME_TYPE, runArray, size);
1308 FreeRunArray(runArray);
1310 clipboard->Commit();
1312 clipboard->Unlock();
1317 void
1318 BTextView::Paste(BClipboard* clipboard)
1320 CALLED();
1321 _CancelInputMethod();
1323 if (!fEditable || !clipboard->Lock())
1324 return;
1326 BMessage* clip = clipboard->Data();
1327 if (clip != NULL) {
1328 const char* text = NULL;
1329 ssize_t length = 0;
1331 if (clip->FindData("text/plain", B_MIME_TYPE,
1332 (const void**)&text, &length) == B_OK) {
1333 text_run_array* runArray = NULL;
1334 ssize_t runLength = 0;
1336 if (fStylable) {
1337 clip->FindData("application/x-vnd.Be-text_run_array",
1338 B_MIME_TYPE, (const void**)&runArray, &runLength);
1341 _FilterDisallowedChars((char*)text, length, runArray);
1343 if (length < 1) {
1344 beep();
1345 clipboard->Unlock();
1346 return;
1349 if (fUndo) {
1350 delete fUndo;
1351 fUndo = new PasteUndoBuffer(this, text, length, runArray,
1352 runLength);
1355 if (fSelStart != fSelEnd)
1356 Delete();
1358 Insert(text, length, runArray);
1359 ScrollToOffset(fSelEnd);
1363 clipboard->Unlock();
1367 void
1368 BTextView::Clear()
1370 // We always check for fUndo != NULL (not only here),
1371 // because when fUndo is NULL, undo is deactivated.
1372 if (fUndo) {
1373 delete fUndo;
1374 fUndo = new ClearUndoBuffer(this);
1377 Delete();
1381 bool
1382 BTextView::AcceptsPaste(BClipboard* clipboard)
1384 bool result = false;
1386 if (fEditable && clipboard && clipboard->Lock()) {
1387 BMessage* data = clipboard->Data();
1388 result = data && data->HasData("text/plain", B_MIME_TYPE);
1389 clipboard->Unlock();
1392 return result;
1396 bool
1397 BTextView::AcceptsDrop(const BMessage* message)
1399 return fEditable && message
1400 && message->HasData("text/plain", B_MIME_TYPE);
1404 void
1405 BTextView::Select(int32 startOffset, int32 endOffset)
1407 CALLED();
1408 if (!fSelectable)
1409 return;
1411 _CancelInputMethod();
1413 // pin offsets at reasonable values
1414 if (startOffset < 0)
1415 startOffset = 0;
1416 else if (startOffset > fText->Length())
1417 startOffset = fText->Length();
1418 if (endOffset < 0)
1419 endOffset = 0;
1420 else if (endOffset > fText->Length())
1421 endOffset = fText->Length();
1423 // a negative selection?
1424 if (startOffset > endOffset)
1425 return;
1427 // is the new selection any different from the current selection?
1428 if (startOffset == fSelStart && endOffset == fSelEnd)
1429 return;
1431 fStyles->InvalidateNullStyle();
1433 _HideCaret();
1435 if (startOffset == endOffset) {
1436 if (fSelStart != fSelEnd) {
1437 // unhilite the selection
1438 if (fActive)
1439 Highlight(fSelStart, fSelEnd);
1441 fSelStart = fSelEnd = fCaretOffset = startOffset;
1442 _ShowCaret();
1443 } else {
1444 if (fActive) {
1445 // draw only those ranges that are different
1446 long start, end;
1447 if (startOffset != fSelStart) {
1448 // start of selection has changed
1449 if (startOffset > fSelStart) {
1450 start = fSelStart;
1451 end = startOffset;
1452 } else {
1453 start = startOffset;
1454 end = fSelStart;
1456 Highlight(start, end);
1459 if (endOffset != fSelEnd) {
1460 // end of selection has changed
1461 if (endOffset > fSelEnd) {
1462 start = fSelEnd;
1463 end = endOffset;
1464 } else {
1465 start = endOffset;
1466 end = fSelEnd;
1468 Highlight(start, end);
1471 fSelStart = startOffset;
1472 fSelEnd = endOffset;
1477 void
1478 BTextView::SelectAll()
1480 Select(0, fText->Length());
1484 void
1485 BTextView::GetSelection(int32* _start, int32* _end) const
1487 int32 start = 0;
1488 int32 end = 0;
1490 if (fSelectable) {
1491 start = fSelStart;
1492 end = fSelEnd;
1495 if (_start)
1496 *_start = start;
1498 if (_end)
1499 *_end = end;
1503 void
1504 BTextView::SetFontAndColor(const BFont* font, uint32 mode,
1505 const rgb_color* color)
1507 SetFontAndColor(fSelStart, fSelEnd, font, mode, color);
1511 void
1512 BTextView::SetFontAndColor(int32 startOffset, int32 endOffset,
1513 const BFont* font, uint32 mode, const rgb_color* color)
1515 CALLED();
1517 _HideCaret();
1519 const int32 textLength = fText->Length();
1521 if (!fStylable) {
1522 // When the text view is not stylable, we always set the whole text's
1523 // style and ignore the offsets
1524 startOffset = 0;
1525 endOffset = textLength;
1526 } else {
1527 // pin offsets at reasonable values
1528 if (startOffset < 0)
1529 startOffset = 0;
1530 else if (startOffset > textLength)
1531 startOffset = textLength;
1533 if (endOffset < 0)
1534 endOffset = 0;
1535 else if (endOffset > textLength)
1536 endOffset = textLength;
1539 // apply the style to the style buffer
1540 fStyles->InvalidateNullStyle();
1541 _ApplyStyleRange(startOffset, endOffset, mode, font, color);
1543 if ((mode & (B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE)) != 0) {
1544 // ToDo: maybe only invalidate the layout (depending on
1545 // B_SUPPORTS_LAYOUT) and have it _Refresh() automatically?
1546 InvalidateLayout();
1547 // recalc the line breaks and redraw with new style
1548 _Refresh(startOffset, endOffset, false);
1549 } else {
1550 // the line breaks wont change, simply redraw
1551 _RequestDrawLines(_LineAt(startOffset), _LineAt(endOffset));
1554 _ShowCaret();
1558 void
1559 BTextView::GetFontAndColor(int32 offset, BFont* _font,
1560 rgb_color* _color) const
1562 fStyles->GetStyle(offset, _font, _color);
1566 void
1567 BTextView::GetFontAndColor(BFont* _font, uint32* _mode,
1568 rgb_color* _color, bool* _sameColor) const
1570 fStyles->ContinuousGetStyle(_font, _mode, _color, _sameColor,
1571 fSelStart, fSelEnd);
1575 void
1576 BTextView::SetRunArray(int32 startOffset, int32 endOffset,
1577 const text_run_array* runs)
1579 CALLED();
1581 _CancelInputMethod();
1583 text_run_array oneRun;
1585 if (!fStylable) {
1586 // when the text view is not stylable, we always set the whole text's
1587 // style with the first run and ignore the offsets
1588 if (runs->count == 0)
1589 return;
1591 startOffset = 0;
1592 endOffset = fText->Length();
1593 oneRun.count = 1;
1594 oneRun.runs[0] = runs->runs[0];
1595 oneRun.runs[0].offset = 0;
1596 runs = &oneRun;
1597 } else {
1598 // pin offsets at reasonable values
1599 if (startOffset < 0)
1600 startOffset = 0;
1601 else if (startOffset > fText->Length())
1602 startOffset = fText->Length();
1604 if (endOffset < 0)
1605 endOffset = 0;
1606 else if (endOffset > fText->Length())
1607 endOffset = fText->Length();
1610 _SetRunArray(startOffset, endOffset, runs);
1612 _Refresh(startOffset, endOffset, false);
1616 text_run_array*
1617 BTextView::RunArray(int32 startOffset, int32 endOffset, int32* _size) const
1619 // pin offsets at reasonable values
1620 if (startOffset < 0)
1621 startOffset = 0;
1622 else if (startOffset > fText->Length())
1623 startOffset = fText->Length();
1625 if (endOffset < 0)
1626 endOffset = 0;
1627 else if (endOffset > fText->Length())
1628 endOffset = fText->Length();
1630 STEStyleRange* styleRange
1631 = fStyles->GetStyleRange(startOffset, endOffset - 1);
1632 if (styleRange == NULL)
1633 return NULL;
1635 text_run_array* runArray = AllocRunArray(styleRange->count, _size);
1636 if (runArray != NULL) {
1637 for (int32 i = 0; i < runArray->count; i++) {
1638 runArray->runs[i].offset = styleRange->runs[i].offset;
1639 runArray->runs[i].font = styleRange->runs[i].style.font;
1640 runArray->runs[i].color = styleRange->runs[i].style.color;
1644 free(styleRange);
1646 return runArray;
1650 int32
1651 BTextView::LineAt(int32 offset) const
1653 // pin offset at reasonable values
1654 if (offset < 0)
1655 offset = 0;
1656 else if (offset > fText->Length())
1657 offset = fText->Length();
1659 int32 lineNum = _LineAt(offset);
1660 if (_IsOnEmptyLastLine(offset))
1661 lineNum++;
1662 return lineNum;
1666 int32
1667 BTextView::LineAt(BPoint point) const
1669 int32 lineNum = _LineAt(point);
1670 if ((*fLines)[lineNum + 1]->origin <= point.y - fTextRect.top)
1671 lineNum++;
1673 return lineNum;
1677 BPoint
1678 BTextView::PointAt(int32 offset, float* _height) const
1680 // pin offset at reasonable values
1681 if (offset < 0)
1682 offset = 0;
1683 else if (offset > fText->Length())
1684 offset = fText->Length();
1686 // ToDo: Cleanup.
1687 int32 lineNum = _LineAt(offset);
1688 STELine* line = (*fLines)[lineNum];
1689 float height = 0;
1691 BPoint result;
1692 result.x = 0.0;
1693 result.y = line->origin + fTextRect.top;
1695 bool onEmptyLastLine = _IsOnEmptyLastLine(offset);
1697 if (fStyles->NumRuns() == 0) {
1698 // Handle the case where there is only one line (no text inserted)
1699 fStyles->SyncNullStyle(0);
1700 height = _NullStyleHeight();
1701 } else {
1702 height = (line + 1)->origin - line->origin;
1704 if (onEmptyLastLine) {
1705 // special case: go down one line if offset is at the newline
1706 // at the end of the buffer ...
1707 result.y += height;
1708 // ... and return the height of that (empty) line
1709 fStyles->SyncNullStyle(offset);
1710 height = _NullStyleHeight();
1711 } else {
1712 int32 length = offset - line->offset;
1713 result.x += _TabExpandedStyledWidth(line->offset, length);
1717 if (fAlignment != B_ALIGN_LEFT) {
1718 float lineWidth = onEmptyLastLine ? 0.0 : LineWidth(lineNum);
1719 float alignmentOffset = fTextRect.Width() - lineWidth;
1720 if (fAlignment == B_ALIGN_CENTER)
1721 alignmentOffset /= 2;
1722 result.x += alignmentOffset;
1725 // convert from text rect coordinates
1726 result.x += fTextRect.left;
1728 // round up
1729 result.x = lroundf(result.x);
1730 result.y = lroundf(result.y);
1731 if (_height != NULL)
1732 *_height = height;
1734 return result;
1738 int32
1739 BTextView::OffsetAt(BPoint point) const
1741 const int32 textLength = fText->Length();
1743 // should we even bother?
1744 if (point.y >= fTextRect.bottom)
1745 return textLength;
1746 else if (point.y < fTextRect.top)
1747 return 0;
1749 int32 lineNum = _LineAt(point);
1750 STELine* line = (*fLines)[lineNum];
1752 #define COMPILE_PROBABLY_BAD_CODE 1
1754 #if COMPILE_PROBABLY_BAD_CODE
1755 // special case: if point is within the text rect and PixelToLine()
1756 // tells us that it's on the last line, but if point is actually
1757 // lower than the bottom of the last line, return the last offset
1758 // (can happen for newlines)
1759 if (lineNum == (fLines->NumLines() - 1)) {
1760 if (point.y >= ((line + 1)->origin + fTextRect.top))
1761 return textLength;
1763 #endif
1765 // convert to text rect coordinates
1766 if (fAlignment != B_ALIGN_LEFT) {
1767 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum);
1768 if (fAlignment == B_ALIGN_CENTER)
1769 alignmentOffset /= 2;
1770 point.x -= alignmentOffset;
1773 point.x -= fTextRect.left;
1774 point.x = max_c(point.x, 0.0);
1776 // ToDo: The following code isn't very efficient, because it always starts
1777 // from the left end, so when the point is near the right end it's very
1778 // slow.
1779 int32 offset = line->offset;
1780 const int32 limit = (line + 1)->offset;
1781 float location = 0;
1782 do {
1783 const int32 nextInitial = _NextInitialByte(offset);
1784 const int32 saveOffset = offset;
1785 float width = 0;
1786 if (ByteAt(offset) == B_TAB)
1787 width = _ActualTabWidth(location);
1788 else
1789 width = _StyledWidth(saveOffset, nextInitial - saveOffset);
1790 if (location + width > point.x) {
1791 if (fabs(location + width - point.x) < fabs(location - point.x))
1792 offset = nextInitial;
1793 break;
1796 location += width;
1797 offset = nextInitial;
1798 } while (offset < limit);
1800 if (offset == (line + 1)->offset) {
1801 // special case: newlines aren't visible
1802 // return the offset of the character preceding the newline
1803 if (ByteAt(offset - 1) == B_ENTER)
1804 return --offset;
1806 // special case: return the offset preceding any spaces that
1807 // aren't at the end of the buffer
1808 if (offset != textLength && ByteAt(offset - 1) == B_SPACE)
1809 return --offset;
1812 return offset;
1816 int32
1817 BTextView::OffsetAt(int32 line) const
1819 if (line < 0)
1820 return 0;
1822 if (line > fLines->NumLines())
1823 return fText->Length();
1825 return (*fLines)[line]->offset;
1829 void
1830 BTextView::FindWord(int32 offset, int32* _fromOffset, int32* _toOffset)
1832 if (offset < 0) {
1833 if (_fromOffset)
1834 *_fromOffset = 0;
1836 if (_toOffset)
1837 *_toOffset = 0;
1839 return;
1842 if (offset > fText->Length()) {
1843 if (_fromOffset)
1844 *_fromOffset = fText->Length();
1846 if (_toOffset)
1847 *_toOffset = fText->Length();
1849 return;
1852 if (_fromOffset)
1853 *_fromOffset = _PreviousWordBoundary(offset);
1855 if (_toOffset)
1856 *_toOffset = _NextWordBoundary(offset);
1860 bool
1861 BTextView::CanEndLine(int32 offset)
1863 if (offset < 0 || offset > fText->Length())
1864 return false;
1866 // TODO: This should be improved using the LocaleKit.
1867 uint32 classification = _CharClassification(offset);
1869 // wrapping is always allowed at end of text and at newlines
1870 if (classification == CHAR_CLASS_END_OF_TEXT || ByteAt(offset) == B_ENTER)
1871 return true;
1873 uint32 nextClassification = _CharClassification(offset + 1);
1874 if (nextClassification == CHAR_CLASS_END_OF_TEXT)
1875 return true;
1877 // never separate a punctuation char from its preceeding word
1878 if (classification == CHAR_CLASS_DEFAULT
1879 && nextClassification == CHAR_CLASS_PUNCTUATION) {
1880 return false;
1883 if ((classification == CHAR_CLASS_WHITESPACE
1884 && nextClassification != CHAR_CLASS_WHITESPACE)
1885 || (classification != CHAR_CLASS_WHITESPACE
1886 && nextClassification == CHAR_CLASS_WHITESPACE)) {
1887 return true;
1890 // allow wrapping after whitespace, unless more whitespace (except for
1891 // newline) follows
1892 if (classification == CHAR_CLASS_WHITESPACE
1893 && (nextClassification != CHAR_CLASS_WHITESPACE
1894 || ByteAt(offset + 1) == B_ENTER)) {
1895 return true;
1898 // allow wrapping after punctuation chars, unless more punctuation, closing
1899 // parens or quotes follow
1900 if (classification == CHAR_CLASS_PUNCTUATION
1901 && nextClassification != CHAR_CLASS_PUNCTUATION
1902 && nextClassification != CHAR_CLASS_PARENS_CLOSE
1903 && nextClassification != CHAR_CLASS_QUOTE) {
1904 return true;
1907 // allow wrapping after quotes, graphical chars and closing parens only if
1908 // whitespace follows (not perfect, but seems to do the right thing most
1909 // of the time)
1910 if ((classification == CHAR_CLASS_QUOTE
1911 || classification == CHAR_CLASS_GRAPHICAL
1912 || classification == CHAR_CLASS_PARENS_CLOSE)
1913 && nextClassification == CHAR_CLASS_WHITESPACE) {
1914 return true;
1917 return false;
1921 float
1922 BTextView::LineWidth(int32 lineNumber) const
1924 if (lineNumber < 0 || lineNumber >= fLines->NumLines())
1925 return 0;
1927 STELine* line = (*fLines)[lineNumber];
1928 int32 length = (line + 1)->offset - line->offset;
1930 // skip newline at the end of the line, if any, as it does no contribute
1931 // to the width
1932 if (ByteAt((line + 1)->offset - 1) == B_ENTER)
1933 length--;
1935 return _TabExpandedStyledWidth(line->offset, length);
1939 float
1940 BTextView::LineHeight(int32 lineNumber) const
1942 float lineHeight = TextHeight(lineNumber, lineNumber);
1943 if (lineHeight == 0.0) {
1944 // We probably don't have text content yet. Take the initial
1945 // style's font height or fall back to the plain font.
1946 const BFont* font;
1947 fStyles->GetNullStyle(&font, NULL);
1948 if (font == NULL)
1949 font = be_plain_font;
1951 font_height fontHeight;
1952 font->GetHeight(&fontHeight);
1953 // This is how the height is calculated in _RecalculateLineBreaks().
1954 lineHeight = ceilf(fontHeight.ascent + fontHeight.descent) + 1;
1957 return lineHeight;
1961 float
1962 BTextView::TextHeight(int32 startLine, int32 endLine) const
1964 const int32 numLines = fLines->NumLines();
1965 if (startLine < 0)
1966 startLine = 0;
1967 else if (startLine > numLines - 1)
1968 startLine = numLines - 1;
1970 if (endLine < 0)
1971 endLine = 0;
1972 else if (endLine > numLines - 1)
1973 endLine = numLines - 1;
1975 float height = (*fLines)[endLine + 1]->origin
1976 - (*fLines)[startLine]->origin;
1978 if (startLine != endLine && endLine == numLines - 1
1979 && fText->RealCharAt(fText->Length() - 1) == B_ENTER) {
1980 height += (*fLines)[endLine + 1]->origin - (*fLines)[endLine]->origin;
1983 return ceilf(height);
1987 void
1988 BTextView::GetTextRegion(int32 startOffset, int32 endOffset,
1989 BRegion* outRegion) const
1991 if (!outRegion)
1992 return;
1994 outRegion->MakeEmpty();
1996 // pin offsets at reasonable values
1997 if (startOffset < 0)
1998 startOffset = 0;
1999 else if (startOffset > fText->Length())
2000 startOffset = fText->Length();
2001 if (endOffset < 0)
2002 endOffset = 0;
2003 else if (endOffset > fText->Length())
2004 endOffset = fText->Length();
2006 // return an empty region if the range is invalid
2007 if (startOffset >= endOffset)
2008 return;
2010 float startLineHeight = 0.0;
2011 float endLineHeight = 0.0;
2012 BPoint startPt = PointAt(startOffset, &startLineHeight);
2013 BPoint endPt = PointAt(endOffset, &endLineHeight);
2015 startLineHeight = ceilf(startLineHeight);
2016 endLineHeight = ceilf(endLineHeight);
2018 BRect selRect;
2020 if (startPt.y == endPt.y) {
2021 // this is a one-line region
2022 selRect.left = max_c(startPt.x, fTextRect.left);
2023 selRect.top = startPt.y;
2024 selRect.right = endPt.x - 1.0;
2025 selRect.bottom = endPt.y + endLineHeight - 1.0;
2026 outRegion->Include(selRect);
2027 } else {
2028 // more than one line in the specified offset range
2029 selRect.left = max_c(startPt.x, fTextRect.left);
2030 selRect.top = startPt.y;
2031 selRect.right = fTextRect.right;
2032 selRect.bottom = startPt.y + startLineHeight - 1.0;
2033 outRegion->Include(selRect);
2035 if (startPt.y + startLineHeight < endPt.y) {
2036 // more than two lines in the range
2037 selRect.left = fTextRect.left;
2038 selRect.top = startPt.y + startLineHeight;
2039 selRect.right = fTextRect.right;
2040 selRect.bottom = endPt.y - 1.0;
2041 outRegion->Include(selRect);
2044 selRect.left = fTextRect.left;
2045 selRect.top = endPt.y;
2046 selRect.right = endPt.x - 1.0;
2047 selRect.bottom = endPt.y + endLineHeight - 1.0;
2048 outRegion->Include(selRect);
2053 void
2054 BTextView::ScrollToOffset(int32 offset)
2056 // pin offset at reasonable values
2057 if (offset < 0)
2058 offset = 0;
2059 else if (offset > fText->Length())
2060 offset = fText->Length();
2062 BRect bounds = Bounds();
2063 float lineHeight = 0.0;
2064 float xDiff = 0.0;
2065 float yDiff = 0.0;
2066 BPoint point = PointAt(offset, &lineHeight);
2068 // horizontal
2069 float extraSpace = fAlignment == B_ALIGN_LEFT ?
2070 ceilf(bounds.IntegerWidth() / 2) : 0.0;
2072 if (point.x < bounds.left)
2073 xDiff = point.x - bounds.left - extraSpace;
2074 else if (point.x > bounds.right)
2075 xDiff = point.x - bounds.right + extraSpace;
2077 // vertical
2078 if (point.y < bounds.top)
2079 yDiff = point.y - bounds.top;
2080 else if (point.y + lineHeight > bounds.bottom
2081 && point.y - lineHeight > bounds.top) {
2082 yDiff = point.y + lineHeight - bounds.bottom;
2085 // prevent negative scroll offset
2086 if (bounds.left + xDiff < 0.0)
2087 xDiff = -bounds.left;
2088 if (bounds.top + yDiff < 0.0)
2089 yDiff = -bounds.top;
2091 ScrollBy(xDiff, yDiff);
2095 void
2096 BTextView::ScrollToSelection()
2098 ScrollToOffset(fSelStart);
2102 void
2103 BTextView::Highlight(int32 startOffset, int32 endOffset)
2105 // pin offsets at reasonable values
2106 if (startOffset < 0)
2107 startOffset = 0;
2108 else if (startOffset > fText->Length())
2109 startOffset = fText->Length();
2110 if (endOffset < 0)
2111 endOffset = 0;
2112 else if (endOffset > fText->Length())
2113 endOffset = fText->Length();
2115 if (startOffset >= endOffset)
2116 return;
2118 BRegion selRegion;
2119 GetTextRegion(startOffset, endOffset, &selRegion);
2121 SetDrawingMode(B_OP_INVERT);
2122 FillRegion(&selRegion, B_SOLID_HIGH);
2123 SetDrawingMode(B_OP_COPY);
2127 // #pragma mark - Configuration methods
2130 void
2131 BTextView::SetTextRect(BRect rect)
2133 if (rect == fTextRect)
2134 return;
2136 if (!fWrap) {
2137 rect.right = Bounds().right - fLayoutData->rightInset;
2138 rect.bottom = Bounds().bottom - fLayoutData->bottomInset;
2141 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), rect);
2143 _ResetTextRect();
2147 BRect
2148 BTextView::TextRect() const
2150 return fTextRect;
2154 void
2155 BTextView::_ResetTextRect()
2157 BRect oldTextRect(fTextRect);
2158 // reset text rect to bounds minus insets ...
2159 fTextRect = Bounds().OffsetToCopy(B_ORIGIN);
2160 fTextRect.left += fLayoutData->leftInset;
2161 fTextRect.top += fLayoutData->topInset;
2162 fTextRect.right -= fLayoutData->rightInset;
2163 fTextRect.bottom -= fLayoutData->bottomInset;
2165 // and rewrap (potentially adjusting the right and the bottom of the text
2166 // rect)
2167 _Refresh(0, TextLength(), false);
2169 // Make sure that the dirty area outside the text is redrawn too.
2170 BRegion invalid(oldTextRect | fTextRect);
2171 invalid.Exclude(fTextRect);
2172 Invalidate(&invalid);
2176 void
2177 BTextView::SetInsets(float left, float top, float right, float bottom)
2179 if (fLayoutData->leftInset == left
2180 && fLayoutData->topInset == top
2181 && fLayoutData->rightInset == right
2182 && fLayoutData->bottomInset == bottom)
2183 return;
2185 fLayoutData->leftInset = left;
2186 fLayoutData->topInset = top;
2187 fLayoutData->rightInset = right;
2188 fLayoutData->bottomInset = bottom;
2190 InvalidateLayout();
2191 Invalidate();
2195 void
2196 BTextView::GetInsets(float* _left, float* _top, float* _right,
2197 float* _bottom) const
2199 if (_left)
2200 *_left = fLayoutData->leftInset;
2201 if (_top)
2202 *_top = fLayoutData->topInset;
2203 if (_right)
2204 *_right = fLayoutData->rightInset;
2205 if (_bottom)
2206 *_bottom = fLayoutData->bottomInset;
2210 void
2211 BTextView::SetStylable(bool stylable)
2213 fStylable = stylable;
2217 bool
2218 BTextView::IsStylable() const
2220 return fStylable;
2224 void
2225 BTextView::SetTabWidth(float width)
2227 if (width == fTabWidth)
2228 return;
2230 fTabWidth = width;
2232 if (Window() != NULL)
2233 _Refresh(0, fText->Length(), false);
2237 float
2238 BTextView::TabWidth() const
2240 return fTabWidth;
2244 void
2245 BTextView::MakeSelectable(bool selectable)
2247 if (selectable == fSelectable)
2248 return;
2250 fSelectable = selectable;
2252 if (fActive && fSelStart != fSelEnd && Window() != NULL)
2253 Highlight(fSelStart, fSelEnd);
2257 bool
2258 BTextView::IsSelectable() const
2260 return fSelectable;
2264 void
2265 BTextView::MakeEditable(bool editable)
2267 if (editable == fEditable)
2268 return;
2270 fEditable = editable;
2271 // TextControls change the color of the text when
2272 // they are made editable, so we need to invalidate
2273 // the NULL style here
2274 // TODO: it works well, but it could be caused by a bug somewhere else
2275 if (fEditable)
2276 fStyles->InvalidateNullStyle();
2277 if (Window() != NULL && fActive) {
2278 if (!fEditable) {
2279 _HideCaret();
2280 _CancelInputMethod();
2286 bool
2287 BTextView::IsEditable() const
2289 return fEditable;
2293 void
2294 BTextView::SetWordWrap(bool wrap)
2296 if (wrap == fWrap)
2297 return;
2299 bool updateOnScreen = fActive && Window() != NULL;
2300 if (updateOnScreen) {
2301 // hide the caret, unhilite the selection
2302 if (fSelStart != fSelEnd) {
2303 if (fSelectable)
2304 Highlight(fSelStart, fSelEnd);
2305 } else
2306 _HideCaret();
2309 fWrap = wrap;
2310 if (wrap)
2311 _ResetTextRect();
2312 _Refresh(0, fText->Length(), false);
2314 if (updateOnScreen) {
2315 // show the caret, hilite the selection
2316 if (fSelStart != fSelEnd) {
2317 if (fSelectable)
2318 Highlight(fSelStart, fSelEnd);
2319 } else
2320 _ShowCaret();
2325 bool
2326 BTextView::DoesWordWrap() const
2328 return fWrap;
2332 void
2333 BTextView::SetMaxBytes(int32 max)
2335 const int32 textLength = fText->Length();
2336 fMaxBytes = max;
2338 if (fMaxBytes < textLength) {
2339 int32 offset = fMaxBytes;
2340 // Delete the text after fMaxBytes, but
2341 // respect multibyte characters boundaries.
2342 const int32 previousInitial = _PreviousInitialByte(offset);
2343 if (_NextInitialByte(previousInitial) != offset)
2344 offset = previousInitial;
2346 Delete(offset, textLength);
2351 int32
2352 BTextView::MaxBytes() const
2354 return fMaxBytes;
2358 void
2359 BTextView::DisallowChar(uint32 character)
2361 if (fDisallowedChars == NULL)
2362 fDisallowedChars = new BList;
2363 if (!fDisallowedChars->HasItem(reinterpret_cast<void*>(character)))
2364 fDisallowedChars->AddItem(reinterpret_cast<void*>(character));
2368 void
2369 BTextView::AllowChar(uint32 character)
2371 if (fDisallowedChars != NULL)
2372 fDisallowedChars->RemoveItem(reinterpret_cast<void*>(character));
2376 void
2377 BTextView::SetAlignment(alignment align)
2379 // Do a reality check
2380 if (fAlignment != align &&
2381 (align == B_ALIGN_LEFT ||
2382 align == B_ALIGN_RIGHT ||
2383 align == B_ALIGN_CENTER)) {
2384 fAlignment = align;
2386 // After setting new alignment, update the view/window
2387 if (Window() != NULL)
2388 Invalidate();
2393 alignment
2394 BTextView::Alignment() const
2396 return fAlignment;
2400 void
2401 BTextView::SetAutoindent(bool state)
2403 fAutoindent = state;
2407 bool
2408 BTextView::DoesAutoindent() const
2410 return fAutoindent;
2414 void
2415 BTextView::SetColorSpace(color_space colors)
2417 if (colors != fColorSpace && fOffscreen) {
2418 fColorSpace = colors;
2419 _DeleteOffscreen();
2420 _NewOffscreen();
2425 color_space
2426 BTextView::ColorSpace() const
2428 return fColorSpace;
2432 void
2433 BTextView::MakeResizable(bool resize, BView* resizeView)
2435 if (resize) {
2436 fResizable = true;
2437 fContainerView = resizeView;
2439 // Wrapping mode and resizable mode can't live together
2440 if (fWrap) {
2441 fWrap = false;
2443 if (fActive && Window() != NULL) {
2444 if (fSelStart != fSelEnd) {
2445 if (fSelectable)
2446 Highlight(fSelStart, fSelEnd);
2447 } else
2448 _HideCaret();
2451 // We need to reset the right inset, as otherwise the auto-resize would
2452 // get confused about just how wide the textview needs to be.
2453 // This seems to be an artefact of how Tracker creates the textview
2454 // during a rename action.
2455 fLayoutData->rightInset = fLayoutData->leftInset;
2456 } else {
2457 fResizable = false;
2458 fContainerView = NULL;
2459 if (fOffscreen)
2460 _DeleteOffscreen();
2461 _NewOffscreen();
2464 _Refresh(0, fText->Length(), false);
2468 bool
2469 BTextView::IsResizable() const
2471 return fResizable;
2475 void
2476 BTextView::SetDoesUndo(bool undo)
2478 if (undo && fUndo == NULL)
2479 fUndo = new UndoBuffer(this, B_UNDO_UNAVAILABLE);
2480 else if (!undo && fUndo != NULL) {
2481 delete fUndo;
2482 fUndo = NULL;
2487 bool
2488 BTextView::DoesUndo() const
2490 return fUndo != NULL;
2494 void
2495 BTextView::HideTyping(bool enabled)
2497 if (enabled)
2498 Delete(0, fText->Length());
2500 fText->SetPasswordMode(enabled);
2504 bool
2505 BTextView::IsTypingHidden() const
2507 return fText->PasswordMode();
2511 // #pragma mark - Size methods
2514 void
2515 BTextView::ResizeToPreferred()
2517 BView::ResizeToPreferred();
2521 void
2522 BTextView::GetPreferredSize(float* _width, float* _height)
2524 CALLED();
2526 _ValidateLayoutData();
2528 if (_width) {
2529 float width = Bounds().Width();
2530 if (width < fLayoutData->min.width
2531 || (Flags() & B_SUPPORTS_LAYOUT) != 0) {
2532 width = fLayoutData->min.width;
2534 *_width = width;
2537 if (_height) {
2538 float height = Bounds().Height();
2539 if (height < fLayoutData->min.height
2540 || (Flags() & B_SUPPORTS_LAYOUT) != 0) {
2541 height = fLayoutData->min.height;
2543 *_height = height;
2548 BSize
2549 BTextView::MinSize()
2551 CALLED();
2553 _ValidateLayoutData();
2554 return BLayoutUtils::ComposeSize(ExplicitMinSize(), fLayoutData->min);
2558 BSize
2559 BTextView::MaxSize()
2561 CALLED();
2563 return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
2564 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
2568 BSize
2569 BTextView::PreferredSize()
2571 CALLED();
2573 _ValidateLayoutData();
2574 return BLayoutUtils::ComposeSize(ExplicitPreferredSize(),
2575 fLayoutData->preferred);
2579 bool
2580 BTextView::HasHeightForWidth()
2582 if (IsEditable())
2583 return BView::HasHeightForWidth();
2585 // When not editable, we assume that all text is supposed to be visible.
2586 return true;
2590 void
2591 BTextView::GetHeightForWidth(float width, float* min, float* max,
2592 float* preferred)
2594 if (IsEditable()) {
2595 BView::GetHeightForWidth(width, min, max, preferred);
2596 return;
2599 // TODO: don't change the actual text rect!
2600 fTextRect.right = fTextRect.left + width;
2601 _Refresh(0, TextLength(), false);
2603 if (min != NULL)
2604 *min = fTextRect.Height();
2605 if (max != NULL)
2606 *max = B_SIZE_UNLIMITED;
2607 if (preferred != NULL)
2608 *preferred = fTextRect.Height();
2612 // #pragma mark - Layout methods
2615 void
2616 BTextView::LayoutInvalidated(bool descendants)
2618 CALLED();
2620 fLayoutData->valid = false;
2624 void
2625 BTextView::DoLayout()
2627 // Bail out, if we shan't do layout.
2628 if (!(Flags() & B_SUPPORTS_LAYOUT))
2629 return;
2631 CALLED();
2633 // If the user set a layout, we let the base class version call its
2634 // hook.
2635 if (GetLayout()) {
2636 BView::DoLayout();
2637 return;
2640 _ValidateLayoutData();
2642 // validate current size
2643 BSize size(Bounds().Size());
2644 if (size.width < fLayoutData->min.width)
2645 size.width = fLayoutData->min.width;
2646 if (size.height < fLayoutData->min.height)
2647 size.height = fLayoutData->min.height;
2649 _ResetTextRect();
2653 void
2654 BTextView::_ValidateLayoutData()
2656 if (fLayoutData->valid)
2657 return;
2659 CALLED();
2661 float lineHeight = ceilf(LineHeight(0));
2662 TRACE("line height: %.2f\n", lineHeight);
2664 // compute our minimal size
2665 BSize min(lineHeight * 3, lineHeight);
2666 min.width += fLayoutData->leftInset + fLayoutData->rightInset;
2667 min.height += fLayoutData->topInset + fLayoutData->bottomInset;
2669 fLayoutData->min = min;
2671 // compute our preferred size
2672 fLayoutData->preferred.height = fTextRect.Height()
2673 + fLayoutData->topInset + fLayoutData->bottomInset;
2675 if (fWrap)
2676 fLayoutData->preferred.width = min.width + 5 * lineHeight;
2677 else {
2678 float maxWidth = fLines->MaxWidth();
2679 if (maxWidth < min.width)
2680 maxWidth = min.width;
2682 fLayoutData->preferred.width
2683 = maxWidth + fLayoutData->leftInset + fLayoutData->rightInset;
2686 fLayoutData->valid = true;
2687 ResetLayoutInvalidation();
2689 TRACE("width: %.2f, height: %.2f\n", min.width, min.height);
2693 // #pragma mark -
2696 void
2697 BTextView::AllAttached()
2699 BView::AllAttached();
2703 void
2704 BTextView::AllDetached()
2706 BView::AllDetached();
2710 /* static */
2711 text_run_array*
2712 BTextView::AllocRunArray(int32 entryCount, int32* outSize)
2714 int32 size = sizeof(text_run_array) + (entryCount - 1) * sizeof(text_run);
2716 text_run_array* runArray = (text_run_array*)calloc(size, 1);
2717 if (runArray == NULL) {
2718 if (outSize != NULL)
2719 *outSize = 0;
2720 return NULL;
2723 runArray->count = entryCount;
2725 // Call constructors explicitly as the text_run_array
2726 // was allocated with malloc (and has to, for backwards
2727 // compatibility)
2728 for (int32 i = 0; i < runArray->count; i++) {
2729 new (&runArray->runs[i].font) BFont;
2732 if (outSize != NULL)
2733 *outSize = size;
2735 return runArray;
2739 /* static */
2740 text_run_array*
2741 BTextView::CopyRunArray(const text_run_array* orig, int32 countDelta)
2743 text_run_array* copy = AllocRunArray(countDelta, NULL);
2744 if (copy != NULL) {
2745 for (int32 i = 0; i < countDelta; i++) {
2746 copy->runs[i].offset = orig->runs[i].offset;
2747 copy->runs[i].font = orig->runs[i].font;
2748 copy->runs[i].color = orig->runs[i].color;
2751 return copy;
2755 /* static */
2756 void
2757 BTextView::FreeRunArray(text_run_array* array)
2759 if (array == NULL)
2760 return;
2762 // Call destructors explicitly
2763 for (int32 i = 0; i < array->count; i++)
2764 array->runs[i].font.~BFont();
2766 free(array);
2770 /* static */
2771 void*
2772 BTextView::FlattenRunArray(const text_run_array* runArray, int32* _size)
2774 CALLED();
2775 int32 size = sizeof(flattened_text_run_array) + (runArray->count - 1)
2776 * sizeof(flattened_text_run);
2778 flattened_text_run_array* array = (flattened_text_run_array*)malloc(size);
2779 if (array == NULL) {
2780 if (_size)
2781 *_size = 0;
2782 return NULL;
2785 array->magic = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayMagic);
2786 array->version = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayVersion);
2787 array->count = B_HOST_TO_BENDIAN_INT32(runArray->count);
2789 for (int32 i = 0; i < runArray->count; i++) {
2790 array->styles[i].offset = B_HOST_TO_BENDIAN_INT32(
2791 runArray->runs[i].offset);
2792 runArray->runs[i].font.GetFamilyAndStyle(&array->styles[i].family,
2793 &array->styles[i].style);
2794 array->styles[i].size = B_HOST_TO_BENDIAN_FLOAT(
2795 runArray->runs[i].font.Size());
2796 array->styles[i].shear = B_HOST_TO_BENDIAN_FLOAT(
2797 runArray->runs[i].font.Shear());
2798 array->styles[i].face = B_HOST_TO_BENDIAN_INT16(
2799 runArray->runs[i].font.Face());
2800 array->styles[i].red = runArray->runs[i].color.red;
2801 array->styles[i].green = runArray->runs[i].color.green;
2802 array->styles[i].blue = runArray->runs[i].color.blue;
2803 array->styles[i].alpha = 255;
2804 array->styles[i]._reserved_ = 0;
2807 if (_size)
2808 *_size = size;
2810 return array;
2814 /* static */
2815 text_run_array*
2816 BTextView::UnflattenRunArray(const void* data, int32* _size)
2818 CALLED();
2819 flattened_text_run_array* array = (flattened_text_run_array*)data;
2821 if (B_BENDIAN_TO_HOST_INT32(array->magic) != kFlattenedTextRunArrayMagic
2822 || B_BENDIAN_TO_HOST_INT32(array->version)
2823 != kFlattenedTextRunArrayVersion) {
2824 if (_size)
2825 *_size = 0;
2827 return NULL;
2830 int32 count = B_BENDIAN_TO_HOST_INT32(array->count);
2832 text_run_array* runArray = AllocRunArray(count, _size);
2833 if (runArray == NULL)
2834 return NULL;
2836 for (int32 i = 0; i < count; i++) {
2837 runArray->runs[i].offset = B_BENDIAN_TO_HOST_INT32(
2838 array->styles[i].offset);
2840 // Set family and style independently from each other, so that
2841 // even if the family doesn't exist, we try to preserve the style
2842 runArray->runs[i].font.SetFamilyAndStyle(array->styles[i].family, NULL);
2843 runArray->runs[i].font.SetFamilyAndStyle(NULL, array->styles[i].style);
2845 runArray->runs[i].font.SetSize(B_BENDIAN_TO_HOST_FLOAT(
2846 array->styles[i].size));
2847 runArray->runs[i].font.SetShear(B_BENDIAN_TO_HOST_FLOAT(
2848 array->styles[i].shear));
2850 uint16 face = B_BENDIAN_TO_HOST_INT16(array->styles[i].face);
2851 if (face != B_REGULAR_FACE) {
2852 // Be's version doesn't seem to set this correctly
2853 runArray->runs[i].font.SetFace(face);
2856 runArray->runs[i].color.red = array->styles[i].red;
2857 runArray->runs[i].color.green = array->styles[i].green;
2858 runArray->runs[i].color.blue = array->styles[i].blue;
2859 runArray->runs[i].color.alpha = array->styles[i].alpha;
2862 return runArray;
2866 void
2867 BTextView::InsertText(const char* text, int32 length, int32 offset,
2868 const text_run_array* runs)
2870 CALLED();
2872 if (length < 0)
2873 length = 0;
2875 if (offset < 0)
2876 offset = 0;
2877 else if (offset > fText->Length())
2878 offset = fText->Length();
2880 if (length > 0) {
2881 // add the text to the buffer
2882 fText->InsertText(text, length, offset);
2884 // update the start offsets of each line below offset
2885 fLines->BumpOffset(length, _LineAt(offset) + 1);
2887 // update the style runs
2888 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1);
2890 // offset the caret/selection, if the text was inserted before it
2891 if (offset <= fSelEnd) {
2892 fSelStart += length;
2893 fCaretOffset = fSelEnd = fSelStart;
2897 if (fStylable && runs != NULL) {
2898 _SetRunArray(offset, offset + length, runs);
2899 } else {
2900 // apply null-style to inserted text
2901 _ApplyStyleRange(offset, offset + length);
2906 void
2907 BTextView::DeleteText(int32 fromOffset, int32 toOffset)
2909 CALLED();
2911 if (fromOffset < 0)
2912 fromOffset = 0;
2913 else if (fromOffset > fText->Length())
2914 fromOffset = fText->Length();
2916 if (toOffset < 0)
2917 toOffset = 0;
2918 else if (toOffset > fText->Length())
2919 toOffset = fText->Length();
2921 if (fromOffset >= toOffset)
2922 return;
2924 // set nullStyle to style at beginning of range
2925 fStyles->InvalidateNullStyle();
2926 fStyles->SyncNullStyle(fromOffset);
2928 // remove from the text buffer
2929 fText->RemoveRange(fromOffset, toOffset);
2931 // remove any lines that have been obliterated
2932 fLines->RemoveLineRange(fromOffset, toOffset);
2934 // remove any style runs that have been obliterated
2935 fStyles->RemoveStyleRange(fromOffset, toOffset);
2937 // adjust the selection accordingly, assumes fSelEnd >= fSelStart!
2938 int32 range = toOffset - fromOffset;
2939 if (fSelStart >= toOffset) {
2940 // selection is behind the range that was removed
2941 fSelStart -= range;
2942 fSelEnd -= range;
2943 } else if (fSelStart >= fromOffset && fSelEnd <= toOffset) {
2944 // the selection is within the range that was removed
2945 fSelStart = fSelEnd = fromOffset;
2946 } else if (fSelStart >= fromOffset && fSelEnd > toOffset) {
2947 // the selection starts within and ends after the range
2948 // the remaining part is the part that was after the range
2949 fSelStart = fromOffset;
2950 fSelEnd = fromOffset + fSelEnd - toOffset;
2951 } else if (fSelStart < fromOffset && fSelEnd < toOffset) {
2952 // the selection starts before, but ends within the range
2953 fSelEnd = fromOffset;
2954 } else if (fSelStart < fromOffset && fSelEnd >= toOffset) {
2955 // the selection starts before and ends after the range
2956 fSelEnd -= range;
2961 /*! Undoes the last changes.
2963 \param clipboard A \a clipboard to use for the undo operation.
2965 void
2966 BTextView::Undo(BClipboard* clipboard)
2968 if (fUndo)
2969 fUndo->Undo(clipboard);
2973 undo_state
2974 BTextView::UndoState(bool* isRedo) const
2976 return fUndo == NULL ? B_UNDO_UNAVAILABLE : fUndo->State(isRedo);
2980 // #pragma mark - GetDragParameters() is protected
2983 void
2984 BTextView::GetDragParameters(BMessage* drag, BBitmap** bitmap, BPoint* point,
2985 BHandler** handler)
2987 CALLED();
2988 if (drag == NULL)
2989 return;
2991 // Add originator and action
2992 drag->AddPointer("be:originator", this);
2993 drag->AddInt32("be_actions", B_TRASH_TARGET);
2995 // add the text
2996 int32 numBytes = fSelEnd - fSelStart;
2997 const char* text = fText->GetString(fSelStart, &numBytes);
2998 drag->AddData("text/plain", B_MIME_TYPE, text, numBytes);
3000 // add the corresponding styles
3001 int32 size = 0;
3002 text_run_array* styles = RunArray(fSelStart, fSelEnd, &size);
3004 if (styles != NULL) {
3005 drag->AddData("application/x-vnd.Be-text_run_array", B_MIME_TYPE,
3006 styles, size);
3008 FreeRunArray(styles);
3011 if (bitmap != NULL)
3012 *bitmap = NULL;
3014 if (handler != NULL)
3015 *handler = NULL;
3019 // #pragma mark - FBC padding and forbidden methods
3022 void BTextView::_ReservedTextView3() {}
3023 void BTextView::_ReservedTextView4() {}
3024 void BTextView::_ReservedTextView5() {}
3025 void BTextView::_ReservedTextView6() {}
3026 void BTextView::_ReservedTextView7() {}
3027 void BTextView::_ReservedTextView8() {}
3028 void BTextView::_ReservedTextView9() {}
3029 void BTextView::_ReservedTextView10() {}
3030 void BTextView::_ReservedTextView11() {}
3031 void BTextView::_ReservedTextView12() {}
3034 // #pragma mark - Private methods
3037 /*! Inits the BTextView object.
3039 \param textRect The BTextView's text rect.
3040 \param initialFont The font which the BTextView will use.
3041 \param initialColor The initial color of the text.
3043 void
3044 BTextView::_InitObject(BRect textRect, const BFont* initialFont,
3045 const rgb_color* initialColor)
3047 BFont font;
3048 if (initialFont == NULL)
3049 GetFont(&font);
3050 else
3051 font = *initialFont;
3053 _NormalizeFont(&font);
3055 rgb_color documentTextColor = ui_color(B_DOCUMENT_TEXT_COLOR);
3057 if (initialColor == NULL)
3058 initialColor = &documentTextColor;
3060 fText = new BPrivate::TextGapBuffer;
3061 fLines = new LineBuffer;
3062 fStyles = new StyleBuffer(&font, initialColor);
3064 fInstalledNavigateCommandWordwiseShortcuts = false;
3065 fInstalledNavigateOptionWordwiseShortcuts = false;
3066 fInstalledNavigateOptionLinewiseShortcuts = false;
3067 fInstalledNavigateHomeEndDocwiseShortcuts = false;
3069 fInstalledSelectCommandWordwiseShortcuts = false;
3070 fInstalledSelectOptionWordwiseShortcuts = false;
3071 fInstalledSelectOptionLinewiseShortcuts = false;
3072 fInstalledSelectHomeEndDocwiseShortcuts = false;
3074 // We put these here instead of in the constructor initializer list
3075 // to have less code duplication, and a single place where to do changes
3076 // if needed.
3077 fTextRect = textRect;
3078 // NOTE: The only places where text rect is changed:
3079 // * width is possibly adjusted in _AutoResize(),
3080 // * height is adjusted in _RecalculateLineBreaks().
3081 // When used within the layout management framework, the
3082 // text rect is changed to maintain constant insets.
3083 fMinTextRectWidth = fTextRect.Width();
3084 // see SetTextRect()
3085 fSelStart = fSelEnd = 0;
3086 fCaretVisible = false;
3087 fCaretTime = 0;
3088 fCaretOffset = 0;
3089 fClickCount = 0;
3090 fClickTime = 0;
3091 fDragOffset = -1;
3092 fCursor = 0;
3093 fActive = false;
3094 fStylable = false;
3095 fTabWidth = 28.0;
3096 fSelectable = true;
3097 fEditable = true;
3098 fWrap = true;
3099 fMaxBytes = INT32_MAX;
3100 fDisallowedChars = NULL;
3101 fAlignment = B_ALIGN_LEFT;
3102 fAutoindent = false;
3103 fOffscreen = NULL;
3104 fColorSpace = B_CMAP8;
3105 fResizable = false;
3106 fContainerView = NULL;
3107 fUndo = NULL;
3108 fInline = NULL;
3109 fDragRunner = NULL;
3110 fClickRunner = NULL;
3111 fTrackingMouse = NULL;
3113 fLayoutData = new LayoutData;
3114 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), fTextRect);
3116 fLastClickOffset = -1;
3118 SetDoesUndo(true);
3119 SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR);
3123 //! Handles when Backspace key is pressed.
3124 void
3125 BTextView::_HandleBackspace()
3127 if (fUndo) {
3128 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(
3129 fUndo);
3130 if (!undoBuffer) {
3131 delete fUndo;
3132 fUndo = undoBuffer = new TypingUndoBuffer(this);
3134 undoBuffer->BackwardErase();
3137 if (fSelStart == fSelEnd) {
3138 if (fSelStart == 0)
3139 return;
3140 else
3141 fSelStart = _PreviousInitialByte(fSelStart);
3142 } else
3143 Highlight(fSelStart, fSelEnd);
3145 DeleteText(fSelStart, fSelEnd);
3146 fCaretOffset = fSelEnd = fSelStart;
3148 _Refresh(fSelStart, fSelEnd, true);
3152 //! Handles when an arrow key is pressed.
3153 void
3154 BTextView::_HandleArrowKey(uint32 arrowKey, int32 modifiers)
3156 // return if there's nowhere to go
3157 if (fText->Length() == 0)
3158 return;
3160 int32 selStart = fSelStart;
3161 int32 selEnd = fSelEnd;
3163 if (modifiers < 0) {
3164 BMessage* currentMessage = Window()->CurrentMessage();
3165 if (currentMessage == NULL
3166 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) {
3167 modifiers = 0;
3171 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0;
3172 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0;
3173 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0;
3174 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0;
3176 int32 lastClickOffset = fCaretOffset;
3178 switch (arrowKey) {
3179 case B_LEFT_ARROW:
3180 if (!fEditable)
3181 _ScrollBy(-1 * kHorizontalScrollBarStep, 0);
3182 else if (fSelStart != fSelEnd && !shiftKeyDown)
3183 fCaretOffset = fSelStart;
3184 else {
3185 if ((commandKeyDown || optionKeyDown) && !controlKeyDown)
3186 fCaretOffset = _PreviousWordStart(fCaretOffset - 1);
3187 else
3188 fCaretOffset = _PreviousInitialByte(fCaretOffset);
3190 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3191 if (fCaretOffset < fSelStart) {
3192 // extend selection to the left
3193 selStart = fCaretOffset;
3194 if (lastClickOffset > fSelStart) {
3195 // caret has jumped across "anchor"
3196 selEnd = fSelStart;
3198 } else {
3199 // shrink selection from the right
3200 selEnd = fCaretOffset;
3204 break;
3206 case B_RIGHT_ARROW:
3207 if (!fEditable)
3208 _ScrollBy(kHorizontalScrollBarStep, 0);
3209 else if (fSelStart != fSelEnd && !shiftKeyDown)
3210 fCaretOffset = fSelEnd;
3211 else {
3212 if ((commandKeyDown || optionKeyDown) && !controlKeyDown)
3213 fCaretOffset = _NextWordEnd(fCaretOffset);
3214 else
3215 fCaretOffset = _NextInitialByte(fCaretOffset);
3217 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3218 if (fCaretOffset > fSelEnd) {
3219 // extend selection to the right
3220 selEnd = fCaretOffset;
3221 if (lastClickOffset < fSelEnd) {
3222 // caret has jumped across "anchor"
3223 selStart = fSelEnd;
3225 } else {
3226 // shrink selection from the left
3227 selStart = fCaretOffset;
3231 break;
3233 case B_UP_ARROW:
3235 if (!fEditable)
3236 _ScrollBy(0, -1 * kVerticalScrollBarStep);
3237 else if (fSelStart != fSelEnd && !shiftKeyDown)
3238 fCaretOffset = fSelStart;
3239 else {
3240 if (optionKeyDown && !commandKeyDown && !controlKeyDown)
3241 fCaretOffset = _PreviousLineStart(fCaretOffset);
3242 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3243 _ScrollTo(0, 0);
3244 fCaretOffset = 0;
3245 } else {
3246 float height;
3247 BPoint point = PointAt(fCaretOffset, &height);
3248 // find the caret position on the previous
3249 // line by gently stepping onto this line
3250 for (int i = 1; i <= height; i++) {
3251 point.y--;
3252 int32 offset = OffsetAt(point);
3253 if (offset < fCaretOffset || i == height) {
3254 fCaretOffset = offset;
3255 break;
3260 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3261 if (fCaretOffset < fSelStart) {
3262 // extend selection to the top
3263 selStart = fCaretOffset;
3264 if (lastClickOffset > fSelStart) {
3265 // caret has jumped across "anchor"
3266 selEnd = fSelStart;
3268 } else {
3269 // shrink selection from the bottom
3270 selEnd = fCaretOffset;
3274 break;
3277 case B_DOWN_ARROW:
3279 if (!fEditable)
3280 _ScrollBy(0, kVerticalScrollBarStep);
3281 else if (fSelStart != fSelEnd && !shiftKeyDown)
3282 fCaretOffset = fSelEnd;
3283 else {
3284 if (optionKeyDown && !commandKeyDown && !controlKeyDown)
3285 fCaretOffset = _NextLineEnd(fCaretOffset);
3286 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3287 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset);
3288 fCaretOffset = fText->Length();
3289 } else {
3290 float height;
3291 BPoint point = PointAt(fCaretOffset, &height);
3292 point.y += height;
3293 fCaretOffset = OffsetAt(point);
3296 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3297 if (fCaretOffset > fSelEnd) {
3298 // extend selection to the bottom
3299 selEnd = fCaretOffset;
3300 if (lastClickOffset < fSelEnd) {
3301 // caret has jumped across "anchor"
3302 selStart = fSelEnd;
3304 } else {
3305 // shrink selection from the top
3306 selStart = fCaretOffset;
3310 break;
3314 fStyles->InvalidateNullStyle();
3316 if (fEditable) {
3317 if (shiftKeyDown)
3318 Select(selStart, selEnd);
3319 else
3320 Select(fCaretOffset, fCaretOffset);
3322 // scroll if needed
3323 ScrollToOffset(fCaretOffset);
3328 //! Handles when the Delete key is pressed.
3329 void
3330 BTextView::_HandleDelete()
3332 if (fUndo) {
3333 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(
3334 fUndo);
3335 if (!undoBuffer) {
3336 delete fUndo;
3337 fUndo = undoBuffer = new TypingUndoBuffer(this);
3339 undoBuffer->ForwardErase();
3342 if (fSelStart == fSelEnd) {
3343 if (fSelEnd == fText->Length())
3344 return;
3345 else
3346 fSelEnd = _NextInitialByte(fSelEnd);
3347 } else
3348 Highlight(fSelStart, fSelEnd);
3350 DeleteText(fSelStart, fSelEnd);
3351 fCaretOffset = fSelEnd = fSelStart;
3353 _Refresh(fSelStart, fSelEnd, true);
3357 //! Handles when the Page Up, Page Down, Home, or End key is pressed.
3358 void
3359 BTextView::_HandlePageKey(uint32 pageKey, int32 modifiers)
3361 if (modifiers < 0) {
3362 BMessage* currentMessage = Window()->CurrentMessage();
3363 if (currentMessage == NULL
3364 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) {
3365 modifiers = 0;
3369 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0;
3370 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0;
3371 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0;
3372 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0;
3374 STELine* line = NULL;
3375 int32 selStart = fSelStart;
3376 int32 selEnd = fSelEnd;
3378 int32 lastClickOffset = fCaretOffset;
3379 switch (pageKey) {
3380 case B_HOME:
3381 if (!fEditable) {
3382 fCaretOffset = 0;
3383 _ScrollTo(0, 0);
3384 break;
3385 } else {
3386 if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3387 _ScrollTo(0, 0);
3388 fCaretOffset = 0;
3389 } else {
3390 // get the start of the last line if caret is on it
3391 line = (*fLines)[_LineAt(lastClickOffset)];
3392 fCaretOffset = line->offset;
3395 if (!shiftKeyDown)
3396 selStart = selEnd = fCaretOffset;
3397 else if (fCaretOffset != lastClickOffset) {
3398 if (fCaretOffset < fSelStart) {
3399 // extend selection to the left
3400 selStart = fCaretOffset;
3401 if (lastClickOffset > fSelStart) {
3402 // caret has jumped across "anchor"
3403 selEnd = fSelStart;
3405 } else {
3406 // shrink selection from the right
3407 selEnd = fCaretOffset;
3411 break;
3413 case B_END:
3414 if (!fEditable) {
3415 fCaretOffset = fText->Length();
3416 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset);
3417 break;
3418 } else {
3419 if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3420 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset);
3421 fCaretOffset = fText->Length();
3422 } else {
3423 // If we are on the last line, just go to the last
3424 // character in the buffer, otherwise get the starting
3425 // offset of the next line, and go to the previous character
3426 int32 currentLine = _LineAt(lastClickOffset);
3427 if (currentLine + 1 < fLines->NumLines()) {
3428 line = (*fLines)[currentLine + 1];
3429 fCaretOffset = _PreviousInitialByte(line->offset);
3430 } else {
3431 // This check is needed to avoid moving the cursor
3432 // when the cursor is on the last line, and that line
3433 // is empty
3434 if (fCaretOffset != fText->Length()) {
3435 fCaretOffset = fText->Length();
3436 if (ByteAt(fCaretOffset - 1) == B_ENTER)
3437 fCaretOffset--;
3442 if (!shiftKeyDown)
3443 selStart = selEnd = fCaretOffset;
3444 else if (fCaretOffset != lastClickOffset) {
3445 if (fCaretOffset > fSelEnd) {
3446 // extend selection to the right
3447 selEnd = fCaretOffset;
3448 if (lastClickOffset < fSelEnd) {
3449 // caret has jumped across "anchor"
3450 selStart = fSelEnd;
3452 } else {
3453 // shrink selection from the left
3454 selStart = fCaretOffset;
3458 break;
3460 case B_PAGE_UP:
3462 float lineHeight;
3463 BPoint currentPos = PointAt(fCaretOffset, &lineHeight);
3464 BPoint nextPos(currentPos.x,
3465 currentPos.y + lineHeight - Bounds().Height());
3466 fCaretOffset = OffsetAt(nextPos);
3467 nextPos = PointAt(fCaretOffset);
3468 _ScrollBy(0, nextPos.y - currentPos.y);
3470 if (!fEditable)
3471 break;
3473 if (!shiftKeyDown)
3474 selStart = selEnd = fCaretOffset;
3475 else if (fCaretOffset != lastClickOffset) {
3476 if (fCaretOffset < fSelStart) {
3477 // extend selection to the top
3478 selStart = fCaretOffset;
3479 if (lastClickOffset > fSelStart) {
3480 // caret has jumped across "anchor"
3481 selEnd = fSelStart;
3483 } else {
3484 // shrink selection from the bottom
3485 selEnd = fCaretOffset;
3489 break;
3492 case B_PAGE_DOWN:
3494 BPoint currentPos = PointAt(fCaretOffset);
3495 BPoint nextPos(currentPos.x, currentPos.y + Bounds().Height());
3496 fCaretOffset = OffsetAt(nextPos);
3497 nextPos = PointAt(fCaretOffset);
3498 _ScrollBy(0, nextPos.y - currentPos.y);
3500 if (!fEditable)
3501 break;
3503 if (!shiftKeyDown)
3504 selStart = selEnd = fCaretOffset;
3505 else if (fCaretOffset != lastClickOffset) {
3506 if (fCaretOffset > fSelEnd) {
3507 // extend selection to the bottom
3508 selEnd = fCaretOffset;
3509 if (lastClickOffset < fSelEnd) {
3510 // caret has jumped across "anchor"
3511 selStart = fSelEnd;
3513 } else {
3514 // shrink selection from the top
3515 selStart = fCaretOffset;
3519 break;
3523 if (fEditable) {
3524 if (shiftKeyDown)
3525 Select(selStart, selEnd);
3526 else
3527 Select(fCaretOffset, fCaretOffset);
3529 ScrollToOffset(fCaretOffset);
3534 /*! Handles when an alpha-numeric key is pressed.
3536 \param bytes The string or character associated with the key.
3537 \param numBytes The amount of bytes containes in "bytes".
3539 void
3540 BTextView::_HandleAlphaKey(const char* bytes, int32 numBytes)
3542 // TODO: block input if not editable (Andrew)
3543 if (fUndo) {
3544 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(fUndo);
3545 if (!undoBuffer) {
3546 delete fUndo;
3547 fUndo = undoBuffer = new TypingUndoBuffer(this);
3549 undoBuffer->InputCharacter(numBytes);
3552 if (fSelStart != fSelEnd) {
3553 Highlight(fSelStart, fSelEnd);
3554 DeleteText(fSelStart, fSelEnd);
3557 if (fAutoindent && numBytes == 1 && *bytes == B_ENTER) {
3558 int32 start, offset;
3559 start = offset = OffsetAt(_LineAt(fSelStart));
3561 while (ByteAt(offset) != '\0' &&
3562 (ByteAt(offset) == B_TAB || ByteAt(offset) == B_SPACE)
3563 && offset < fSelStart)
3564 offset++;
3566 _DoInsertText(bytes, numBytes, fSelStart, NULL);
3567 if (start != offset)
3568 _DoInsertText(Text() + start, offset - start, fSelStart, NULL);
3569 } else
3570 _DoInsertText(bytes, numBytes, fSelStart, NULL);
3572 fCaretOffset = fSelEnd;
3574 ScrollToOffset(fCaretOffset);
3578 /*! Redraw the text between the two given offsets, recalculating line-breaks
3579 if needed.
3581 \param fromOffset The offset from where to refresh.
3582 \param toOffset The offset where to refresh to.
3583 \param scroll If \c true, scroll the view to the end offset.
3585 void
3586 BTextView::_Refresh(int32 fromOffset, int32 toOffset, bool scroll)
3588 // TODO: Cleanup
3589 float saveHeight = fTextRect.Height();
3590 float saveWidth = fTextRect.Width();
3591 int32 fromLine = _LineAt(fromOffset);
3592 int32 toLine = _LineAt(toOffset);
3593 int32 saveFromLine = fromLine;
3594 int32 saveToLine = toLine;
3596 _RecalculateLineBreaks(&fromLine, &toLine);
3598 // TODO: Maybe there is still something we can do without a window...
3599 if (!Window())
3600 return;
3602 BRect bounds = Bounds();
3603 float newHeight = fTextRect.Height();
3605 // if the line breaks have changed, force an erase
3606 if (fromLine != saveFromLine || toLine != saveToLine
3607 || newHeight != saveHeight) {
3608 fromOffset = -1;
3611 if (newHeight != saveHeight) {
3612 // the text area has changed
3613 if (newHeight < saveHeight)
3614 toLine = _LineAt(BPoint(0.0f, saveHeight + fTextRect.top));
3615 else
3616 toLine = _LineAt(BPoint(0.0f, newHeight + fTextRect.top));
3619 // draw only those lines that are visible
3620 int32 fromVisible = _LineAt(BPoint(0.0f, bounds.top));
3621 int32 toVisible = _LineAt(BPoint(0.0f, bounds.bottom));
3622 fromLine = max_c(fromVisible, fromLine);
3623 toLine = min_c(toLine, toVisible);
3625 _AutoResize(false);
3627 _RequestDrawLines(fromLine, toLine);
3629 // erase the area below the text
3630 BRect eraseRect = bounds;
3631 eraseRect.top = fTextRect.top + (*fLines)[fLines->NumLines()]->origin;
3632 eraseRect.bottom = fTextRect.top + saveHeight;
3633 if (eraseRect.bottom > eraseRect.top && eraseRect.Intersects(bounds)) {
3634 SetLowColor(ViewColor());
3635 FillRect(eraseRect, B_SOLID_LOW);
3638 // update the scroll bars if the text area has changed
3639 if (newHeight != saveHeight || fMinTextRectWidth != saveWidth)
3640 _UpdateScrollbars();
3642 if (scroll)
3643 ScrollToOffset(fSelEnd);
3645 Flush();
3649 /*! Recalculate line breaks between two lines.
3651 \param startLine The line number to start recalculating line breaks.
3652 \param endLine The line number to stop recalculating line breaks.
3654 void
3655 BTextView::_RecalculateLineBreaks(int32* startLine, int32* endLine)
3657 CALLED();
3659 // are we insane?
3660 *startLine = (*startLine < 0) ? 0 : *startLine;
3661 *endLine = (*endLine > fLines->NumLines() - 1) ? fLines->NumLines() - 1
3662 : *endLine;
3664 int32 textLength = fText->Length();
3665 int32 lineIndex = (*startLine > 0) ? *startLine - 1 : 0;
3666 int32 recalThreshold = (*fLines)[*endLine + 1]->offset;
3667 float width = max_c(fTextRect.Width(), 10);
3668 // TODO: The minimum width of 10 is a work around for the following
3669 // problem: If the text rect is too small, we are not calculating any
3670 // line heights, not even for the first line. Maybe this is a bug
3671 // in the algorithm, but other places in the class rely on at least
3672 // the first line to return a valid height. Maybe "10" should really
3673 // be the width of the very first glyph instead.
3674 STELine* curLine = (*fLines)[lineIndex];
3675 STELine* nextLine = curLine + 1;
3677 do {
3678 float ascent, descent;
3679 int32 fromOffset = curLine->offset;
3680 int32 toOffset = _FindLineBreak(fromOffset, &ascent, &descent, &width);
3682 curLine->ascent = ascent;
3683 curLine->width = width;
3685 // we want to advance at least by one character
3686 int32 nextOffset = _NextInitialByte(fromOffset);
3687 if (toOffset < nextOffset && fromOffset < textLength)
3688 toOffset = nextOffset;
3690 lineIndex++;
3691 STELine saveLine = *nextLine;
3692 if (lineIndex > fLines->NumLines() || toOffset < nextLine->offset) {
3693 // the new line comes before the old line start, add a line
3694 STELine newLine;
3695 newLine.offset = toOffset;
3696 newLine.origin = ceilf(curLine->origin + ascent + descent) + 1;
3697 newLine.ascent = 0;
3698 fLines->InsertLine(&newLine, lineIndex);
3699 } else {
3700 // update the existing line
3701 nextLine->offset = toOffset;
3702 nextLine->origin = ceilf(curLine->origin + ascent + descent) + 1;
3704 // remove any lines that start before the current line
3705 while (lineIndex < fLines->NumLines()
3706 && toOffset >= ((*fLines)[lineIndex] + 1)->offset) {
3707 fLines->RemoveLines(lineIndex + 1);
3710 nextLine = (*fLines)[lineIndex];
3711 if (nextLine->offset == saveLine.offset) {
3712 if (nextLine->offset >= recalThreshold) {
3713 if (nextLine->origin != saveLine.origin)
3714 fLines->BumpOrigin(nextLine->origin - saveLine.origin,
3715 lineIndex + 1);
3716 break;
3718 } else {
3719 if (lineIndex > 0 && lineIndex == *startLine)
3720 *startLine = lineIndex - 1;
3724 curLine = (*fLines)[lineIndex];
3725 nextLine = curLine + 1;
3726 } while (curLine->offset < textLength);
3728 // make sure that the sentinel line (which starts at the end of the buffer)
3729 // has always a width of 0
3730 (*fLines)[fLines->NumLines()]->width = 0;
3732 // update the text rect
3733 float newHeight = TextHeight(0, fLines->NumLines() - 1);
3734 fTextRect.bottom = fTextRect.top + newHeight;
3735 if (!fWrap) {
3736 fMinTextRectWidth = fLines->MaxWidth();
3737 fTextRect.right = ceilf(fTextRect.left + fMinTextRectWidth);
3740 *endLine = lineIndex - 1;
3741 *startLine = min_c(*startLine, *endLine);
3745 int32
3746 BTextView::_FindLineBreak(int32 fromOffset, float* _ascent, float* _descent,
3747 float* inOutWidth)
3749 *_ascent = 0.0;
3750 *_descent = 0.0;
3752 const int32 limit = fText->Length();
3754 // is fromOffset at the end?
3755 if (fromOffset >= limit) {
3756 // try to return valid height info anyway
3757 if (fStyles->NumRuns() > 0) {
3758 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent,
3759 _descent);
3760 } else {
3761 if (fStyles->IsValidNullStyle()) {
3762 const BFont* font = NULL;
3763 fStyles->GetNullStyle(&font, NULL);
3765 font_height fh;
3766 font->GetHeight(&fh);
3767 *_ascent = fh.ascent;
3768 *_descent = fh.descent + fh.leading;
3771 *inOutWidth = 0;
3773 return limit;
3776 int32 offset = fromOffset;
3778 if (!fWrap) {
3779 // Text wrapping is turned off.
3780 // Just find the offset of the first \n character
3781 offset = limit - fromOffset;
3782 fText->FindChar(B_ENTER, fromOffset, &offset);
3783 offset += fromOffset;
3784 int32 toOffset = (offset < limit) ? offset : limit;
3786 *inOutWidth = _TabExpandedStyledWidth(fromOffset, toOffset - fromOffset,
3787 _ascent, _descent);
3789 return offset < limit ? offset + 1 : limit;
3792 bool done = false;
3793 float ascent = 0.0;
3794 float descent = 0.0;
3795 int32 delta = 0;
3796 float deltaWidth = 0.0;
3797 float strWidth = 0.0;
3798 uchar theChar;
3800 // wrap the text
3801 while (offset < limit && !done) {
3802 // find the next line break candidate
3803 for (; (offset + delta) < limit; delta++) {
3804 if (CanEndLine(offset + delta)) {
3805 theChar = fText->RealCharAt(offset + delta);
3806 if (theChar != B_SPACE && theChar != B_TAB
3807 && theChar != B_ENTER) {
3808 // we are scanning for trailing whitespace below, so we
3809 // have to skip non-whitespace characters, that can end
3810 // the line, here
3811 delta++;
3813 break;
3817 int32 deltaBeforeWhitespace = delta;
3818 // now skip over trailing whitespace, if any
3819 for (; (offset + delta) < limit; delta++) {
3820 theChar = fText->RealCharAt(offset + delta);
3821 if (theChar == B_ENTER) {
3822 // found a newline, we're done!
3823 done = true;
3824 delta++;
3825 break;
3826 } else if (theChar != B_SPACE && theChar != B_TAB) {
3827 // stop at anything else than trailing whitespace
3828 break;
3832 delta = max_c(delta, 1);
3834 // do not include B_ENTER-terminator into width & height calculations
3835 deltaWidth = _TabExpandedStyledWidth(offset,
3836 done ? delta - 1 : delta, &ascent, &descent);
3837 strWidth += deltaWidth;
3839 if (strWidth >= *inOutWidth) {
3840 // we've found where the line will wrap
3841 done = true;
3843 // we have included trailing whitespace in the width computation
3844 // above, but that is not being shown anyway, so we try again
3845 // without the trailing whitespace
3846 if (delta == deltaBeforeWhitespace) {
3847 // there is no trailing whitespace, no point in trying
3848 break;
3851 // reset string width to start of current run ...
3852 strWidth -= deltaWidth;
3854 // ... and compute the resulting width (of visible characters)
3855 strWidth += _StyledWidth(offset, deltaBeforeWhitespace, NULL, NULL);
3856 if (strWidth >= *inOutWidth) {
3857 // width of visible characters exceeds line, we need to wrap
3858 // before the current "word"
3859 break;
3863 *_ascent = max_c(ascent, *_ascent);
3864 *_descent = max_c(descent, *_descent);
3866 offset += delta;
3867 delta = 0;
3870 if (offset - fromOffset < 1) {
3871 // there weren't any words that fit entirely in this line
3872 // force a break in the middle of a word
3873 *_ascent = 0.0;
3874 *_descent = 0.0;
3875 strWidth = 0.0;
3877 int32 current = fromOffset;
3878 for (offset = _NextInitialByte(current); current < limit;
3879 current = offset, offset = _NextInitialByte(offset)) {
3880 strWidth += _StyledWidth(current, offset - current, &ascent,
3881 &descent);
3882 if (strWidth >= *inOutWidth) {
3883 offset = _PreviousInitialByte(offset);
3884 break;
3887 *_ascent = max_c(ascent, *_ascent);
3888 *_descent = max_c(descent, *_descent);
3892 return min_c(offset, limit);
3896 int32
3897 BTextView::_PreviousLineStart(int32 offset)
3899 if (offset <= 0)
3900 return 0;
3902 while (offset > 0) {
3903 offset = _PreviousInitialByte(offset);
3904 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE
3905 && ByteAt(offset) == B_ENTER) {
3906 return offset + 1;
3910 return offset;
3914 int32
3915 BTextView::_NextLineEnd(int32 offset)
3917 int32 textLen = fText->Length();
3918 if (offset >= textLen)
3919 return textLen;
3921 while (offset < textLen) {
3922 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE
3923 && ByteAt(offset) == B_ENTER) {
3924 break;
3926 offset = _NextInitialByte(offset);
3929 return offset;
3933 int32
3934 BTextView::_PreviousWordBoundary(int32 offset)
3936 uint32 charType = _CharClassification(offset);
3937 int32 previous;
3938 while (offset > 0) {
3939 previous = _PreviousInitialByte(offset);
3940 if (_CharClassification(previous) != charType)
3941 break;
3942 offset = previous;
3945 return offset;
3949 int32
3950 BTextView::_NextWordBoundary(int32 offset)
3952 int32 textLen = fText->Length();
3953 uint32 charType = _CharClassification(offset);
3954 while (offset < textLen) {
3955 offset = _NextInitialByte(offset);
3956 if (_CharClassification(offset) != charType)
3957 break;
3960 return offset;
3964 int32
3965 BTextView::_PreviousWordStart(int32 offset)
3967 if (offset <= 1)
3968 return 0;
3970 --offset;
3971 // need to look at previous char
3972 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) {
3973 // skip non-word characters
3974 while (offset > 0) {
3975 offset = _PreviousInitialByte(offset);
3976 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT)
3977 break;
3980 while (offset > 0) {
3981 // skip to start of word
3982 int32 previous = _PreviousInitialByte(offset);
3983 if (_CharClassification(previous) != CHAR_CLASS_DEFAULT)
3984 break;
3985 offset = previous;
3988 return offset;
3992 int32
3993 BTextView::_NextWordEnd(int32 offset)
3995 int32 textLen = fText->Length();
3996 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) {
3997 // skip non-word characters
3998 while (offset < textLen) {
3999 offset = _NextInitialByte(offset);
4000 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT)
4001 break;
4004 while (offset < textLen) {
4005 // skip to end of word
4006 offset = _NextInitialByte(offset);
4007 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT)
4008 break;
4011 return offset;
4015 /*! Returns the width used by the characters starting at the given
4016 offset with the given length, expanding all tab characters as needed.
4018 float
4019 BTextView::_TabExpandedStyledWidth(int32 offset, int32 length, float* _ascent,
4020 float* _descent) const
4022 float ascent = 0.0;
4023 float descent = 0.0;
4024 float maxAscent = 0.0;
4025 float maxDescent = 0.0;
4027 float width = 0.0;
4028 int32 numBytes = length;
4029 bool foundTab = false;
4030 do {
4031 foundTab = fText->FindChar(B_TAB, offset, &numBytes);
4032 width += _StyledWidth(offset, numBytes, &ascent, &descent);
4034 if (maxAscent < ascent)
4035 maxAscent = ascent;
4036 if (maxDescent < descent)
4037 maxDescent = descent;
4039 if (foundTab) {
4040 width += _ActualTabWidth(width);
4041 numBytes++;
4044 offset += numBytes;
4045 length -= numBytes;
4046 numBytes = length;
4047 } while (foundTab && length > 0);
4049 if (_ascent != NULL)
4050 *_ascent = maxAscent;
4051 if (_descent != NULL)
4052 *_descent = maxDescent;
4054 return width;
4058 /*! Calculate the width of the text within the given limits.
4060 \param fromOffset The offset where to start.
4061 \param length The length of the text to examine.
4062 \param _ascent A pointer to a float which will contain the maximum ascent.
4063 \param _descent A pointer to a float which will contain the maximum descent.
4065 \return The width for the text within the given limits.
4067 float
4068 BTextView::_StyledWidth(int32 fromOffset, int32 length, float* _ascent,
4069 float* _descent) const
4071 if (length == 0) {
4072 // determine height of char at given offset, but return empty width
4073 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent,
4074 _descent);
4075 return 0.0;
4078 float result = 0.0;
4079 float ascent = 0.0;
4080 float descent = 0.0;
4081 float maxAscent = 0.0;
4082 float maxDescent = 0.0;
4084 // iterate through the style runs
4085 const BFont* font = NULL;
4086 int32 numBytes;
4087 while ((numBytes = fStyles->Iterate(fromOffset, length, fInline, &font,
4088 NULL, &ascent, &descent)) != 0) {
4089 maxAscent = max_c(ascent, maxAscent);
4090 maxDescent = max_c(descent, maxDescent);
4092 #if USE_WIDTHBUFFER
4093 // Use _BWidthBuffer_ if possible
4094 if (BPrivate::gWidthBuffer != NULL) {
4095 result += BPrivate::gWidthBuffer->StringWidth(*fText, fromOffset,
4096 numBytes, font);
4097 } else {
4098 #endif
4099 const char* text = fText->GetString(fromOffset, &numBytes);
4100 result += font->StringWidth(text, numBytes);
4102 #if USE_WIDTHBUFFER
4104 #endif
4106 fromOffset += numBytes;
4107 length -= numBytes;
4110 if (_ascent != NULL)
4111 *_ascent = maxAscent;
4112 if (_descent != NULL)
4113 *_descent = maxDescent;
4115 return result;
4119 //! Calculate the actual tab width for the given location.
4120 float
4121 BTextView::_ActualTabWidth(float location) const
4123 float tabWidth = fTabWidth - fmod(location, fTabWidth);
4124 if (round(tabWidth) == 0)
4125 tabWidth = fTabWidth;
4127 return tabWidth;
4131 void
4132 BTextView::_DoInsertText(const char* text, int32 length, int32 offset,
4133 const text_run_array* runs)
4135 _CancelInputMethod();
4137 if (TextLength() + length > MaxBytes())
4138 return;
4140 if (fSelStart != fSelEnd)
4141 Select(fSelStart, fSelStart);
4143 const int32 textLength = TextLength();
4144 if (offset > textLength)
4145 offset = textLength;
4147 // copy data into buffer
4148 InsertText(text, length, offset, runs);
4150 // recalc line breaks and draw the text
4151 _Refresh(offset, offset + length, false);
4155 void
4156 BTextView::_DoDeleteText(int32 fromOffset, int32 toOffset)
4158 CALLED();
4162 void
4163 BTextView::_DrawLine(BView* view, const int32 &lineNum,
4164 const int32 &startOffset, const bool &erase, BRect &eraseRect,
4165 BRegion &inputRegion)
4167 STELine* line = (*fLines)[lineNum];
4168 float startLeft = fTextRect.left;
4169 if (startOffset != -1) {
4170 if (ByteAt(startOffset) == B_ENTER) {
4171 // StartOffset is a newline
4172 startLeft = PointAt(line->offset).x;
4173 } else
4174 startLeft = PointAt(startOffset).x;
4176 else if (fAlignment != B_ALIGN_LEFT) {
4177 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum);
4178 if (fAlignment == B_ALIGN_CENTER)
4179 alignmentOffset /= 2;
4180 startLeft = fTextRect.left + alignmentOffset;
4183 int32 length = (line + 1)->offset;
4184 if (startOffset != -1)
4185 length -= startOffset;
4186 else
4187 length -= line->offset;
4189 // DrawString() chokes if you draw a newline
4190 if (ByteAt((line + 1)->offset - 1) == B_ENTER)
4191 length--;
4193 view->MovePenTo(startLeft, line->origin + line->ascent + fTextRect.top + 1);
4195 if (erase) {
4196 eraseRect.top = line->origin + fTextRect.top;
4197 eraseRect.bottom = (line + 1)->origin + fTextRect.top;
4198 view->FillRect(eraseRect, B_SOLID_LOW);
4201 // do we have any text to draw?
4202 if (length <= 0)
4203 return;
4205 bool foundTab = false;
4206 int32 tabChars = 0;
4207 int32 numTabs = 0;
4208 int32 offset = startOffset != -1 ? startOffset : line->offset;
4209 const BFont* font = NULL;
4210 const rgb_color* color = NULL;
4211 int32 numBytes;
4212 drawing_mode defaultTextRenderingMode = DrawingMode();
4213 // iterate through each style on this line
4214 while ((numBytes = fStyles->Iterate(offset, length, fInline, &font,
4215 &color)) != 0) {
4216 view->SetFont(font);
4217 view->SetHighColor(*color);
4219 tabChars = min_c(numBytes, length);
4220 do {
4221 foundTab = fText->FindChar(B_TAB, offset, &tabChars);
4222 if (foundTab) {
4223 do {
4224 numTabs++;
4225 if (ByteAt(offset + tabChars + numTabs) != B_TAB)
4226 break;
4227 } while ((tabChars + numTabs) < numBytes);
4230 drawing_mode textRenderingMode = defaultTextRenderingMode;
4232 if (inputRegion.CountRects() > 0
4233 && ((offset <= fInline->Offset()
4234 && fInline->Offset() < offset + tabChars)
4235 || (fInline->Offset() <= offset
4236 && offset < fInline->Offset() + fInline->Length()))) {
4238 textRenderingMode = B_OP_OVER;
4240 BRegion textRegion;
4241 GetTextRegion(offset, offset + length, &textRegion);
4243 textRegion.IntersectWith(&inputRegion);
4244 view->PushState();
4246 // Highlight in blue the inputted text
4247 view->SetHighColor(kBlueInputColor);
4248 view->FillRect(textRegion.Frame());
4250 // Highlight in red the selected part
4251 if (fInline->SelectionLength() > 0) {
4252 BRegion selectedRegion;
4253 GetTextRegion(fInline->Offset()
4254 + fInline->SelectionOffset(), fInline->Offset()
4255 + fInline->SelectionOffset()
4256 + fInline->SelectionLength(), &selectedRegion);
4258 textRegion.IntersectWith(&selectedRegion);
4260 view->SetHighColor(kRedInputColor);
4261 view->FillRect(textRegion.Frame());
4264 view->PopState();
4267 int32 returnedBytes = tabChars;
4268 const char* stringToDraw = fText->GetString(offset, &returnedBytes);
4269 view->SetDrawingMode(textRenderingMode);
4270 view->DrawString(stringToDraw, returnedBytes);
4271 if (foundTab) {
4272 float penPos = PenLocation().x - fTextRect.left;
4273 float tabWidth = _ActualTabWidth(penPos);
4274 if (numTabs > 1)
4275 tabWidth += ((numTabs - 1) * fTabWidth);
4277 view->MovePenBy(tabWidth, 0.0);
4278 tabChars += numTabs;
4281 offset += tabChars;
4282 length -= tabChars;
4283 numBytes -= tabChars;
4284 tabChars = min_c(numBytes, length);
4285 numTabs = 0;
4286 } while (foundTab && tabChars > 0);
4291 void
4292 BTextView::_DrawLines(int32 startLine, int32 endLine, int32 startOffset,
4293 bool erase)
4295 if (!Window())
4296 return;
4298 // clip the text
4299 BRect textRect(fTextRect);
4300 float minWidth
4301 = Bounds().Width() - fLayoutData->leftInset - fLayoutData->rightInset;
4302 if (textRect.Width() < minWidth)
4303 textRect.right = textRect.left + minWidth;
4304 BRect clipRect = Bounds() & textRect;
4305 clipRect.InsetBy(-1, -1);
4307 BRegion newClip;
4308 newClip.Set(clipRect);
4309 ConstrainClippingRegion(&newClip);
4311 // set the low color to the view color so that
4312 // drawing to a non-white background will work
4313 SetLowColor(ViewColor());
4315 BView* view = NULL;
4316 if (fOffscreen == NULL)
4317 view = this;
4318 else {
4319 fOffscreen->Lock();
4320 view = fOffscreen->ChildAt(0);
4321 view->SetLowColor(ViewColor());
4322 view->FillRect(view->Bounds(), B_SOLID_LOW);
4325 long maxLine = fLines->NumLines() - 1;
4326 if (startLine < 0)
4327 startLine = 0;
4328 if (endLine > maxLine)
4329 endLine = maxLine;
4331 // TODO: See if we can avoid this
4332 if (fAlignment != B_ALIGN_LEFT)
4333 erase = true;
4335 BRect eraseRect = clipRect;
4336 int32 startEraseLine = startLine;
4337 STELine* line = (*fLines)[startLine];
4339 if (erase && startOffset != -1 && fAlignment == B_ALIGN_LEFT) {
4340 // erase only to the right of startOffset
4341 startEraseLine++;
4342 int32 startErase = startOffset;
4344 BPoint erasePoint = PointAt(startErase);
4345 eraseRect.left = erasePoint.x;
4346 eraseRect.top = erasePoint.y;
4347 eraseRect.bottom = (line + 1)->origin + fTextRect.top;
4349 view->FillRect(eraseRect, B_SOLID_LOW);
4351 eraseRect = clipRect;
4354 BRegion inputRegion;
4355 if (fInline != NULL && fInline->IsActive()) {
4356 GetTextRegion(fInline->Offset(), fInline->Offset() + fInline->Length(),
4357 &inputRegion);
4360 //BPoint leftTop(startLeft, line->origin);
4361 for (int32 lineNum = startLine; lineNum <= endLine; lineNum++) {
4362 const bool eraseThisLine = erase && lineNum >= startEraseLine;
4363 _DrawLine(view, lineNum, startOffset, eraseThisLine, eraseRect,
4364 inputRegion);
4365 startOffset = -1;
4366 // Set this to -1 so the next iteration will use the line offset
4369 // draw the caret/hilite the selection
4370 if (fActive) {
4371 if (fSelStart != fSelEnd) {
4372 if (fSelectable)
4373 Highlight(fSelStart, fSelEnd);
4374 } else {
4375 if (fCaretVisible)
4376 _DrawCaret(fSelStart, true);
4380 if (fOffscreen != NULL) {
4381 view->Sync();
4382 /*BPoint penLocation = view->PenLocation();
4383 BRect drawRect(leftTop.x, leftTop.y, penLocation.x, penLocation.y);
4384 DrawBitmap(fOffscreen, drawRect, drawRect);*/
4385 fOffscreen->Unlock();
4388 ConstrainClippingRegion(NULL);
4392 void
4393 BTextView::_RequestDrawLines(int32 startLine, int32 endLine)
4395 if (!Window())
4396 return;
4398 long maxLine = fLines->NumLines() - 1;
4400 STELine* from = (*fLines)[startLine];
4401 STELine* to = endLine == maxLine ? NULL : (*fLines)[endLine + 1];
4402 BRect invalidRect(Bounds().left, from->origin + fTextRect.top,
4403 Bounds().right,
4404 to != NULL ? to->origin + fTextRect.top : fTextRect.bottom);
4405 Invalidate(invalidRect);
4406 Window()->UpdateIfNeeded();
4410 void
4411 BTextView::_DrawCaret(int32 offset, bool visible)
4413 float lineHeight;
4414 BPoint caretPoint = PointAt(offset, &lineHeight);
4415 caretPoint.x = min_c(caretPoint.x, fTextRect.right);
4417 BRect caretRect;
4418 caretRect.left = caretRect.right = caretPoint.x;
4419 caretRect.top = caretPoint.y;
4420 caretRect.bottom = caretPoint.y + lineHeight - 1;
4422 if (visible)
4423 InvertRect(caretRect);
4424 else
4425 Invalidate(caretRect);
4429 inline void
4430 BTextView::_ShowCaret()
4432 if (fActive && !fCaretVisible && fEditable && fSelStart == fSelEnd)
4433 _InvertCaret();
4437 inline void
4438 BTextView::_HideCaret()
4440 if (fCaretVisible && fSelStart == fSelEnd)
4441 _InvertCaret();
4445 //! Hides the caret if it is being shown, and if it's hidden, shows it.
4446 void
4447 BTextView::_InvertCaret()
4449 fCaretVisible = !fCaretVisible;
4450 _DrawCaret(fSelStart, fCaretVisible);
4451 fCaretTime = system_time();
4455 /*! Place the dragging caret at the given offset.
4457 \param offset The offset (zero based within the object's text) where to
4458 place the dragging caret. If it's -1, hide the caret.
4460 void
4461 BTextView::_DragCaret(int32 offset)
4463 // does the caret need to move?
4464 if (offset == fDragOffset)
4465 return;
4467 // hide the previous drag caret
4468 if (fDragOffset != -1)
4469 _DrawCaret(fDragOffset, false);
4471 // do we have a new location?
4472 if (offset != -1) {
4473 if (fActive) {
4474 // ignore if offset is within active selection
4475 if (offset >= fSelStart && offset <= fSelEnd) {
4476 fDragOffset = -1;
4477 return;
4481 _DrawCaret(offset, true);
4484 fDragOffset = offset;
4488 void
4489 BTextView::_StopMouseTracking()
4491 delete fTrackingMouse;
4492 fTrackingMouse = NULL;
4496 bool
4497 BTextView::_PerformMouseUp(BPoint where)
4499 if (fTrackingMouse == NULL)
4500 return false;
4502 if (fTrackingMouse->selectionRect.IsValid())
4503 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset);
4505 _StopMouseTracking();
4506 // adjust cursor if necessary
4507 _TrackMouse(where, NULL, true);
4509 return true;
4513 bool
4514 BTextView::_PerformMouseMoved(BPoint where, uint32 code)
4516 fWhere = where;
4518 if (fTrackingMouse == NULL)
4519 return false;
4521 int32 currentOffset = OffsetAt(where);
4522 if (fTrackingMouse->selectionRect.IsValid()) {
4523 // we are tracking the mouse for drag action, if the mouse has moved
4524 // to another index or more than three pixels from where it was clicked,
4525 // we initiate a drag now:
4526 if (currentOffset != fTrackingMouse->clickOffset
4527 || fabs(fTrackingMouse->where.x - where.x) > 3
4528 || fabs(fTrackingMouse->where.y - where.y) > 3) {
4529 _StopMouseTracking();
4530 _InitiateDrag();
4531 return true;
4533 return false;
4536 switch (fClickCount) {
4537 case 3:
4538 // triple click, extend selection linewise
4539 if (currentOffset <= fTrackingMouse->anchor) {
4540 fTrackingMouse->selStart
4541 = (*fLines)[_LineAt(currentOffset)]->offset;
4542 fTrackingMouse->selEnd = fTrackingMouse->shiftDown
4543 ? fSelEnd
4544 : (*fLines)[_LineAt(fTrackingMouse->anchor) + 1]->offset;
4545 } else {
4546 fTrackingMouse->selStart
4547 = fTrackingMouse->shiftDown
4548 ? fSelStart
4549 : (*fLines)[_LineAt(fTrackingMouse->anchor)]->offset;
4550 fTrackingMouse->selEnd
4551 = (*fLines)[_LineAt(currentOffset) + 1]->offset;
4553 break;
4555 case 2:
4556 // double click, extend selection wordwise
4557 if (currentOffset <= fTrackingMouse->anchor) {
4558 fTrackingMouse->selStart = _PreviousWordBoundary(currentOffset);
4559 fTrackingMouse->selEnd
4560 = fTrackingMouse->shiftDown
4561 ? fSelEnd
4562 : _NextWordBoundary(fTrackingMouse->anchor);
4563 } else {
4564 fTrackingMouse->selStart
4565 = fTrackingMouse->shiftDown
4566 ? fSelStart
4567 : _PreviousWordBoundary(fTrackingMouse->anchor);
4568 fTrackingMouse->selEnd = _NextWordBoundary(currentOffset);
4570 break;
4572 default:
4573 // new click, extend selection char by char
4574 if (currentOffset <= fTrackingMouse->anchor) {
4575 fTrackingMouse->selStart = currentOffset;
4576 fTrackingMouse->selEnd
4577 = fTrackingMouse->shiftDown
4578 ? fSelEnd : fTrackingMouse->anchor;
4579 } else {
4580 fTrackingMouse->selStart
4581 = fTrackingMouse->shiftDown
4582 ? fSelStart : fTrackingMouse->anchor;
4583 fTrackingMouse->selEnd = currentOffset;
4585 break;
4588 // position caret to follow the direction of the selection
4589 if (fTrackingMouse->selEnd != fSelEnd)
4590 fCaretOffset = fTrackingMouse->selEnd;
4591 else if (fTrackingMouse->selStart != fSelStart)
4592 fCaretOffset = fTrackingMouse->selStart;
4594 Select(fTrackingMouse->selStart, fTrackingMouse->selEnd);
4595 _TrackMouse(where, NULL);
4597 return true;
4601 /*! Tracks the mouse position, doing special actions like changing the
4602 view cursor.
4604 \param where The point where the mouse has moved.
4605 \param message The dragging message, if there is any.
4606 \param force Passed as second parameter of SetViewCursor()
4608 void
4609 BTextView::_TrackMouse(BPoint where, const BMessage* message, bool force)
4611 BRegion textRegion;
4612 GetTextRegion(fSelStart, fSelEnd, &textRegion);
4614 if (message && AcceptsDrop(message))
4615 _TrackDrag(where);
4616 else if ((fSelectable || fEditable)
4617 && (fTrackingMouse != NULL || !textRegion.Contains(where))) {
4618 SetViewCursor(B_CURSOR_I_BEAM, force);
4619 } else
4620 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT, force);
4624 //! Tracks the mouse position when the user is dragging some data.
4625 void
4626 BTextView::_TrackDrag(BPoint where)
4628 CALLED();
4629 if (Bounds().Contains(where))
4630 _DragCaret(OffsetAt(where));
4634 //! Initiates a drag operation.
4635 void
4636 BTextView::_InitiateDrag()
4638 BMessage dragMessage(B_MIME_DATA);
4639 BBitmap* dragBitmap = NULL;
4640 BPoint bitmapPoint;
4641 BHandler* dragHandler = NULL;
4643 GetDragParameters(&dragMessage, &dragBitmap, &bitmapPoint, &dragHandler);
4644 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
4646 if (dragBitmap != NULL)
4647 DragMessage(&dragMessage, dragBitmap, bitmapPoint, dragHandler);
4648 else {
4649 BRegion region;
4650 GetTextRegion(fSelStart, fSelEnd, &region);
4651 BRect bounds = Bounds();
4652 BRect dragRect = region.Frame();
4653 if (!bounds.Contains(dragRect))
4654 dragRect = bounds & dragRect;
4656 DragMessage(&dragMessage, dragRect, dragHandler);
4659 BMessenger messenger(this);
4660 BMessage message(_DISPOSE_DRAG_);
4661 fDragRunner = new (nothrow) BMessageRunner(messenger, &message, 100000);
4665 //! Handles when some data is dropped on the view.
4666 bool
4667 BTextView::_MessageDropped(BMessage* message, BPoint where, BPoint offset)
4669 ASSERT(message);
4671 void* from = NULL;
4672 bool internalDrop = false;
4673 if (message->FindPointer("be:originator", &from) == B_OK
4674 && from == this && fSelEnd != fSelStart)
4675 internalDrop = true;
4677 _DragCaret(-1);
4679 delete fDragRunner;
4680 fDragRunner = NULL;
4682 _TrackMouse(where, NULL);
4684 // are we sure we like this message?
4685 if (!AcceptsDrop(message))
4686 return false;
4688 int32 dropOffset = OffsetAt(where);
4689 if (dropOffset > TextLength())
4690 dropOffset = TextLength();
4692 // if this view initiated the drag, move instead of copy
4693 if (internalDrop) {
4694 // dropping onto itself?
4695 if (dropOffset >= fSelStart && dropOffset <= fSelEnd)
4696 return true;
4699 ssize_t dataLength = 0;
4700 const char* text = NULL;
4701 entry_ref ref;
4702 if (message->FindData("text/plain", B_MIME_TYPE, (const void**)&text,
4703 &dataLength) == B_OK) {
4704 text_run_array* runArray = NULL;
4705 ssize_t runLength = 0;
4706 if (fStylable) {
4707 message->FindData("application/x-vnd.Be-text_run_array",
4708 B_MIME_TYPE, (const void**)&runArray, &runLength);
4711 _FilterDisallowedChars((char*)text, dataLength, runArray);
4713 if (dataLength < 1) {
4714 beep();
4715 return true;
4718 if (fUndo) {
4719 delete fUndo;
4720 fUndo = new DropUndoBuffer(this, text, dataLength, runArray,
4721 runLength, dropOffset, internalDrop);
4724 if (internalDrop) {
4725 if (dropOffset > fSelEnd)
4726 dropOffset -= dataLength;
4727 Delete();
4730 Insert(dropOffset, text, dataLength, runArray);
4733 return true;
4737 void
4738 BTextView::_PerformAutoScrolling()
4740 // Scroll the view a bit if mouse is outside the view bounds
4741 BRect bounds = Bounds();
4742 BPoint scrollBy(B_ORIGIN);
4744 // R5 does a pretty soft auto-scroll, we try to do the same by
4745 // simply scrolling the distance between cursor and border
4746 if (fWhere.x > bounds.right) {
4747 scrollBy.x = fWhere.x - bounds.right;
4748 } else if (fWhere.x < bounds.left) {
4749 scrollBy.x = fWhere.x - bounds.left; // negative value
4752 // prevent from scrolling out of view
4753 if (scrollBy.x != 0.0) {
4754 float rightMax = floorf(fTextRect.right + fLayoutData->rightInset);
4755 if (bounds.right + scrollBy.x > rightMax)
4756 scrollBy.x = rightMax - bounds.right;
4757 if (bounds.left + scrollBy.x < 0)
4758 scrollBy.x = -bounds.left;
4761 if (CountLines() > 1) {
4762 // scroll in Y only if multiple lines!
4763 if (fWhere.y > bounds.bottom) {
4764 scrollBy.y = fWhere.y - bounds.bottom;
4765 } else if (fWhere.y < bounds.top) {
4766 scrollBy.y = fWhere.y - bounds.top; // negative value
4769 // prevent from scrolling out of view
4770 if (scrollBy.y != 0.0) {
4771 float bottomMax = floorf(fTextRect.bottom
4772 + fLayoutData->bottomInset);
4773 if (bounds.bottom + scrollBy.y > bottomMax)
4774 scrollBy.y = bottomMax - bounds.bottom;
4775 if (bounds.top + scrollBy.y < 0)
4776 scrollBy.y = -bounds.top;
4780 if (scrollBy != B_ORIGIN)
4781 ScrollBy(scrollBy.x, scrollBy.y);
4785 //! Updates the scrollbars associated with the object (if any).
4786 void
4787 BTextView::_UpdateScrollbars()
4789 BRect bounds(Bounds());
4790 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL);
4791 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL);
4793 // do we have a horizontal scroll bar?
4794 if (horizontalScrollBar != NULL) {
4795 long viewWidth = bounds.IntegerWidth();
4796 long dataWidth = (long)ceilf(fTextRect.IntegerWidth()
4797 + fLayoutData->leftInset + fLayoutData->rightInset);
4799 long maxRange = dataWidth - viewWidth;
4800 maxRange = max_c(maxRange, 0);
4802 horizontalScrollBar->SetRange(0, (float)maxRange);
4803 horizontalScrollBar->SetProportion((float)viewWidth / (float)dataWidth);
4804 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10);
4807 // how about a vertical scroll bar?
4808 if (verticalScrollBar != NULL) {
4809 long viewHeight = bounds.IntegerHeight();
4810 long dataHeight = (long)ceilf(fTextRect.IntegerHeight()
4811 + fLayoutData->topInset + fLayoutData->bottomInset);
4813 long maxRange = dataHeight - viewHeight;
4814 maxRange = max_c(maxRange, 0);
4816 verticalScrollBar->SetRange(0, maxRange);
4817 verticalScrollBar->SetProportion((float)viewHeight / (float)dataHeight);
4818 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight);
4823 //! Scrolls by the given offsets
4824 void
4825 BTextView::_ScrollBy(float horizontal, float vertical)
4827 BRect bounds = Bounds();
4828 _ScrollTo(bounds.left + horizontal, bounds.top + vertical);
4832 //! Scrolls to the given position, making sure not to scroll out of bounds.
4833 void
4834 BTextView::_ScrollTo(float x, float y)
4836 BRect bounds = Bounds();
4837 long viewWidth = bounds.IntegerWidth();
4838 long viewHeight = bounds.IntegerHeight();
4840 if (x > fTextRect.right - viewWidth)
4841 x = fTextRect.right - viewWidth;
4842 if (x < 0.0)
4843 x = 0.0;
4845 if (y > fTextRect.bottom + fLayoutData->bottomInset - viewHeight)
4846 y = fTextRect.bottom + fLayoutData->bottomInset - viewHeight;
4847 if (y < 0.0)
4848 y = 0.0;
4850 ScrollTo(x, y);
4854 //! Autoresizes the view to fit the contained text.
4855 void
4856 BTextView::_AutoResize(bool redraw)
4858 if (!fResizable)
4859 return;
4861 BRect bounds = Bounds();
4862 float oldWidth = bounds.Width();
4863 float newWidth = ceilf(fLayoutData->leftInset + fTextRect.Width()
4864 + fLayoutData->rightInset);
4866 if (fContainerView != NULL) {
4867 // NOTE: This container view thing is only used by Tracker.
4868 // move container view if not left aligned
4869 if (fAlignment == B_ALIGN_CENTER) {
4870 if (fmod(ceilf(newWidth - oldWidth), 2.0) != 0.0)
4871 newWidth += 1;
4872 fContainerView->MoveBy(ceilf(oldWidth - newWidth) / 2, 0);
4873 } else if (fAlignment == B_ALIGN_RIGHT) {
4874 fContainerView->MoveBy(ceilf(oldWidth - newWidth), 0);
4876 // resize container view
4877 fContainerView->ResizeBy(ceilf(newWidth - oldWidth), 0);
4881 if (redraw)
4882 _RequestDrawLines(0, 0);
4884 // erase any potential left over outside the text rect
4885 // (can only be on right hand side)
4886 BRect dirty(fTextRect.right + 1, fTextRect.top, bounds.right,
4887 fTextRect.bottom);
4888 if (dirty.IsValid()) {
4889 SetLowColor(ViewColor());
4890 FillRect(dirty, B_SOLID_LOW);
4895 //! Creates a new offscreen BBitmap with an associated BView.
4896 void
4897 BTextView::_NewOffscreen(float padding)
4899 if (fOffscreen != NULL)
4900 _DeleteOffscreen();
4902 #if USE_DOUBLEBUFFERING
4903 BRect bitmapRect(0, 0, fTextRect.Width() + padding, fTextRect.Height());
4904 fOffscreen = new BBitmap(bitmapRect, fColorSpace, true, false);
4905 if (fOffscreen != NULL && fOffscreen->Lock()) {
4906 BView* bufferView = new BView(bitmapRect, "drawing view", 0, 0);
4907 fOffscreen->AddChild(bufferView);
4908 fOffscreen->Unlock();
4910 #endif
4914 //! Deletes the textview's offscreen bitmap, if any.
4915 void
4916 BTextView::_DeleteOffscreen()
4918 if (fOffscreen != NULL && fOffscreen->Lock()) {
4919 delete fOffscreen;
4920 fOffscreen = NULL;
4925 /*! Creates a new offscreen bitmap, highlight the selection, and set the
4926 cursor to \c B_CURSOR_I_BEAM.
4928 void
4929 BTextView::_Activate()
4931 fActive = true;
4933 // Create a new offscreen BBitmap
4934 _NewOffscreen();
4936 if (fSelStart != fSelEnd) {
4937 if (fSelectable)
4938 Highlight(fSelStart, fSelEnd);
4939 } else
4940 _ShowCaret();
4942 BPoint where;
4943 uint32 buttons;
4944 GetMouse(&where, &buttons, false);
4945 if (Bounds().Contains(where))
4946 _TrackMouse(where, NULL);
4948 if (Window() != NULL) {
4949 BMessage* message;
4951 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY)
4952 && !Window()->HasShortcut(B_RIGHT_ARROW, B_COMMAND_KEY)) {
4953 message = new BMessage(kMsgNavigateArrow);
4954 message->AddInt32("key", B_LEFT_ARROW);
4955 message->AddInt32("modifiers", B_COMMAND_KEY);
4956 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY, message, this);
4958 message = new BMessage(kMsgNavigateArrow);
4959 message->AddInt32("key", B_RIGHT_ARROW);
4960 message->AddInt32("modifiers", B_COMMAND_KEY);
4961 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY, message, this);
4963 fInstalledNavigateCommandWordwiseShortcuts = true;
4965 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY)
4966 && !Window()->HasShortcut(B_RIGHT_ARROW,
4967 B_COMMAND_KEY | B_SHIFT_KEY)) {
4968 message = new BMessage(kMsgNavigateArrow);
4969 message->AddInt32("key", B_LEFT_ARROW);
4970 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
4971 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY,
4972 message, this);
4974 message = new BMessage(kMsgNavigateArrow);
4975 message->AddInt32("key", B_RIGHT_ARROW);
4976 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
4977 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY,
4978 message, this);
4980 fInstalledSelectCommandWordwiseShortcuts = true;
4983 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY)
4984 && !Window()->HasShortcut(B_RIGHT_ARROW, B_OPTION_KEY)) {
4985 message = new BMessage(kMsgNavigateArrow);
4986 message->AddInt32("key", B_LEFT_ARROW);
4987 message->AddInt32("modifiers", B_OPTION_KEY);
4988 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY, message, this);
4990 message = new BMessage(kMsgNavigateArrow);
4991 message->AddInt32("key", B_RIGHT_ARROW);
4992 message->AddInt32("modifiers", B_OPTION_KEY);
4993 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY, message, this);
4995 fInstalledNavigateOptionWordwiseShortcuts = true;
4997 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY)
4998 && !Window()->HasShortcut(B_RIGHT_ARROW,
4999 B_OPTION_KEY | B_SHIFT_KEY)) {
5000 message = new BMessage(kMsgNavigateArrow);
5001 message->AddInt32("key", B_LEFT_ARROW);
5002 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5003 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5004 message, this);
5006 message = new BMessage(kMsgNavigateArrow);
5007 message->AddInt32("key", B_RIGHT_ARROW);
5008 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5009 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5010 message, this);
5012 fInstalledSelectOptionWordwiseShortcuts = true;
5015 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY)
5016 && !Window()->HasShortcut(B_DOWN_ARROW, B_OPTION_KEY)) {
5017 message = new BMessage(kMsgNavigateArrow);
5018 message->AddInt32("key", B_UP_ARROW);
5019 message->AddInt32("modifiers", B_OPTION_KEY);
5020 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY, message, this);
5022 message = new BMessage(kMsgNavigateArrow);
5023 message->AddInt32("key", B_DOWN_ARROW);
5024 message->AddInt32("modifiers", B_OPTION_KEY);
5025 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY, message, this);
5027 fInstalledNavigateOptionLinewiseShortcuts = true;
5029 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY)
5030 && !Window()->HasShortcut(B_DOWN_ARROW,
5031 B_OPTION_KEY | B_SHIFT_KEY)) {
5032 message = new BMessage(kMsgNavigateArrow);
5033 message->AddInt32("key", B_UP_ARROW);
5034 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5035 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5036 message, this);
5038 message = new BMessage(kMsgNavigateArrow);
5039 message->AddInt32("key", B_DOWN_ARROW);
5040 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5041 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5042 message, this);
5044 fInstalledSelectOptionLinewiseShortcuts = true;
5047 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY)
5048 && !Window()->HasShortcut(B_END, B_COMMAND_KEY)) {
5049 message = new BMessage(kMsgNavigatePage);
5050 message->AddInt32("key", B_HOME);
5051 message->AddInt32("modifiers", B_COMMAND_KEY);
5052 Window()->AddShortcut(B_HOME, B_COMMAND_KEY, message, this);
5054 message = new BMessage(kMsgNavigatePage);
5055 message->AddInt32("key", B_END);
5056 message->AddInt32("modifiers", B_COMMAND_KEY);
5057 Window()->AddShortcut(B_END, B_COMMAND_KEY, message, this);
5059 fInstalledNavigateHomeEndDocwiseShortcuts = true;
5061 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY)
5062 && !Window()->HasShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY)) {
5063 message = new BMessage(kMsgNavigatePage);
5064 message->AddInt32("key", B_HOME);
5065 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
5066 Window()->AddShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY,
5067 message, this);
5069 message = new BMessage(kMsgNavigatePage);
5070 message->AddInt32("key", B_END);
5071 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
5072 Window()->AddShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY,
5073 message, this);
5075 fInstalledSelectHomeEndDocwiseShortcuts = true;
5081 //! Unhilights the selection, set the cursor to \c B_CURSOR_SYSTEM_DEFAULT.
5082 void
5083 BTextView::_Deactivate()
5085 fActive = false;
5087 _CancelInputMethod();
5088 _DeleteOffscreen();
5090 if (fSelStart != fSelEnd) {
5091 if (fSelectable)
5092 Highlight(fSelStart, fSelEnd);
5093 } else
5094 _HideCaret();
5096 if (Window() != NULL) {
5097 if (fInstalledNavigateCommandWordwiseShortcuts) {
5098 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY);
5099 Window()->RemoveShortcut(B_RIGHT_ARROW, B_COMMAND_KEY);
5100 fInstalledNavigateCommandWordwiseShortcuts = false;
5102 if (fInstalledSelectCommandWordwiseShortcuts) {
5103 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY);
5104 Window()->RemoveShortcut(B_RIGHT_ARROW,
5105 B_COMMAND_KEY | B_SHIFT_KEY);
5106 fInstalledSelectCommandWordwiseShortcuts = false;
5109 if (fInstalledNavigateOptionWordwiseShortcuts) {
5110 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY);
5111 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY);
5112 fInstalledNavigateOptionWordwiseShortcuts = false;
5114 if (fInstalledSelectOptionWordwiseShortcuts) {
5115 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5116 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5117 fInstalledSelectOptionWordwiseShortcuts = false;
5120 if (fInstalledNavigateOptionLinewiseShortcuts) {
5121 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY);
5122 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY);
5123 fInstalledNavigateOptionLinewiseShortcuts = false;
5125 if (fInstalledSelectOptionLinewiseShortcuts) {
5126 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5127 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5128 fInstalledSelectOptionLinewiseShortcuts = false;
5131 if (fInstalledNavigateHomeEndDocwiseShortcuts) {
5132 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY);
5133 Window()->RemoveShortcut(B_END, B_COMMAND_KEY);
5134 fInstalledNavigateHomeEndDocwiseShortcuts = false;
5136 if (fInstalledSelectHomeEndDocwiseShortcuts) {
5137 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY);
5138 Window()->RemoveShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY);
5139 fInstalledSelectHomeEndDocwiseShortcuts = false;
5145 /*! Changes the passed in font to be displayable by the object.
5147 Set font rotation to 0, removes any font flag, set font spacing
5148 to \c B_BITMAP_SPACING and font encoding to \c B_UNICODE_UTF8.
5150 void
5151 BTextView::_NormalizeFont(BFont* font)
5153 if (font) {
5154 font->SetRotation(0.0f);
5155 font->SetFlags(0);
5156 font->SetSpacing(B_BITMAP_SPACING);
5157 font->SetEncoding(B_UNICODE_UTF8);
5162 void
5163 BTextView::_SetRunArray(int32 startOffset, int32 endOffset,
5164 const text_run_array* runs)
5166 const int32 numStyles = runs->count;
5167 if (numStyles > 0) {
5168 const text_run* theRun = &runs->runs[0];
5169 for (int32 index = 0; index < numStyles; index++) {
5170 int32 fromOffset = theRun->offset + startOffset;
5171 int32 toOffset = endOffset;
5172 if (index + 1 < numStyles) {
5173 toOffset = (theRun + 1)->offset + startOffset;
5174 toOffset = (toOffset > endOffset) ? endOffset : toOffset;
5177 _ApplyStyleRange(fromOffset, toOffset, B_FONT_ALL, &theRun->font,
5178 &theRun->color, false);
5180 theRun++;
5182 fStyles->InvalidateNullStyle();
5187 /*! Returns the character class of the character at the given offset.
5189 \param offset The offset where the wanted character can be found.
5191 \return A value which represents the character's classification.
5193 uint32
5194 BTextView::_CharClassification(int32 offset) const
5196 // TODO: Should check against a list of characters containing also
5197 // japanese word breakers.
5198 // And what about other languages ? Isn't there a better way to check
5199 // for separator characters ?
5200 // Andrew suggested to have a look at UnicodeBlockObject.h
5201 switch (fText->RealCharAt(offset)) {
5202 case '\0':
5203 return CHAR_CLASS_END_OF_TEXT;
5205 case B_SPACE:
5206 case B_TAB:
5207 case B_ENTER:
5208 return CHAR_CLASS_WHITESPACE;
5210 case '=':
5211 case '+':
5212 case '@':
5213 case '#':
5214 case '$':
5215 case '%':
5216 case '^':
5217 case '&':
5218 case '*':
5219 case '\\':
5220 case '|':
5221 case '<':
5222 case '>':
5223 case '/':
5224 case '~':
5225 return CHAR_CLASS_GRAPHICAL;
5227 case '\'':
5228 case '"':
5229 return CHAR_CLASS_QUOTE;
5231 case ',':
5232 case '.':
5233 case '?':
5234 case '!':
5235 case ';':
5236 case ':':
5237 case '-':
5238 return CHAR_CLASS_PUNCTUATION;
5240 case '(':
5241 case '[':
5242 case '{':
5243 return CHAR_CLASS_PARENS_OPEN;
5245 case ')':
5246 case ']':
5247 case '}':
5248 return CHAR_CLASS_PARENS_CLOSE;
5250 default:
5251 return CHAR_CLASS_DEFAULT;
5256 /*! Returns the offset of the next UTF-8 character.
5258 \param offset The offset where to start looking.
5260 \return The offset of the next UTF-8 character.
5262 int32
5263 BTextView::_NextInitialByte(int32 offset) const
5265 if (offset >= fText->Length())
5266 return offset;
5268 for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset)
5271 return offset;
5275 /*! Returns the offset of the previous UTF-8 character.
5277 \param offset The offset where to start looking.
5279 \return The offset of the previous UTF-8 character.
5281 int32
5282 BTextView::_PreviousInitialByte(int32 offset) const
5284 if (offset <= 0)
5285 return 0;
5287 int32 count = 6;
5289 for (--offset; offset > 0 && count; --offset, --count) {
5290 if ((ByteAt(offset) & 0xC0) != 0x80)
5291 break;
5294 return count ? offset : 0;
5298 bool
5299 BTextView::_GetProperty(BMessage* specifier, int32 form, const char* property,
5300 BMessage* reply)
5302 CALLED();
5303 if (strcmp(property, "selection") == 0) {
5304 reply->what = B_REPLY;
5305 reply->AddInt32("result", fSelStart);
5306 reply->AddInt32("result", fSelEnd);
5307 reply->AddInt32("error", B_OK);
5309 return true;
5310 } else if (strcmp(property, "Text") == 0) {
5311 if (IsTypingHidden()) {
5312 // Do not allow stealing passwords via scripting
5313 beep();
5314 return false;
5317 int32 index, range;
5318 specifier->FindInt32("index", &index);
5319 specifier->FindInt32("range", &range);
5321 char* buffer = new char[range + 1];
5322 GetText(index, range, buffer);
5324 reply->what = B_REPLY;
5325 reply->AddString("result", buffer);
5326 reply->AddInt32("error", B_OK);
5328 delete[] buffer;
5330 return true;
5331 } else if (strcmp(property, "text_run_array") == 0)
5332 return false;
5334 return false;
5338 bool
5339 BTextView::_SetProperty(BMessage* specifier, int32 form, const char* property,
5340 BMessage* reply)
5342 CALLED();
5343 if (strcmp(property, "selection") == 0) {
5344 int32 index, range;
5346 specifier->FindInt32("index", &index);
5347 specifier->FindInt32("range", &range);
5349 Select(index, index + range);
5351 reply->what = B_REPLY;
5352 reply->AddInt32("error", B_OK);
5354 return true;
5355 } else if (strcmp(property, "Text") == 0) {
5356 int32 index, range;
5357 specifier->FindInt32("index", &index);
5358 specifier->FindInt32("range", &range);
5360 const char* buffer = NULL;
5361 if (specifier->FindString("data", &buffer) == B_OK)
5362 InsertText(buffer, range, index, NULL);
5363 else
5364 DeleteText(index, range);
5366 reply->what = B_REPLY;
5367 reply->AddInt32("error", B_OK);
5369 return true;
5370 } else if (strcmp(property, "text_run_array") == 0)
5371 return false;
5373 return false;
5377 bool
5378 BTextView::_CountProperties(BMessage* specifier, int32 form,
5379 const char* property, BMessage* reply)
5381 CALLED();
5382 if (strcmp(property, "Text") == 0) {
5383 reply->what = B_REPLY;
5384 reply->AddInt32("result", TextLength());
5385 reply->AddInt32("error", B_OK);
5386 return true;
5389 return false;
5393 //! Called when the object receives a \c B_INPUT_METHOD_CHANGED message.
5394 void
5395 BTextView::_HandleInputMethodChanged(BMessage* message)
5397 // TODO: block input if not editable (Andrew)
5398 ASSERT(fInline != NULL);
5400 const char* string = NULL;
5401 if (message->FindString("be:string", &string) < B_OK || string == NULL)
5402 return;
5404 _HideCaret();
5406 if (IsFocus())
5407 be_app->ObscureCursor();
5409 // If we find the "be:confirmed" boolean (and the boolean is true),
5410 // it means it's over for now, so the current InlineInput object
5411 // should become inactive. We will probably receive a
5412 // B_INPUT_METHOD_STOPPED message after this one.
5413 bool confirmed;
5414 if (message->FindBool("be:confirmed", &confirmed) != B_OK)
5415 confirmed = false;
5417 // Delete the previously inserted text (if any)
5418 if (fInline->IsActive()) {
5419 const int32 oldOffset = fInline->Offset();
5420 DeleteText(oldOffset, oldOffset + fInline->Length());
5421 if (confirmed)
5422 fInline->SetActive(false);
5423 fCaretOffset = fSelStart = fSelEnd = oldOffset;
5426 const int32 stringLen = strlen(string);
5428 fInline->SetOffset(fSelStart);
5429 fInline->SetLength(stringLen);
5430 fInline->ResetClauses();
5432 if (!confirmed && !fInline->IsActive())
5433 fInline->SetActive(true);
5435 // Get the clauses, and pass them to the InlineInput object
5436 // TODO: Find out if what we did it's ok, currently we don't consider
5437 // clauses at all, while the bebook says we should; though the visual
5438 // effect we obtained seems correct. Weird.
5439 int32 clauseCount = 0;
5440 int32 clauseStart;
5441 int32 clauseEnd;
5442 while (message->FindInt32("be:clause_start", clauseCount, &clauseStart)
5443 == B_OK
5444 && message->FindInt32("be:clause_end", clauseCount, &clauseEnd)
5445 == B_OK) {
5446 if (!fInline->AddClause(clauseStart, clauseEnd))
5447 break;
5448 clauseCount++;
5451 if (confirmed) {
5452 _Refresh(fSelStart, fSelEnd, true);
5453 _ShowCaret();
5455 // now we need to feed ourselves the individual characters as if the
5456 // user would have pressed them now - this lets KeyDown() pick out all
5457 // the special characters like B_BACKSPACE, cursor keys and the like:
5458 const char* currPos = string;
5459 const char* prevPos = currPos;
5460 while (*currPos != '\0') {
5461 if ((*currPos & 0xC0) == 0xC0) {
5462 // found the start of an UTF-8 char, we collect while it lasts
5463 ++currPos;
5464 while ((*currPos & 0xC0) == 0x80)
5465 ++currPos;
5466 } else if ((*currPos & 0xC0) == 0x80) {
5467 // illegal: character starts with utf-8 intermediate byte,
5468 // skip it
5469 prevPos = ++currPos;
5470 } else {
5471 // single byte character/code, just feed that
5472 ++currPos;
5474 KeyDown(prevPos, currPos - prevPos);
5475 prevPos = currPos;
5478 _Refresh(fSelStart, fSelEnd, true);
5479 } else {
5480 // temporarily show transient state of inline input
5481 int32 selectionStart = 0;
5482 int32 selectionEnd = 0;
5483 message->FindInt32("be:selection", 0, &selectionStart);
5484 message->FindInt32("be:selection", 1, &selectionEnd);
5486 fInline->SetSelectionOffset(selectionStart);
5487 fInline->SetSelectionLength(selectionEnd - selectionStart);
5489 const int32 inlineOffset = fInline->Offset();
5490 InsertText(string, stringLen, fSelStart, NULL);
5492 _Refresh(inlineOffset, fSelEnd, true);
5493 _ShowCaret();
5499 /*! Called when the object receives a \c B_INPUT_METHOD_LOCATION_REQUEST
5500 message.
5502 void
5503 BTextView::_HandleInputMethodLocationRequest()
5505 ASSERT(fInline != NULL);
5507 int32 offset = fInline->Offset();
5508 const int32 limit = offset + fInline->Length();
5510 BMessage message(B_INPUT_METHOD_EVENT);
5511 message.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST);
5513 // Add the location of the UTF8 characters
5514 while (offset < limit) {
5515 float height;
5516 BPoint where = PointAt(offset, &height);
5517 ConvertToScreen(&where);
5519 message.AddPoint("be:location_reply", where);
5520 message.AddFloat("be:height_reply", height);
5522 offset = _NextInitialByte(offset);
5525 fInline->Method()->SendMessage(&message);
5529 //! Tells the Input Server method add-on to stop the current transaction.
5530 void
5531 BTextView::_CancelInputMethod()
5533 if (!fInline)
5534 return;
5536 InlineInput* inlineInput = fInline;
5537 fInline = NULL;
5539 if (inlineInput->IsActive() && Window()) {
5540 _Refresh(inlineInput->Offset(), fText->Length() - inlineInput->Offset(),
5541 false);
5543 BMessage message(B_INPUT_METHOD_EVENT);
5544 message.AddInt32("be:opcode", B_INPUT_METHOD_STOPPED);
5545 inlineInput->Method()->SendMessage(&message);
5548 delete inlineInput;
5552 /*! Returns the line number of the character at the given \a offset.
5554 \note This will never return the last line (use LineAt() if you
5555 need to be correct about that.) N.B.
5557 \param offset The offset of the wanted character.
5559 \return The line number of the character at the given \a offset as an int32.
5561 int32
5562 BTextView::_LineAt(int32 offset) const
5564 return fLines->OffsetToLine(offset);
5568 /*! Returns the line number that the given \a point is on.
5570 \note This will never return the last line (use LineAt() if you
5571 need to be correct about that.) N.B.
5573 \param point The \a point the get the line number of.
5575 \return The line number of the given \a point as an int32.
5577 int32
5578 BTextView::_LineAt(const BPoint& point) const
5580 return fLines->PixelToLine(point.y - fTextRect.top);
5584 /*! Returns whether or not the given \a offset is on the empty line at the end
5585 of the buffer.
5587 bool
5588 BTextView::_IsOnEmptyLastLine(int32 offset) const
5590 return (offset == TextLength() && offset > 0
5591 && fText->RealCharAt(offset - 1) == B_ENTER);
5595 void
5596 BTextView::_ApplyStyleRange(int32 fromOffset, int32 toOffset, uint32 mode,
5597 const BFont* font, const rgb_color* color, bool syncNullStyle)
5599 if (font != NULL) {
5600 // if a font has been given, normalize it
5601 BFont normalized = *font;
5602 _NormalizeFont(&normalized);
5603 font = &normalized;
5606 if (!fStylable) {
5607 // always apply font and color to full range for non-stylable textviews
5608 fromOffset = 0;
5609 toOffset = fText->Length();
5612 if (syncNullStyle)
5613 fStyles->SyncNullStyle(fromOffset);
5615 fStyles->SetStyleRange(fromOffset, toOffset, fText->Length(), mode,
5616 font, color);
5620 float
5621 BTextView::_NullStyleHeight() const
5623 const BFont* font = NULL;
5624 fStyles->GetNullStyle(&font, NULL);
5626 font_height fontHeight;
5627 font->GetHeight(&fontHeight);
5628 return ceilf(fontHeight.ascent + fontHeight.descent + 1);
5632 void
5633 BTextView::_ShowContextMenu(BPoint where)
5635 bool isRedo;
5636 undo_state state = UndoState(&isRedo);
5637 bool isUndo = state != B_UNDO_UNAVAILABLE && !isRedo;
5639 int32 start;
5640 int32 finish;
5641 GetSelection(&start, &finish);
5643 bool canEdit = IsEditable();
5644 int32 length = TextLength();
5646 BPopUpMenu* menu = new BPopUpMenu(B_EMPTY_STRING, false, false);
5648 BLayoutBuilder::Menu<>(menu)
5649 .AddItem(TRANSLATE("Undo"), B_UNDO/*, 'Z'*/)
5650 .SetEnabled(canEdit && isUndo)
5651 .AddItem(TRANSLATE("Redo"), B_UNDO/*, 'Z', B_SHIFT_KEY*/)
5652 .SetEnabled(canEdit && isRedo)
5653 .AddSeparator()
5654 .AddItem(TRANSLATE("Cut"), B_CUT, 'X')
5655 .SetEnabled(canEdit && start != finish)
5656 .AddItem(TRANSLATE("Copy"), B_COPY, 'C')
5657 .SetEnabled(start != finish)
5658 .AddItem(TRANSLATE("Paste"), B_PASTE, 'V')
5659 .SetEnabled(canEdit && be_clipboard->SystemCount() > 0)
5660 .AddSeparator()
5661 .AddItem(TRANSLATE("Select all"), B_SELECT_ALL, 'A')
5662 .SetEnabled(!(start == 0 && finish == length))
5665 menu->SetTargetForItems(this);
5666 ConvertToScreen(&where);
5667 menu->Go(where, true, true, true);
5671 void
5672 BTextView::_FilterDisallowedChars(char* text, ssize_t& length,
5673 text_run_array* runArray)
5675 if (!fDisallowedChars)
5676 return;
5678 if (fDisallowedChars->IsEmpty() || !text)
5679 return;
5681 ssize_t stringIndex = 0;
5682 if (runArray) {
5683 ssize_t remNext = 0;
5685 for (int i = 0; i < runArray->count; i++) {
5686 runArray->runs[i].offset -= remNext;
5687 while (stringIndex < runArray->runs[i].offset
5688 && stringIndex < length) {
5689 if (fDisallowedChars->HasItem(
5690 reinterpret_cast<void*>(text[stringIndex]))) {
5691 memmove(text + stringIndex, text + stringIndex + 1,
5692 length - stringIndex - 1);
5693 length--;
5694 runArray->runs[i].offset--;
5695 remNext++;
5696 } else
5697 stringIndex++;
5702 while (stringIndex < length) {
5703 if (fDisallowedChars->HasItem(
5704 reinterpret_cast<void*>(text[stringIndex]))) {
5705 memmove(text + stringIndex, text + stringIndex + 1,
5706 length - stringIndex - 1);
5707 length--;
5708 } else
5709 stringIndex++;
5714 // #pragma mark - BTextView::TextTrackState
5717 BTextView::TextTrackState::TextTrackState(BMessenger messenger)
5719 clickOffset(0),
5720 shiftDown(false),
5721 anchor(0),
5722 selStart(0),
5723 selEnd(0),
5724 fRunner(NULL)
5726 BMessage message(_PING_);
5727 const bigtime_t scrollSpeed = 25 * 1000; // 40 scroll steps per second
5728 fRunner = new (nothrow) BMessageRunner(messenger, &message, scrollSpeed);
5732 BTextView::TextTrackState::~TextTrackState()
5734 delete fRunner;
5738 void
5739 BTextView::TextTrackState::SimulateMouseMovement(BTextView* textView)
5741 BPoint where;
5742 uint32 buttons;
5743 // When the mouse cursor is still and outside the textview,
5744 // no B_MOUSE_MOVED message are sent, obviously. But scrolling
5745 // has to work neverthless, so we "fake" a MouseMoved() call here.
5746 textView->GetMouse(&where, &buttons);
5747 textView->_PerformMouseMoved(where, B_INSIDE_VIEW);
5751 // #pragma mark - Binary ABI compat
5754 extern "C" void
5755 B_IF_GCC_2(InvalidateLayout__9BTextViewb, _ZN9BTextView16InvalidateLayoutEb)(
5756 BTextView* view, bool descendants)
5758 perform_data_layout_invalidated data;
5759 data.descendants = descendants;
5761 view->Perform(PERFORM_CODE_LAYOUT_INVALIDATED, &data);