tcp: Fix 64 bit build with debugging features enabled.
[haiku.git] / src / kits / interface / TextView.cpp
blobf5625d90a5a6cab74b3eae867c5ecad16e624089
1 /*
2 * Copyright 2001-2014 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 },
254 { 0 }
258 BTextView::BTextView(BRect frame, const char* name, BRect textRect,
259 uint32 resizeMask, uint32 flags)
261 BView(frame, name, resizeMask,
262 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
264 _InitObject(textRect, NULL, NULL);
268 BTextView::BTextView(BRect frame, const char* name, BRect textRect,
269 const BFont* initialFont, const rgb_color* initialColor,
270 uint32 resizeMask, uint32 flags)
272 BView(frame, name, resizeMask,
273 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
275 _InitObject(textRect, initialFont, initialColor);
279 BTextView::BTextView(const char* name, uint32 flags)
281 BView(name,
282 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
284 _InitObject(Bounds(), NULL, NULL);
288 BTextView::BTextView(const char* name, const BFont* initialFont,
289 const rgb_color* initialColor, uint32 flags)
291 BView(name,
292 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE)
294 _InitObject(Bounds(), initialFont, initialColor);
298 BTextView::BTextView(BMessage* archive)
300 BView(archive)
302 CALLED();
303 BRect rect;
305 if (archive->FindRect("_trect", &rect) != B_OK)
306 rect.Set(0, 0, 0, 0);
308 _InitObject(rect, NULL, NULL);
310 const char* text = NULL;
311 if (archive->FindString("_text", &text) == B_OK)
312 SetText(text);
314 int32 flag, flag2;
315 if (archive->FindInt32("_align", &flag) == B_OK)
316 SetAlignment((alignment)flag);
318 float value;
320 if (archive->FindFloat("_tab", &value) == B_OK)
321 SetTabWidth(value);
323 if (archive->FindInt32("_col_sp", &flag) == B_OK)
324 SetColorSpace((color_space)flag);
326 if (archive->FindInt32("_max", &flag) == B_OK)
327 SetMaxBytes(flag);
329 if (archive->FindInt32("_sel", &flag) == B_OK &&
330 archive->FindInt32("_sel", &flag2) == B_OK)
331 Select(flag, flag2);
333 bool toggle;
335 if (archive->FindBool("_stylable", &toggle) == B_OK)
336 SetStylable(toggle);
338 if (archive->FindBool("_auto_in", &toggle) == B_OK)
339 SetAutoindent(toggle);
341 if (archive->FindBool("_wrap", &toggle) == B_OK)
342 SetWordWrap(toggle);
344 if (archive->FindBool("_nsel", &toggle) == B_OK)
345 MakeSelectable(!toggle);
347 if (archive->FindBool("_nedit", &toggle) == B_OK)
348 MakeEditable(!toggle);
350 ssize_t disallowedCount = 0;
351 const int32* disallowedChars = NULL;
352 if (archive->FindData("_dis_ch", B_RAW_TYPE,
353 (const void**)&disallowedChars, &disallowedCount) == B_OK) {
355 fDisallowedChars = new BList;
356 disallowedCount /= sizeof(int32);
357 for (int32 x = 0; x < disallowedCount; x++) {
358 fDisallowedChars->AddItem(
359 reinterpret_cast<void*>(disallowedChars[x]));
363 ssize_t runSize = 0;
364 const void* flattenedRun = NULL;
366 if (archive->FindData("_runs", B_RAW_TYPE, &flattenedRun, &runSize)
367 == B_OK) {
368 text_run_array* runArray = UnflattenRunArray(flattenedRun,
369 (int32*)&runSize);
370 if (runArray) {
371 SetRunArray(0, TextLength(), runArray);
372 FreeRunArray(runArray);
378 BTextView::~BTextView()
380 _CancelInputMethod();
381 _StopMouseTracking();
382 _DeleteOffscreen();
384 delete fText;
385 delete fLines;
386 delete fStyles;
387 delete fDisallowedChars;
388 delete fUndo;
389 delete fClickRunner;
390 delete fDragRunner;
391 delete fLayoutData;
395 BArchivable*
396 BTextView::Instantiate(BMessage* archive)
398 CALLED();
399 if (validate_instantiation(archive, "BTextView"))
400 return new BTextView(archive);
401 return NULL;
405 status_t
406 BTextView::Archive(BMessage* data, bool deep) const
408 CALLED();
409 status_t err = BView::Archive(data, deep);
410 if (err == B_OK)
411 err = data->AddString("_text", Text());
412 if (err == B_OK)
413 err = data->AddInt32("_align", fAlignment);
414 if (err == B_OK)
415 err = data->AddFloat("_tab", fTabWidth);
416 if (err == B_OK)
417 err = data->AddInt32("_col_sp", fColorSpace);
418 if (err == B_OK)
419 err = data->AddRect("_trect", fTextRect);
420 if (err == B_OK)
421 err = data->AddInt32("_max", fMaxBytes);
422 if (err == B_OK)
423 err = data->AddInt32("_sel", fSelStart);
424 if (err == B_OK)
425 err = data->AddInt32("_sel", fSelEnd);
426 if (err == B_OK)
427 err = data->AddBool("_stylable", fStylable);
428 if (err == B_OK)
429 err = data->AddBool("_auto_in", fAutoindent);
430 if (err == B_OK)
431 err = data->AddBool("_wrap", fWrap);
432 if (err == B_OK)
433 err = data->AddBool("_nsel", !fSelectable);
434 if (err == B_OK)
435 err = data->AddBool("_nedit", !fEditable);
437 if (err == B_OK && fDisallowedChars != NULL) {
438 err = data->AddData("_dis_ch", B_RAW_TYPE, fDisallowedChars->Items(),
439 fDisallowedChars->CountItems() * sizeof(int32));
442 if (err == B_OK) {
443 int32 runSize = 0;
444 text_run_array* runArray = RunArray(0, TextLength());
446 void* flattened = FlattenRunArray(runArray, &runSize);
447 if (flattened != NULL) {
448 data->AddData("_runs", B_RAW_TYPE, flattened, runSize);
449 free(flattened);
450 } else
451 err = B_NO_MEMORY;
453 FreeRunArray(runArray);
456 return err;
460 void
461 BTextView::AttachedToWindow()
463 BView::AttachedToWindow();
465 SetDrawingMode(B_OP_COPY);
467 Window()->SetPulseRate(500000);
469 fCaretVisible = false;
470 fCaretTime = 0;
471 fClickCount = 0;
472 fClickTime = 0;
473 fDragOffset = -1;
474 fActive = false;
476 _AutoResize(true);
478 _UpdateScrollbars();
480 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
484 void
485 BTextView::DetachedFromWindow()
487 BView::DetachedFromWindow();
491 void
492 BTextView::Draw(BRect updateRect)
494 // what lines need to be drawn?
495 int32 startLine = _LineAt(BPoint(0.0, updateRect.top));
496 int32 endLine = _LineAt(BPoint(0.0, updateRect.bottom));
498 _DrawLines(startLine, endLine, -1, true);
502 void
503 BTextView::MouseDown(BPoint where)
505 // should we even bother?
506 if (!fEditable && !fSelectable)
507 return;
509 _CancelInputMethod();
511 if (!IsFocus())
512 MakeFocus();
514 _HideCaret();
516 _StopMouseTracking();
518 int32 modifiers = 0;
519 uint32 buttons = 0;
520 BMessage* currentMessage = Window()->CurrentMessage();
521 if (currentMessage != NULL) {
522 currentMessage->FindInt32("modifiers", &modifiers);
523 currentMessage->FindInt32("buttons", (int32*)&buttons);
526 if (buttons == B_SECONDARY_MOUSE_BUTTON) {
527 _ShowContextMenu(where);
528 return;
531 BMessenger messenger(this);
532 fTrackingMouse = new (nothrow) TextTrackState(messenger);
533 if (fTrackingMouse == NULL)
534 return;
536 fTrackingMouse->clickOffset = OffsetAt(where);
537 fTrackingMouse->shiftDown = modifiers & B_SHIFT_KEY;
538 fTrackingMouse->where = where;
540 bigtime_t clickTime = system_time();
541 bigtime_t clickSpeed = 0;
542 get_click_speed(&clickSpeed);
543 bool multipleClick
544 = clickTime - fClickTime < clickSpeed
545 && fLastClickOffset == fTrackingMouse->clickOffset;
547 fWhere = where;
549 SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS,
550 B_LOCK_WINDOW_FOCUS | B_NO_POINTER_HISTORY);
552 if (fSelStart != fSelEnd && !fTrackingMouse->shiftDown && !multipleClick) {
553 BRegion region;
554 GetTextRegion(fSelStart, fSelEnd, &region);
555 if (region.Contains(where)) {
556 // Setup things for dragging
557 fTrackingMouse->selectionRect = region.Frame();
558 fClickCount = 1;
559 fClickTime = clickTime;
560 fLastClickOffset = OffsetAt(where);
561 return;
565 if (multipleClick) {
566 if (fClickCount > 3) {
567 fClickCount = 0;
568 fClickTime = 0;
569 } else {
570 fClickCount++;
571 fClickTime = clickTime;
573 } else if (!fTrackingMouse->shiftDown) {
574 // If no multiple click yet and shift is not pressed, this is an
575 // independent first click somewhere into the textview - we initialize
576 // the corresponding members for handling potential multiple clicks:
577 fLastClickOffset = fCaretOffset = fTrackingMouse->clickOffset;
578 fClickCount = 1;
579 fClickTime = clickTime;
581 // Deselect any previously selected text
582 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset);
585 if (fClickTime == clickTime) {
586 BMessage message(_PING_);
587 message.AddInt64("clickTime", clickTime);
588 delete fClickRunner;
590 BMessenger messenger(this);
591 fClickRunner = new (nothrow) BMessageRunner(messenger, &message,
592 clickSpeed, 1);
595 if (!fSelectable) {
596 _StopMouseTracking();
597 return;
600 int32 offset = fSelStart;
601 if (fTrackingMouse->clickOffset > fSelStart)
602 offset = fSelEnd;
604 fTrackingMouse->anchor = offset;
606 MouseMoved(where, B_INSIDE_VIEW, NULL);
610 void
611 BTextView::MouseUp(BPoint where)
613 BView::MouseUp(where);
614 _PerformMouseUp(where);
616 delete fDragRunner;
617 fDragRunner = NULL;
621 void
622 BTextView::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage)
624 // check if it's a "click'n'move"
625 if (_PerformMouseMoved(where, code))
626 return;
628 switch (code) {
629 case B_ENTERED_VIEW:
630 case B_INSIDE_VIEW:
631 _TrackMouse(where, dragMessage, true);
632 break;
634 case B_EXITED_VIEW:
635 _DragCaret(-1);
636 if (Window()->IsActive() && dragMessage == NULL)
637 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
638 break;
640 default:
641 BView::MouseMoved(where, code, dragMessage);
646 void
647 BTextView::WindowActivated(bool active)
649 BView::WindowActivated(active);
651 if (active && IsFocus()) {
652 if (!fActive)
653 _Activate();
654 } else {
655 if (fActive)
656 _Deactivate();
659 BPoint where;
660 uint32 buttons;
661 GetMouse(&where, &buttons, false);
663 if (Bounds().Contains(where))
664 _TrackMouse(where, NULL);
668 void
669 BTextView::KeyDown(const char* bytes, int32 numBytes)
671 const char keyPressed = bytes[0];
673 if (!fEditable) {
674 // only arrow and page keys are allowed
675 // (no need to hide the cursor)
676 switch (keyPressed) {
677 case B_LEFT_ARROW:
678 case B_RIGHT_ARROW:
679 case B_UP_ARROW:
680 case B_DOWN_ARROW:
681 _HandleArrowKey(keyPressed);
682 break;
684 case B_HOME:
685 case B_END:
686 case B_PAGE_UP:
687 case B_PAGE_DOWN:
688 _HandlePageKey(keyPressed);
689 break;
691 default:
692 BView::KeyDown(bytes, numBytes);
693 break;
696 return;
699 // hide the cursor and caret
700 if (IsFocus())
701 be_app->ObscureCursor();
702 _HideCaret();
704 switch (keyPressed) {
705 case B_BACKSPACE:
706 _HandleBackspace();
707 break;
709 case B_LEFT_ARROW:
710 case B_RIGHT_ARROW:
711 case B_UP_ARROW:
712 case B_DOWN_ARROW:
713 _HandleArrowKey(keyPressed);
714 break;
716 case B_DELETE:
717 _HandleDelete();
718 break;
720 case B_HOME:
721 case B_END:
722 case B_PAGE_UP:
723 case B_PAGE_DOWN:
724 _HandlePageKey(keyPressed);
725 break;
727 case B_ESCAPE:
728 case B_INSERT:
729 case B_FUNCTION_KEY:
730 // ignore, pass it up to superclass
731 BView::KeyDown(bytes, numBytes);
732 break;
734 default:
735 // bail out if the character is not allowed
736 if (fDisallowedChars
737 && fDisallowedChars->HasItem(
738 reinterpret_cast<void*>((uint32)keyPressed))) {
739 beep();
740 return;
743 _HandleAlphaKey(bytes, numBytes);
744 break;
747 // draw the caret
748 if (fSelStart == fSelEnd)
749 _ShowCaret();
753 void
754 BTextView::Pulse()
756 if (fActive && fEditable && fSelStart == fSelEnd) {
757 if (system_time() > (fCaretTime + 500000.0))
758 _InvertCaret();
763 void
764 BTextView::FrameResized(float newWidth, float newHeight)
766 BView::FrameResized(newWidth, newHeight);
767 _UpdateScrollbars();
771 void
772 BTextView::MakeFocus(bool focus)
774 BView::MakeFocus(focus);
776 if (focus && Window() != NULL && Window()->IsActive()) {
777 if (!fActive)
778 _Activate();
779 } else {
780 if (fActive)
781 _Deactivate();
786 void
787 BTextView::MessageReceived(BMessage* message)
789 // ToDo: block input if not editable (Andrew)
791 // was this message dropped?
792 if (message->WasDropped()) {
793 BPoint dropOffset;
794 BPoint dropPoint = message->DropPoint(&dropOffset);
795 ConvertFromScreen(&dropPoint);
796 ConvertFromScreen(&dropOffset);
797 if (!_MessageDropped(message, dropPoint, dropOffset))
798 BView::MessageReceived(message);
800 return;
803 switch (message->what) {
804 case B_CUT:
805 if (!IsTypingHidden())
806 Cut(be_clipboard);
807 else
808 beep();
809 break;
811 case B_COPY:
812 if (!IsTypingHidden())
813 Copy(be_clipboard);
814 else
815 beep();
816 break;
818 case B_PASTE:
819 Paste(be_clipboard);
820 break;
822 case B_UNDO:
823 Undo(be_clipboard);
824 break;
826 case B_SELECT_ALL:
827 SelectAll();
828 break;
830 case B_INPUT_METHOD_EVENT:
832 int32 opcode;
833 if (message->FindInt32("be:opcode", &opcode) == B_OK) {
834 switch (opcode) {
835 case B_INPUT_METHOD_STARTED:
837 BMessenger messenger;
838 if (message->FindMessenger("be:reply_to", &messenger)
839 == B_OK) {
840 ASSERT(fInline == NULL);
841 fInline = new InlineInput(messenger);
843 break;
846 case B_INPUT_METHOD_STOPPED:
847 delete fInline;
848 fInline = NULL;
849 break;
851 case B_INPUT_METHOD_CHANGED:
852 if (fInline != NULL)
853 _HandleInputMethodChanged(message);
854 break;
856 case B_INPUT_METHOD_LOCATION_REQUEST:
857 if (fInline != NULL)
858 _HandleInputMethodLocationRequest();
859 break;
861 default:
862 break;
865 break;
868 case B_SET_PROPERTY:
869 case B_GET_PROPERTY:
870 case B_COUNT_PROPERTIES:
872 BPropertyInfo propInfo(sPropertyList);
873 BMessage specifier;
874 const char* property;
876 if (message->GetCurrentSpecifier(NULL, &specifier) < B_OK
877 || specifier.FindString("property", &property) < B_OK)
878 return;
880 if (propInfo.FindMatch(message, 0, &specifier, specifier.what,
881 property) < B_OK) {
882 BView::MessageReceived(message);
883 break;
886 BMessage reply;
887 bool handled = false;
888 switch(message->what) {
889 case B_GET_PROPERTY:
890 handled = _GetProperty(&specifier, specifier.what, property,
891 &reply);
892 break;
894 case B_SET_PROPERTY:
895 handled = _SetProperty(&specifier, specifier.what, property,
896 &reply);
897 break;
899 case B_COUNT_PROPERTIES:
900 handled = _CountProperties(&specifier, specifier.what,
901 property, &reply);
902 break;
904 default:
905 break;
907 if (handled)
908 message->SendReply(&reply);
909 else
910 BView::MessageReceived(message);
911 break;
914 case _PING_:
916 if (message->HasInt64("clickTime")) {
917 bigtime_t clickTime;
918 message->FindInt64("clickTime", &clickTime);
919 if (clickTime == fClickTime) {
920 if (fSelStart != fSelEnd && fSelectable) {
921 BRegion region;
922 GetTextRegion(fSelStart, fSelEnd, &region);
923 if (region.Contains(fWhere))
924 _TrackMouse(fWhere, NULL);
926 delete fClickRunner;
927 fClickRunner = NULL;
929 } else if (fTrackingMouse) {
930 fTrackingMouse->SimulateMouseMovement(this);
931 _PerformAutoScrolling();
933 break;
936 case _DISPOSE_DRAG_:
937 if (fEditable)
938 _TrackDrag(fWhere);
939 break;
941 case kMsgNavigateArrow:
943 int32 key = message->GetInt32("key", 0);
944 int32 modifiers = message->GetInt32("modifiers", 0);
945 _HandleArrowKey(key, modifiers);
946 break;
949 case kMsgNavigatePage:
951 int32 key = message->GetInt32("key", 0);
952 int32 modifiers = message->GetInt32("modifiers", 0);
953 _HandlePageKey(key, modifiers);
954 break;
957 default:
958 BView::MessageReceived(message);
959 break;
964 BHandler*
965 BTextView::ResolveSpecifier(BMessage* message, int32 index, BMessage* specifier,
966 int32 what, const char* property)
968 BPropertyInfo propInfo(sPropertyList);
969 BHandler* target = this;
971 if (propInfo.FindMatch(message, index, specifier, what, property) < B_OK) {
972 target = BView::ResolveSpecifier(message, index, specifier, what,
973 property);
976 return target;
980 status_t
981 BTextView::GetSupportedSuites(BMessage* data)
983 if (data == NULL)
984 return B_BAD_VALUE;
986 status_t err = data->AddString("suites", "suite/vnd.Be-text-view");
987 if (err != B_OK)
988 return err;
990 BPropertyInfo prop_info(sPropertyList);
991 err = data->AddFlat("messages", &prop_info);
993 if (err != B_OK)
994 return err;
995 return BView::GetSupportedSuites(data);
999 status_t
1000 BTextView::Perform(perform_code code, void* _data)
1002 switch (code) {
1003 case PERFORM_CODE_MIN_SIZE:
1004 ((perform_data_min_size*)_data)->return_value
1005 = BTextView::MinSize();
1006 return B_OK;
1007 case PERFORM_CODE_MAX_SIZE:
1008 ((perform_data_max_size*)_data)->return_value
1009 = BTextView::MaxSize();
1010 return B_OK;
1011 case PERFORM_CODE_PREFERRED_SIZE:
1012 ((perform_data_preferred_size*)_data)->return_value
1013 = BTextView::PreferredSize();
1014 return B_OK;
1015 case PERFORM_CODE_LAYOUT_ALIGNMENT:
1016 ((perform_data_layout_alignment*)_data)->return_value
1017 = BTextView::LayoutAlignment();
1018 return B_OK;
1019 case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
1020 ((perform_data_has_height_for_width*)_data)->return_value
1021 = BTextView::HasHeightForWidth();
1022 return B_OK;
1023 case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
1025 perform_data_get_height_for_width* data
1026 = (perform_data_get_height_for_width*)_data;
1027 BTextView::GetHeightForWidth(data->width, &data->min, &data->max,
1028 &data->preferred);
1029 return B_OK;
1031 case PERFORM_CODE_SET_LAYOUT:
1033 perform_data_set_layout* data = (perform_data_set_layout*)_data;
1034 BTextView::SetLayout(data->layout);
1035 return B_OK;
1037 case PERFORM_CODE_LAYOUT_INVALIDATED:
1039 perform_data_layout_invalidated* data
1040 = (perform_data_layout_invalidated*)_data;
1041 BTextView::LayoutInvalidated(data->descendants);
1042 return B_OK;
1044 case PERFORM_CODE_DO_LAYOUT:
1046 BTextView::DoLayout();
1047 return B_OK;
1051 return BView::Perform(code, _data);
1055 void
1056 BTextView::SetText(const char* text, const text_run_array* runs)
1058 SetText(text, text ? strlen(text) : 0, runs);
1062 void
1063 BTextView::SetText(const char* text, int32 length, const text_run_array* runs)
1065 _CancelInputMethod();
1067 // hide the caret/unhighlight the selection
1068 if (fActive) {
1069 if (fSelStart != fSelEnd) {
1070 if (fSelectable)
1071 Highlight(fSelStart, fSelEnd);
1072 } else
1073 _HideCaret();
1076 // remove data from buffer
1077 if (fText->Length() > 0)
1078 DeleteText(0, fText->Length());
1080 if (text != NULL && length > 0)
1081 InsertText(text, length, 0, runs);
1083 // recalculate line breaks and draw the text
1084 _Refresh(0, length, false);
1085 fCaretOffset = fSelStart = fSelEnd = 0;
1086 ScrollTo(B_ORIGIN);
1088 // draw the caret
1089 _ShowCaret();
1093 void
1094 BTextView::SetText(BFile* file, int32 offset, int32 length,
1095 const text_run_array* runs)
1097 CALLED();
1099 _CancelInputMethod();
1101 if (!file)
1102 return;
1104 if (fText->Length() > 0)
1105 DeleteText(0, fText->Length());
1107 fText->InsertText(file, offset, length, 0);
1109 // update the start offsets of each line below offset
1110 fLines->BumpOffset(length, _LineAt(offset) + 1);
1112 // update the style runs
1113 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1);
1115 if (fStylable && runs != NULL)
1116 SetRunArray(offset, offset + length, runs);
1117 else {
1118 // apply null-style to inserted text
1119 _ApplyStyleRange(offset, offset + length);
1122 // recalculate line breaks and draw the text
1123 _Refresh(0, length, false);
1124 fCaretOffset = fSelStart = fSelEnd = 0;
1125 ScrollToOffset(fSelStart);
1127 // draw the caret
1128 _ShowCaret();
1132 void
1133 BTextView::Insert(const char* text, const text_run_array* runs)
1135 if (text != NULL)
1136 _DoInsertText(text, strlen(text), fSelStart, runs);
1140 void
1141 BTextView::Insert(const char* text, int32 length, const text_run_array* runs)
1143 if (text != NULL && length > 0)
1144 _DoInsertText(text, strnlen(text, length), fSelStart, runs);
1148 void
1149 BTextView::Insert(int32 offset, const char* text, int32 length,
1150 const text_run_array* runs)
1152 // pin offset at reasonable values
1153 if (offset < 0)
1154 offset = 0;
1155 else if (offset > fText->Length())
1156 offset = fText->Length();
1158 if (text != NULL && length > 0)
1159 _DoInsertText(text, strnlen(text, length), offset, runs);
1163 void
1164 BTextView::Delete()
1166 Delete(fSelStart, fSelEnd);
1170 void
1171 BTextView::Delete(int32 startOffset, int32 endOffset)
1173 CALLED();
1175 // pin offsets at reasonable values
1176 if (startOffset < 0)
1177 startOffset = 0;
1178 else if (startOffset > fText->Length())
1179 startOffset = fText->Length();
1180 if (endOffset < 0)
1181 endOffset = 0;
1182 else if (endOffset > fText->Length())
1183 endOffset = fText->Length();
1185 // anything to delete?
1186 if (startOffset == endOffset)
1187 return;
1189 // hide the caret/unhighlight the selection
1190 if (fActive) {
1191 if (fSelStart != fSelEnd) {
1192 if (fSelectable)
1193 Highlight(fSelStart, fSelEnd);
1194 } else
1195 _HideCaret();
1197 // remove data from buffer
1198 DeleteText(startOffset, endOffset);
1200 // check if the caret needs to be moved
1201 if (fCaretOffset >= endOffset)
1202 fCaretOffset -= (endOffset - startOffset);
1203 else if (fCaretOffset >= startOffset && fCaretOffset < endOffset)
1204 fCaretOffset = startOffset;
1206 fSelEnd = fSelStart = fCaretOffset;
1208 // recalculate line breaks and draw what's left
1209 _Refresh(startOffset, endOffset, false);
1211 // draw the caret
1212 _ShowCaret();
1216 const char*
1217 BTextView::Text() const
1219 return fText->RealText();
1223 int32
1224 BTextView::TextLength() const
1226 return fText->Length();
1230 void
1231 BTextView::GetText(int32 offset, int32 length, char* buffer) const
1233 if (buffer != NULL)
1234 fText->GetString(offset, length, buffer);
1238 uchar
1239 BTextView::ByteAt(int32 offset) const
1241 if (offset < 0 || offset >= fText->Length())
1242 return '\0';
1244 return fText->RealCharAt(offset);
1248 int32
1249 BTextView::CountLines() const
1251 return fLines->NumLines();
1255 int32
1256 BTextView::CurrentLine() const
1258 return LineAt(fSelStart);
1262 void
1263 BTextView::GoToLine(int32 index)
1265 _CancelInputMethod();
1266 _HideCaret();
1267 fSelStart = fSelEnd = fCaretOffset = OffsetAt(index);
1268 _ShowCaret();
1272 void
1273 BTextView::Cut(BClipboard* clipboard)
1275 _CancelInputMethod();
1276 if (!fEditable)
1277 return;
1278 if (fUndo) {
1279 delete fUndo;
1280 fUndo = new CutUndoBuffer(this);
1282 Copy(clipboard);
1283 Delete();
1287 void
1288 BTextView::Copy(BClipboard* clipboard)
1290 _CancelInputMethod();
1292 if (clipboard->Lock()) {
1293 clipboard->Clear();
1295 BMessage* clip = clipboard->Data();
1296 if (clip != NULL) {
1297 int32 numBytes = fSelEnd - fSelStart;
1298 const char* text = fText->GetString(fSelStart, &numBytes);
1299 clip->AddData("text/plain", B_MIME_TYPE, text, numBytes);
1301 int32 size;
1302 if (fStylable) {
1303 text_run_array* runArray = RunArray(fSelStart, fSelEnd, &size);
1304 clip->AddData("application/x-vnd.Be-text_run_array",
1305 B_MIME_TYPE, runArray, size);
1306 FreeRunArray(runArray);
1308 clipboard->Commit();
1310 clipboard->Unlock();
1315 void
1316 BTextView::Paste(BClipboard* clipboard)
1318 CALLED();
1319 _CancelInputMethod();
1321 if (!fEditable || !clipboard->Lock())
1322 return;
1324 BMessage* clip = clipboard->Data();
1325 if (clip != NULL) {
1326 const char* text = NULL;
1327 ssize_t length = 0;
1329 if (clip->FindData("text/plain", B_MIME_TYPE,
1330 (const void**)&text, &length) == B_OK) {
1331 text_run_array* runArray = NULL;
1332 ssize_t runLength = 0;
1334 if (fStylable) {
1335 clip->FindData("application/x-vnd.Be-text_run_array",
1336 B_MIME_TYPE, (const void**)&runArray, &runLength);
1339 _FilterDisallowedChars((char*)text, length, runArray);
1341 if (length < 1) {
1342 beep();
1343 clipboard->Unlock();
1344 return;
1347 if (fUndo) {
1348 delete fUndo;
1349 fUndo = new PasteUndoBuffer(this, text, length, runArray,
1350 runLength);
1353 if (fSelStart != fSelEnd)
1354 Delete();
1356 Insert(text, length, runArray);
1357 ScrollToOffset(fSelEnd);
1361 clipboard->Unlock();
1365 void
1366 BTextView::Clear()
1368 // We always check for fUndo != NULL (not only here),
1369 // because when fUndo is NULL, undo is deactivated.
1370 if (fUndo) {
1371 delete fUndo;
1372 fUndo = new ClearUndoBuffer(this);
1375 Delete();
1379 bool
1380 BTextView::AcceptsPaste(BClipboard* clipboard)
1382 bool result = false;
1384 if (fEditable && clipboard && clipboard->Lock()) {
1385 BMessage* data = clipboard->Data();
1386 result = data && data->HasData("text/plain", B_MIME_TYPE);
1387 clipboard->Unlock();
1390 return result;
1394 bool
1395 BTextView::AcceptsDrop(const BMessage* message)
1397 return fEditable && message
1398 && message->HasData("text/plain", B_MIME_TYPE);
1402 void
1403 BTextView::Select(int32 startOffset, int32 endOffset)
1405 CALLED();
1406 if (!fSelectable)
1407 return;
1409 _CancelInputMethod();
1411 // pin offsets at reasonable values
1412 if (startOffset < 0)
1413 startOffset = 0;
1414 else if (startOffset > fText->Length())
1415 startOffset = fText->Length();
1416 if (endOffset < 0)
1417 endOffset = 0;
1418 else if (endOffset > fText->Length())
1419 endOffset = fText->Length();
1421 // a negative selection?
1422 if (startOffset > endOffset)
1423 return;
1425 // is the new selection any different from the current selection?
1426 if (startOffset == fSelStart && endOffset == fSelEnd)
1427 return;
1429 fStyles->InvalidateNullStyle();
1431 _HideCaret();
1433 if (startOffset == endOffset) {
1434 if (fSelStart != fSelEnd) {
1435 // unhilite the selection
1436 if (fActive)
1437 Highlight(fSelStart, fSelEnd);
1439 fSelStart = fSelEnd = fCaretOffset = startOffset;
1440 _ShowCaret();
1441 } else {
1442 if (fActive) {
1443 // draw only those ranges that are different
1444 long start, end;
1445 if (startOffset != fSelStart) {
1446 // start of selection has changed
1447 if (startOffset > fSelStart) {
1448 start = fSelStart;
1449 end = startOffset;
1450 } else {
1451 start = startOffset;
1452 end = fSelStart;
1454 Highlight(start, end);
1457 if (endOffset != fSelEnd) {
1458 // end of selection has changed
1459 if (endOffset > fSelEnd) {
1460 start = fSelEnd;
1461 end = endOffset;
1462 } else {
1463 start = endOffset;
1464 end = fSelEnd;
1466 Highlight(start, end);
1469 fSelStart = startOffset;
1470 fSelEnd = endOffset;
1475 void
1476 BTextView::SelectAll()
1478 Select(0, fText->Length());
1482 void
1483 BTextView::GetSelection(int32* _start, int32* _end) const
1485 int32 start = 0;
1486 int32 end = 0;
1488 if (fSelectable) {
1489 start = fSelStart;
1490 end = fSelEnd;
1493 if (_start)
1494 *_start = start;
1496 if (_end)
1497 *_end = end;
1501 void
1502 BTextView::SetFontAndColor(const BFont* font, uint32 mode,
1503 const rgb_color* color)
1505 SetFontAndColor(fSelStart, fSelEnd, font, mode, color);
1509 void
1510 BTextView::SetFontAndColor(int32 startOffset, int32 endOffset,
1511 const BFont* font, uint32 mode, const rgb_color* color)
1513 CALLED();
1515 _HideCaret();
1517 const int32 textLength = fText->Length();
1519 if (!fStylable) {
1520 // When the text view is not stylable, we always set the whole text's
1521 // style and ignore the offsets
1522 startOffset = 0;
1523 endOffset = textLength;
1524 } else {
1525 // pin offsets at reasonable values
1526 if (startOffset < 0)
1527 startOffset = 0;
1528 else if (startOffset > textLength)
1529 startOffset = textLength;
1531 if (endOffset < 0)
1532 endOffset = 0;
1533 else if (endOffset > textLength)
1534 endOffset = textLength;
1537 // apply the style to the style buffer
1538 fStyles->InvalidateNullStyle();
1539 _ApplyStyleRange(startOffset, endOffset, mode, font, color);
1541 if ((mode & (B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE)) != 0) {
1542 // ToDo: maybe only invalidate the layout (depending on
1543 // B_SUPPORTS_LAYOUT) and have it _Refresh() automatically?
1544 InvalidateLayout();
1545 // recalc the line breaks and redraw with new style
1546 _Refresh(startOffset, endOffset, false);
1547 } else {
1548 // the line breaks wont change, simply redraw
1549 _RequestDrawLines(_LineAt(startOffset), _LineAt(endOffset));
1552 _ShowCaret();
1556 void
1557 BTextView::GetFontAndColor(int32 offset, BFont* _font,
1558 rgb_color* _color) const
1560 fStyles->GetStyle(offset, _font, _color);
1564 void
1565 BTextView::GetFontAndColor(BFont* _font, uint32* _mode,
1566 rgb_color* _color, bool* _sameColor) const
1568 fStyles->ContinuousGetStyle(_font, _mode, _color, _sameColor,
1569 fSelStart, fSelEnd);
1573 void
1574 BTextView::SetRunArray(int32 startOffset, int32 endOffset,
1575 const text_run_array* runs)
1577 CALLED();
1579 _CancelInputMethod();
1581 text_run_array oneRun;
1583 if (!fStylable) {
1584 // when the text view is not stylable, we always set the whole text's
1585 // style with the first run and ignore the offsets
1586 if (runs->count == 0)
1587 return;
1589 startOffset = 0;
1590 endOffset = fText->Length();
1591 oneRun.count = 1;
1592 oneRun.runs[0] = runs->runs[0];
1593 oneRun.runs[0].offset = 0;
1594 runs = &oneRun;
1595 } else {
1596 // pin offsets at reasonable values
1597 if (startOffset < 0)
1598 startOffset = 0;
1599 else if (startOffset > fText->Length())
1600 startOffset = fText->Length();
1602 if (endOffset < 0)
1603 endOffset = 0;
1604 else if (endOffset > fText->Length())
1605 endOffset = fText->Length();
1608 _SetRunArray(startOffset, endOffset, runs);
1610 _Refresh(startOffset, endOffset, false);
1614 text_run_array*
1615 BTextView::RunArray(int32 startOffset, int32 endOffset, int32* _size) const
1617 // pin offsets at reasonable values
1618 if (startOffset < 0)
1619 startOffset = 0;
1620 else if (startOffset > fText->Length())
1621 startOffset = fText->Length();
1623 if (endOffset < 0)
1624 endOffset = 0;
1625 else if (endOffset > fText->Length())
1626 endOffset = fText->Length();
1628 STEStyleRange* styleRange
1629 = fStyles->GetStyleRange(startOffset, endOffset - 1);
1630 if (styleRange == NULL)
1631 return NULL;
1633 text_run_array* runArray = AllocRunArray(styleRange->count, _size);
1634 if (runArray != NULL) {
1635 for (int32 i = 0; i < runArray->count; i++) {
1636 runArray->runs[i].offset = styleRange->runs[i].offset;
1637 runArray->runs[i].font = styleRange->runs[i].style.font;
1638 runArray->runs[i].color = styleRange->runs[i].style.color;
1642 free(styleRange);
1644 return runArray;
1648 int32
1649 BTextView::LineAt(int32 offset) const
1651 // pin offset at reasonable values
1652 if (offset < 0)
1653 offset = 0;
1654 else if (offset > fText->Length())
1655 offset = fText->Length();
1657 int32 lineNum = _LineAt(offset);
1658 if (_IsOnEmptyLastLine(offset))
1659 lineNum++;
1660 return lineNum;
1664 int32
1665 BTextView::LineAt(BPoint point) const
1667 int32 lineNum = _LineAt(point);
1668 if ((*fLines)[lineNum + 1]->origin <= point.y - fTextRect.top)
1669 lineNum++;
1671 return lineNum;
1675 BPoint
1676 BTextView::PointAt(int32 offset, float* _height) const
1678 // pin offset at reasonable values
1679 if (offset < 0)
1680 offset = 0;
1681 else if (offset > fText->Length())
1682 offset = fText->Length();
1684 // ToDo: Cleanup.
1685 int32 lineNum = _LineAt(offset);
1686 STELine* line = (*fLines)[lineNum];
1687 float height = 0;
1689 BPoint result;
1690 result.x = 0.0;
1691 result.y = line->origin + fTextRect.top;
1693 bool onEmptyLastLine = _IsOnEmptyLastLine(offset);
1695 if (fStyles->NumRuns() == 0) {
1696 // Handle the case where there is only one line (no text inserted)
1697 fStyles->SyncNullStyle(0);
1698 height = _NullStyleHeight();
1699 } else {
1700 height = (line + 1)->origin - line->origin;
1702 if (onEmptyLastLine) {
1703 // special case: go down one line if offset is at the newline
1704 // at the end of the buffer ...
1705 result.y += height;
1706 // ... and return the height of that (empty) line
1707 fStyles->SyncNullStyle(offset);
1708 height = _NullStyleHeight();
1709 } else {
1710 int32 length = offset - line->offset;
1711 result.x += _TabExpandedStyledWidth(line->offset, length);
1715 if (fAlignment != B_ALIGN_LEFT) {
1716 float lineWidth = onEmptyLastLine ? 0.0 : LineWidth(lineNum);
1717 float alignmentOffset = fTextRect.Width() - lineWidth;
1718 if (fAlignment == B_ALIGN_CENTER)
1719 alignmentOffset /= 2;
1720 result.x += alignmentOffset;
1723 // convert from text rect coordinates
1724 result.x += fTextRect.left;
1726 // round up
1727 result.x = lroundf(result.x);
1728 result.y = lroundf(result.y);
1729 if (_height != NULL)
1730 *_height = height;
1732 return result;
1736 int32
1737 BTextView::OffsetAt(BPoint point) const
1739 const int32 textLength = fText->Length();
1741 // should we even bother?
1742 if (point.y >= fTextRect.bottom)
1743 return textLength;
1744 else if (point.y < fTextRect.top)
1745 return 0;
1747 int32 lineNum = _LineAt(point);
1748 STELine* line = (*fLines)[lineNum];
1750 #define COMPILE_PROBABLY_BAD_CODE 1
1752 #if COMPILE_PROBABLY_BAD_CODE
1753 // special case: if point is within the text rect and PixelToLine()
1754 // tells us that it's on the last line, but if point is actually
1755 // lower than the bottom of the last line, return the last offset
1756 // (can happen for newlines)
1757 if (lineNum == (fLines->NumLines() - 1)) {
1758 if (point.y >= ((line + 1)->origin + fTextRect.top))
1759 return textLength;
1761 #endif
1763 // convert to text rect coordinates
1764 if (fAlignment != B_ALIGN_LEFT) {
1765 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum);
1766 if (fAlignment == B_ALIGN_CENTER)
1767 alignmentOffset /= 2;
1768 point.x -= alignmentOffset;
1771 point.x -= fTextRect.left;
1772 point.x = max_c(point.x, 0.0);
1774 // ToDo: The following code isn't very efficient, because it always starts
1775 // from the left end, so when the point is near the right end it's very
1776 // slow.
1777 int32 offset = line->offset;
1778 const int32 limit = (line + 1)->offset;
1779 float location = 0;
1780 do {
1781 const int32 nextInitial = _NextInitialByte(offset);
1782 const int32 saveOffset = offset;
1783 float width = 0;
1784 if (ByteAt(offset) == B_TAB)
1785 width = _ActualTabWidth(location);
1786 else
1787 width = _StyledWidth(saveOffset, nextInitial - saveOffset);
1788 if (location + width > point.x) {
1789 if (fabs(location + width - point.x) < fabs(location - point.x))
1790 offset = nextInitial;
1791 break;
1794 location += width;
1795 offset = nextInitial;
1796 } while (offset < limit);
1798 if (offset == (line + 1)->offset) {
1799 // special case: newlines aren't visible
1800 // return the offset of the character preceding the newline
1801 if (ByteAt(offset - 1) == B_ENTER)
1802 return --offset;
1804 // special case: return the offset preceding any spaces that
1805 // aren't at the end of the buffer
1806 if (offset != textLength && ByteAt(offset - 1) == B_SPACE)
1807 return --offset;
1810 return offset;
1814 int32
1815 BTextView::OffsetAt(int32 line) const
1817 if (line < 0)
1818 return 0;
1820 if (line > fLines->NumLines())
1821 return fText->Length();
1823 return (*fLines)[line]->offset;
1827 void
1828 BTextView::FindWord(int32 offset, int32* _fromOffset, int32* _toOffset)
1830 if (offset < 0) {
1831 if (_fromOffset)
1832 *_fromOffset = 0;
1834 if (_toOffset)
1835 *_toOffset = 0;
1837 return;
1840 if (offset > fText->Length()) {
1841 if (_fromOffset)
1842 *_fromOffset = fText->Length();
1844 if (_toOffset)
1845 *_toOffset = fText->Length();
1847 return;
1850 if (_fromOffset)
1851 *_fromOffset = _PreviousWordBoundary(offset);
1853 if (_toOffset)
1854 *_toOffset = _NextWordBoundary(offset);
1858 bool
1859 BTextView::CanEndLine(int32 offset)
1861 if (offset < 0 || offset > fText->Length())
1862 return false;
1864 // TODO: This should be improved using the LocaleKit.
1865 uint32 classification = _CharClassification(offset);
1867 // wrapping is always allowed at end of text and at newlines
1868 if (classification == CHAR_CLASS_END_OF_TEXT || ByteAt(offset) == B_ENTER)
1869 return true;
1871 uint32 nextClassification = _CharClassification(offset + 1);
1872 if (nextClassification == CHAR_CLASS_END_OF_TEXT)
1873 return true;
1875 // never separate a punctuation char from its preceeding word
1876 if (classification == CHAR_CLASS_DEFAULT
1877 && nextClassification == CHAR_CLASS_PUNCTUATION) {
1878 return false;
1881 if ((classification == CHAR_CLASS_WHITESPACE
1882 && nextClassification != CHAR_CLASS_WHITESPACE)
1883 || (classification != CHAR_CLASS_WHITESPACE
1884 && nextClassification == CHAR_CLASS_WHITESPACE)) {
1885 return true;
1888 // allow wrapping after whitespace, unless more whitespace (except for
1889 // newline) follows
1890 if (classification == CHAR_CLASS_WHITESPACE
1891 && (nextClassification != CHAR_CLASS_WHITESPACE
1892 || ByteAt(offset + 1) == B_ENTER)) {
1893 return true;
1896 // allow wrapping after punctuation chars, unless more punctuation, closing
1897 // parens or quotes follow
1898 if (classification == CHAR_CLASS_PUNCTUATION
1899 && nextClassification != CHAR_CLASS_PUNCTUATION
1900 && nextClassification != CHAR_CLASS_PARENS_CLOSE
1901 && nextClassification != CHAR_CLASS_QUOTE) {
1902 return true;
1905 // allow wrapping after quotes, graphical chars and closing parens only if
1906 // whitespace follows (not perfect, but seems to do the right thing most
1907 // of the time)
1908 if ((classification == CHAR_CLASS_QUOTE
1909 || classification == CHAR_CLASS_GRAPHICAL
1910 || classification == CHAR_CLASS_PARENS_CLOSE)
1911 && nextClassification == CHAR_CLASS_WHITESPACE) {
1912 return true;
1915 return false;
1919 float
1920 BTextView::LineWidth(int32 lineNumber) const
1922 if (lineNumber < 0 || lineNumber >= fLines->NumLines())
1923 return 0;
1925 STELine* line = (*fLines)[lineNumber];
1926 int32 length = (line + 1)->offset - line->offset;
1928 // skip newline at the end of the line, if any, as it does no contribute
1929 // to the width
1930 if (ByteAt((line + 1)->offset - 1) == B_ENTER)
1931 length--;
1933 return _TabExpandedStyledWidth(line->offset, length);
1937 float
1938 BTextView::LineHeight(int32 lineNumber) const
1940 float lineHeight = TextHeight(lineNumber, lineNumber);
1941 if (lineHeight == 0.0) {
1942 // We probably don't have text content yet. Take the initial
1943 // style's font height or fall back to the plain font.
1944 const BFont* font;
1945 fStyles->GetNullStyle(&font, NULL);
1946 if (font == NULL)
1947 font = be_plain_font;
1949 font_height fontHeight;
1950 font->GetHeight(&fontHeight);
1951 // This is how the height is calculated in _RecalculateLineBreaks().
1952 lineHeight = ceilf(fontHeight.ascent + fontHeight.descent) + 1;
1955 return lineHeight;
1959 float
1960 BTextView::TextHeight(int32 startLine, int32 endLine) const
1962 const int32 numLines = fLines->NumLines();
1963 if (startLine < 0)
1964 startLine = 0;
1965 else if (startLine > numLines - 1)
1966 startLine = numLines - 1;
1968 if (endLine < 0)
1969 endLine = 0;
1970 else if (endLine > numLines - 1)
1971 endLine = numLines - 1;
1973 float height = (*fLines)[endLine + 1]->origin
1974 - (*fLines)[startLine]->origin;
1976 if (startLine != endLine && endLine == numLines - 1
1977 && fText->RealCharAt(fText->Length() - 1) == B_ENTER) {
1978 height += (*fLines)[endLine + 1]->origin - (*fLines)[endLine]->origin;
1981 return ceilf(height);
1985 void
1986 BTextView::GetTextRegion(int32 startOffset, int32 endOffset,
1987 BRegion* outRegion) const
1989 if (!outRegion)
1990 return;
1992 outRegion->MakeEmpty();
1994 // pin offsets at reasonable values
1995 if (startOffset < 0)
1996 startOffset = 0;
1997 else if (startOffset > fText->Length())
1998 startOffset = fText->Length();
1999 if (endOffset < 0)
2000 endOffset = 0;
2001 else if (endOffset > fText->Length())
2002 endOffset = fText->Length();
2004 // return an empty region if the range is invalid
2005 if (startOffset >= endOffset)
2006 return;
2008 float startLineHeight = 0.0;
2009 float endLineHeight = 0.0;
2010 BPoint startPt = PointAt(startOffset, &startLineHeight);
2011 BPoint endPt = PointAt(endOffset, &endLineHeight);
2013 startLineHeight = ceilf(startLineHeight);
2014 endLineHeight = ceilf(endLineHeight);
2016 BRect selRect;
2018 if (startPt.y == endPt.y) {
2019 // this is a one-line region
2020 selRect.left = max_c(startPt.x, fTextRect.left);
2021 selRect.top = startPt.y;
2022 selRect.right = endPt.x - 1.0;
2023 selRect.bottom = endPt.y + endLineHeight - 1.0;
2024 outRegion->Include(selRect);
2025 } else {
2026 // more than one line in the specified offset range
2027 selRect.left = max_c(startPt.x, fTextRect.left);
2028 selRect.top = startPt.y;
2029 selRect.right = fTextRect.right;
2030 selRect.bottom = startPt.y + startLineHeight - 1.0;
2031 outRegion->Include(selRect);
2033 if (startPt.y + startLineHeight < endPt.y) {
2034 // more than two lines in the range
2035 selRect.left = fTextRect.left;
2036 selRect.top = startPt.y + startLineHeight;
2037 selRect.right = fTextRect.right;
2038 selRect.bottom = endPt.y - 1.0;
2039 outRegion->Include(selRect);
2042 selRect.left = fTextRect.left;
2043 selRect.top = endPt.y;
2044 selRect.right = endPt.x - 1.0;
2045 selRect.bottom = endPt.y + endLineHeight - 1.0;
2046 outRegion->Include(selRect);
2051 void
2052 BTextView::ScrollToOffset(int32 offset)
2054 // pin offset at reasonable values
2055 if (offset < 0)
2056 offset = 0;
2057 else if (offset > fText->Length())
2058 offset = fText->Length();
2060 BRect bounds = Bounds();
2061 float lineHeight = 0.0;
2062 float xDiff = 0.0;
2063 float yDiff = 0.0;
2064 BPoint point = PointAt(offset, &lineHeight);
2066 // horizontal
2067 float extraSpace = fAlignment == B_ALIGN_LEFT ?
2068 ceilf(bounds.IntegerWidth() / 2) : 0.0;
2070 if (point.x < bounds.left)
2071 xDiff = point.x - bounds.left - extraSpace;
2072 else if (point.x > bounds.right)
2073 xDiff = point.x - bounds.right + extraSpace;
2075 // vertical
2076 if (point.y < bounds.top)
2077 yDiff = point.y - bounds.top;
2078 else if (point.y + lineHeight > bounds.bottom
2079 && point.y - lineHeight > bounds.top) {
2080 yDiff = point.y + lineHeight - bounds.bottom;
2083 // prevent negative scroll offset
2084 if (bounds.left + xDiff < 0.0)
2085 xDiff = -bounds.left;
2086 if (bounds.top + yDiff < 0.0)
2087 yDiff = -bounds.top;
2089 ScrollBy(xDiff, yDiff);
2093 void
2094 BTextView::ScrollToSelection()
2096 ScrollToOffset(fSelStart);
2100 void
2101 BTextView::Highlight(int32 startOffset, int32 endOffset)
2103 // pin offsets at reasonable values
2104 if (startOffset < 0)
2105 startOffset = 0;
2106 else if (startOffset > fText->Length())
2107 startOffset = fText->Length();
2108 if (endOffset < 0)
2109 endOffset = 0;
2110 else if (endOffset > fText->Length())
2111 endOffset = fText->Length();
2113 if (startOffset >= endOffset)
2114 return;
2116 BRegion selRegion;
2117 GetTextRegion(startOffset, endOffset, &selRegion);
2119 SetDrawingMode(B_OP_INVERT);
2120 FillRegion(&selRegion, B_SOLID_HIGH);
2121 SetDrawingMode(B_OP_COPY);
2125 // #pragma mark - Configuration methods
2128 void
2129 BTextView::SetTextRect(BRect rect)
2131 if (rect == fTextRect)
2132 return;
2134 if (!fWrap) {
2135 rect.right = Bounds().right - fLayoutData->rightInset;
2136 rect.bottom = Bounds().bottom - fLayoutData->bottomInset;
2139 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), rect);
2141 _ResetTextRect();
2145 BRect
2146 BTextView::TextRect() const
2148 return fTextRect;
2152 void
2153 BTextView::_ResetTextRect()
2155 BRect oldTextRect(fTextRect);
2156 // reset text rect to bounds minus insets ...
2157 fTextRect = Bounds().OffsetToCopy(B_ORIGIN);
2158 fTextRect.left += fLayoutData->leftInset;
2159 fTextRect.top += fLayoutData->topInset;
2160 fTextRect.right -= fLayoutData->rightInset;
2161 fTextRect.bottom -= fLayoutData->bottomInset;
2163 // and rewrap (potentially adjusting the right and the bottom of the text
2164 // rect)
2165 _Refresh(0, TextLength(), false);
2167 // Make sure that the dirty area outside the text is redrawn too.
2168 BRegion invalid(oldTextRect | fTextRect);
2169 invalid.Exclude(fTextRect);
2170 Invalidate(&invalid);
2174 void
2175 BTextView::SetInsets(float left, float top, float right, float bottom)
2177 if (fLayoutData->leftInset == left
2178 && fLayoutData->topInset == top
2179 && fLayoutData->rightInset == right
2180 && fLayoutData->bottomInset == bottom)
2181 return;
2183 fLayoutData->leftInset = left;
2184 fLayoutData->topInset = top;
2185 fLayoutData->rightInset = right;
2186 fLayoutData->bottomInset = bottom;
2188 InvalidateLayout();
2189 Invalidate();
2193 void
2194 BTextView::GetInsets(float* _left, float* _top, float* _right,
2195 float* _bottom) const
2197 if (_left)
2198 *_left = fLayoutData->leftInset;
2199 if (_top)
2200 *_top = fLayoutData->topInset;
2201 if (_right)
2202 *_right = fLayoutData->rightInset;
2203 if (_bottom)
2204 *_bottom = fLayoutData->bottomInset;
2208 void
2209 BTextView::SetStylable(bool stylable)
2211 fStylable = stylable;
2215 bool
2216 BTextView::IsStylable() const
2218 return fStylable;
2222 void
2223 BTextView::SetTabWidth(float width)
2225 if (width == fTabWidth)
2226 return;
2228 fTabWidth = width;
2230 if (Window() != NULL)
2231 _Refresh(0, fText->Length(), false);
2235 float
2236 BTextView::TabWidth() const
2238 return fTabWidth;
2242 void
2243 BTextView::MakeSelectable(bool selectable)
2245 if (selectable == fSelectable)
2246 return;
2248 fSelectable = selectable;
2250 if (fActive && fSelStart != fSelEnd && Window() != NULL)
2251 Highlight(fSelStart, fSelEnd);
2255 bool
2256 BTextView::IsSelectable() const
2258 return fSelectable;
2262 void
2263 BTextView::MakeEditable(bool editable)
2265 if (editable == fEditable)
2266 return;
2268 fEditable = editable;
2269 // TextControls change the color of the text when
2270 // they are made editable, so we need to invalidate
2271 // the NULL style here
2272 // TODO: it works well, but it could be caused by a bug somewhere else
2273 if (fEditable)
2274 fStyles->InvalidateNullStyle();
2275 if (Window() != NULL && fActive) {
2276 if (!fEditable) {
2277 _HideCaret();
2278 _CancelInputMethod();
2284 bool
2285 BTextView::IsEditable() const
2287 return fEditable;
2291 void
2292 BTextView::SetWordWrap(bool wrap)
2294 if (wrap == fWrap)
2295 return;
2297 bool updateOnScreen = fActive && Window() != NULL;
2298 if (updateOnScreen) {
2299 // hide the caret, unhilite the selection
2300 if (fSelStart != fSelEnd) {
2301 if (fSelectable)
2302 Highlight(fSelStart, fSelEnd);
2303 } else
2304 _HideCaret();
2307 fWrap = wrap;
2308 if (wrap)
2309 _ResetTextRect();
2310 _Refresh(0, fText->Length(), false);
2312 if (updateOnScreen) {
2313 // show the caret, hilite the selection
2314 if (fSelStart != fSelEnd) {
2315 if (fSelectable)
2316 Highlight(fSelStart, fSelEnd);
2317 } else
2318 _ShowCaret();
2323 bool
2324 BTextView::DoesWordWrap() const
2326 return fWrap;
2330 void
2331 BTextView::SetMaxBytes(int32 max)
2333 const int32 textLength = fText->Length();
2334 fMaxBytes = max;
2336 if (fMaxBytes < textLength) {
2337 int32 offset = fMaxBytes;
2338 // Delete the text after fMaxBytes, but
2339 // respect multibyte characters boundaries.
2340 const int32 previousInitial = _PreviousInitialByte(offset);
2341 if (_NextInitialByte(previousInitial) != offset)
2342 offset = previousInitial;
2344 Delete(offset, textLength);
2349 int32
2350 BTextView::MaxBytes() const
2352 return fMaxBytes;
2356 void
2357 BTextView::DisallowChar(uint32 character)
2359 if (fDisallowedChars == NULL)
2360 fDisallowedChars = new BList;
2361 if (!fDisallowedChars->HasItem(reinterpret_cast<void*>(character)))
2362 fDisallowedChars->AddItem(reinterpret_cast<void*>(character));
2366 void
2367 BTextView::AllowChar(uint32 character)
2369 if (fDisallowedChars != NULL)
2370 fDisallowedChars->RemoveItem(reinterpret_cast<void*>(character));
2374 void
2375 BTextView::SetAlignment(alignment align)
2377 // Do a reality check
2378 if (fAlignment != align &&
2379 (align == B_ALIGN_LEFT ||
2380 align == B_ALIGN_RIGHT ||
2381 align == B_ALIGN_CENTER)) {
2382 fAlignment = align;
2384 // After setting new alignment, update the view/window
2385 if (Window() != NULL)
2386 Invalidate();
2391 alignment
2392 BTextView::Alignment() const
2394 return fAlignment;
2398 void
2399 BTextView::SetAutoindent(bool state)
2401 fAutoindent = state;
2405 bool
2406 BTextView::DoesAutoindent() const
2408 return fAutoindent;
2412 void
2413 BTextView::SetColorSpace(color_space colors)
2415 if (colors != fColorSpace && fOffscreen) {
2416 fColorSpace = colors;
2417 _DeleteOffscreen();
2418 _NewOffscreen();
2423 color_space
2424 BTextView::ColorSpace() const
2426 return fColorSpace;
2430 void
2431 BTextView::MakeResizable(bool resize, BView* resizeView)
2433 if (resize) {
2434 fResizable = true;
2435 fContainerView = resizeView;
2437 // Wrapping mode and resizable mode can't live together
2438 if (fWrap) {
2439 fWrap = false;
2441 if (fActive && Window() != NULL) {
2442 if (fSelStart != fSelEnd) {
2443 if (fSelectable)
2444 Highlight(fSelStart, fSelEnd);
2445 } else
2446 _HideCaret();
2449 // We need to reset the right inset, as otherwise the auto-resize would
2450 // get confused about just how wide the textview needs to be.
2451 // This seems to be an artefact of how Tracker creates the textview
2452 // during a rename action.
2453 fLayoutData->rightInset = fLayoutData->leftInset;
2454 } else {
2455 fResizable = false;
2456 fContainerView = NULL;
2457 if (fOffscreen)
2458 _DeleteOffscreen();
2459 _NewOffscreen();
2462 _Refresh(0, fText->Length(), false);
2466 bool
2467 BTextView::IsResizable() const
2469 return fResizable;
2473 void
2474 BTextView::SetDoesUndo(bool undo)
2476 if (undo && fUndo == NULL)
2477 fUndo = new UndoBuffer(this, B_UNDO_UNAVAILABLE);
2478 else if (!undo && fUndo != NULL) {
2479 delete fUndo;
2480 fUndo = NULL;
2485 bool
2486 BTextView::DoesUndo() const
2488 return fUndo != NULL;
2492 void
2493 BTextView::HideTyping(bool enabled)
2495 if (enabled)
2496 Delete(0, fText->Length());
2498 fText->SetPasswordMode(enabled);
2502 bool
2503 BTextView::IsTypingHidden() const
2505 return fText->PasswordMode();
2509 // #pragma mark - Size methods
2512 void
2513 BTextView::ResizeToPreferred()
2515 BView::ResizeToPreferred();
2519 void
2520 BTextView::GetPreferredSize(float* _width, float* _height)
2522 CALLED();
2524 _ValidateLayoutData();
2526 if (_width) {
2527 float width = Bounds().Width();
2528 if (width < fLayoutData->min.width
2529 || (Flags() & B_SUPPORTS_LAYOUT) != 0) {
2530 width = fLayoutData->min.width;
2532 *_width = width;
2535 if (_height) {
2536 float height = Bounds().Height();
2537 if (height < fLayoutData->min.height
2538 || (Flags() & B_SUPPORTS_LAYOUT) != 0) {
2539 height = fLayoutData->min.height;
2541 *_height = height;
2546 BSize
2547 BTextView::MinSize()
2549 CALLED();
2551 _ValidateLayoutData();
2552 return BLayoutUtils::ComposeSize(ExplicitMinSize(), fLayoutData->min);
2556 BSize
2557 BTextView::MaxSize()
2559 CALLED();
2561 return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
2562 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
2566 BSize
2567 BTextView::PreferredSize()
2569 CALLED();
2571 _ValidateLayoutData();
2572 return BLayoutUtils::ComposeSize(ExplicitPreferredSize(),
2573 fLayoutData->preferred);
2577 bool
2578 BTextView::HasHeightForWidth()
2580 if (IsEditable())
2581 return BView::HasHeightForWidth();
2583 // When not editable, we assume that all text is supposed to be visible.
2584 return true;
2588 void
2589 BTextView::GetHeightForWidth(float width, float* min, float* max,
2590 float* preferred)
2592 if (IsEditable()) {
2593 BView::GetHeightForWidth(width, min, max, preferred);
2594 return;
2597 // TODO: don't change the actual text rect!
2598 fTextRect.right = fTextRect.left + width;
2599 _Refresh(0, TextLength(), false);
2601 if (min != NULL)
2602 *min = fTextRect.Height();
2603 if (max != NULL)
2604 *max = fTextRect.Height();
2605 if (preferred != NULL)
2606 *preferred = fTextRect.Height();
2610 // #pragma mark - Layout methods
2613 void
2614 BTextView::LayoutInvalidated(bool descendants)
2616 CALLED();
2618 fLayoutData->valid = false;
2622 void
2623 BTextView::DoLayout()
2625 // Bail out, if we shan't do layout.
2626 if (!(Flags() & B_SUPPORTS_LAYOUT))
2627 return;
2629 CALLED();
2631 // If the user set a layout, we let the base class version call its
2632 // hook.
2633 if (GetLayout()) {
2634 BView::DoLayout();
2635 return;
2638 _ValidateLayoutData();
2640 // validate current size
2641 BSize size(Bounds().Size());
2642 if (size.width < fLayoutData->min.width)
2643 size.width = fLayoutData->min.width;
2644 if (size.height < fLayoutData->min.height)
2645 size.height = fLayoutData->min.height;
2647 _ResetTextRect();
2651 void
2652 BTextView::_ValidateLayoutData()
2654 if (fLayoutData->valid)
2655 return;
2657 CALLED();
2659 float lineHeight = ceilf(LineHeight(0));
2660 TRACE("line height: %.2f\n", lineHeight);
2662 // compute our minimal size
2663 BSize min(lineHeight * 3, lineHeight);
2664 min.width += fLayoutData->leftInset + fLayoutData->rightInset;
2665 min.height += fLayoutData->topInset + fLayoutData->bottomInset;
2667 fLayoutData->min = min;
2669 // compute our preferred size
2670 fLayoutData->preferred.height = fTextRect.Height()
2671 + fLayoutData->topInset + fLayoutData->bottomInset;
2673 if (fWrap)
2674 fLayoutData->preferred.width = min.width + 5 * lineHeight;
2675 else {
2676 float maxWidth = fLines->MaxWidth();
2677 if (maxWidth < min.width)
2678 maxWidth = min.width;
2680 fLayoutData->preferred.width
2681 = maxWidth + fLayoutData->leftInset + fLayoutData->rightInset;
2684 fLayoutData->valid = true;
2685 ResetLayoutInvalidation();
2687 TRACE("width: %.2f, height: %.2f\n", min.width, min.height);
2691 // #pragma mark -
2694 void
2695 BTextView::AllAttached()
2697 BView::AllAttached();
2701 void
2702 BTextView::AllDetached()
2704 BView::AllDetached();
2708 /* static */
2709 text_run_array*
2710 BTextView::AllocRunArray(int32 entryCount, int32* outSize)
2712 int32 size = sizeof(text_run_array) + (entryCount - 1) * sizeof(text_run);
2714 text_run_array* runArray = (text_run_array*)calloc(size, 1);
2715 if (runArray == NULL) {
2716 if (outSize != NULL)
2717 *outSize = 0;
2718 return NULL;
2721 runArray->count = entryCount;
2723 // Call constructors explicitly as the text_run_array
2724 // was allocated with malloc (and has to, for backwards
2725 // compatibility)
2726 for (int32 i = 0; i < runArray->count; i++) {
2727 new (&runArray->runs[i].font) BFont;
2730 if (outSize != NULL)
2731 *outSize = size;
2733 return runArray;
2737 /* static */
2738 text_run_array*
2739 BTextView::CopyRunArray(const text_run_array* orig, int32 countDelta)
2741 text_run_array* copy = AllocRunArray(countDelta, NULL);
2742 if (copy != NULL) {
2743 for (int32 i = 0; i < countDelta; i++) {
2744 copy->runs[i].offset = orig->runs[i].offset;
2745 copy->runs[i].font = orig->runs[i].font;
2746 copy->runs[i].color = orig->runs[i].color;
2749 return copy;
2753 /* static */
2754 void
2755 BTextView::FreeRunArray(text_run_array* array)
2757 if (array == NULL)
2758 return;
2760 // Call destructors explicitly
2761 for (int32 i = 0; i < array->count; i++)
2762 array->runs[i].font.~BFont();
2764 free(array);
2768 /* static */
2769 void*
2770 BTextView::FlattenRunArray(const text_run_array* runArray, int32* _size)
2772 CALLED();
2773 int32 size = sizeof(flattened_text_run_array) + (runArray->count - 1)
2774 * sizeof(flattened_text_run);
2776 flattened_text_run_array* array = (flattened_text_run_array*)malloc(size);
2777 if (array == NULL) {
2778 if (_size)
2779 *_size = 0;
2780 return NULL;
2783 array->magic = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayMagic);
2784 array->version = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayVersion);
2785 array->count = B_HOST_TO_BENDIAN_INT32(runArray->count);
2787 for (int32 i = 0; i < runArray->count; i++) {
2788 array->styles[i].offset = B_HOST_TO_BENDIAN_INT32(
2789 runArray->runs[i].offset);
2790 runArray->runs[i].font.GetFamilyAndStyle(&array->styles[i].family,
2791 &array->styles[i].style);
2792 array->styles[i].size = B_HOST_TO_BENDIAN_FLOAT(
2793 runArray->runs[i].font.Size());
2794 array->styles[i].shear = B_HOST_TO_BENDIAN_FLOAT(
2795 runArray->runs[i].font.Shear());
2796 array->styles[i].face = B_HOST_TO_BENDIAN_INT16(
2797 runArray->runs[i].font.Face());
2798 array->styles[i].red = runArray->runs[i].color.red;
2799 array->styles[i].green = runArray->runs[i].color.green;
2800 array->styles[i].blue = runArray->runs[i].color.blue;
2801 array->styles[i].alpha = 255;
2802 array->styles[i]._reserved_ = 0;
2805 if (_size)
2806 *_size = size;
2808 return array;
2812 /* static */
2813 text_run_array*
2814 BTextView::UnflattenRunArray(const void* data, int32* _size)
2816 CALLED();
2817 flattened_text_run_array* array = (flattened_text_run_array*)data;
2819 if (B_BENDIAN_TO_HOST_INT32(array->magic) != kFlattenedTextRunArrayMagic
2820 || B_BENDIAN_TO_HOST_INT32(array->version)
2821 != kFlattenedTextRunArrayVersion) {
2822 if (_size)
2823 *_size = 0;
2825 return NULL;
2828 int32 count = B_BENDIAN_TO_HOST_INT32(array->count);
2830 text_run_array* runArray = AllocRunArray(count, _size);
2831 if (runArray == NULL)
2832 return NULL;
2834 for (int32 i = 0; i < count; i++) {
2835 runArray->runs[i].offset = B_BENDIAN_TO_HOST_INT32(
2836 array->styles[i].offset);
2838 // Set family and style independently from each other, so that
2839 // even if the family doesn't exist, we try to preserve the style
2840 runArray->runs[i].font.SetFamilyAndStyle(array->styles[i].family, NULL);
2841 runArray->runs[i].font.SetFamilyAndStyle(NULL, array->styles[i].style);
2843 runArray->runs[i].font.SetSize(B_BENDIAN_TO_HOST_FLOAT(
2844 array->styles[i].size));
2845 runArray->runs[i].font.SetShear(B_BENDIAN_TO_HOST_FLOAT(
2846 array->styles[i].shear));
2848 uint16 face = B_BENDIAN_TO_HOST_INT16(array->styles[i].face);
2849 if (face != B_REGULAR_FACE) {
2850 // Be's version doesn't seem to set this correctly
2851 runArray->runs[i].font.SetFace(face);
2854 runArray->runs[i].color.red = array->styles[i].red;
2855 runArray->runs[i].color.green = array->styles[i].green;
2856 runArray->runs[i].color.blue = array->styles[i].blue;
2857 runArray->runs[i].color.alpha = array->styles[i].alpha;
2860 return runArray;
2864 void
2865 BTextView::InsertText(const char* text, int32 length, int32 offset,
2866 const text_run_array* runs)
2868 CALLED();
2870 if (length < 0)
2871 length = 0;
2873 if (offset < 0)
2874 offset = 0;
2875 else if (offset > fText->Length())
2876 offset = fText->Length();
2878 if (length > 0) {
2879 // add the text to the buffer
2880 fText->InsertText(text, length, offset);
2882 // update the start offsets of each line below offset
2883 fLines->BumpOffset(length, _LineAt(offset) + 1);
2885 // update the style runs
2886 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1);
2888 // offset the caret/selection, if the text was inserted before it
2889 if (offset <= fSelEnd) {
2890 fSelStart += length;
2891 fCaretOffset = fSelEnd = fSelStart;
2895 if (fStylable && runs != NULL) {
2896 _SetRunArray(offset, offset + length, runs);
2897 } else {
2898 // apply null-style to inserted text
2899 _ApplyStyleRange(offset, offset + length);
2904 void
2905 BTextView::DeleteText(int32 fromOffset, int32 toOffset)
2907 CALLED();
2909 if (fromOffset < 0)
2910 fromOffset = 0;
2911 else if (fromOffset > fText->Length())
2912 fromOffset = fText->Length();
2914 if (toOffset < 0)
2915 toOffset = 0;
2916 else if (toOffset > fText->Length())
2917 toOffset = fText->Length();
2919 if (fromOffset >= toOffset)
2920 return;
2922 // set nullStyle to style at beginning of range
2923 fStyles->InvalidateNullStyle();
2924 fStyles->SyncNullStyle(fromOffset);
2926 // remove from the text buffer
2927 fText->RemoveRange(fromOffset, toOffset);
2929 // remove any lines that have been obliterated
2930 fLines->RemoveLineRange(fromOffset, toOffset);
2932 // remove any style runs that have been obliterated
2933 fStyles->RemoveStyleRange(fromOffset, toOffset);
2935 // adjust the selection accordingly, assumes fSelEnd >= fSelStart!
2936 int32 range = toOffset - fromOffset;
2937 if (fSelStart >= toOffset) {
2938 // selection is behind the range that was removed
2939 fSelStart -= range;
2940 fSelEnd -= range;
2941 } else if (fSelStart >= fromOffset && fSelEnd <= toOffset) {
2942 // the selection is within the range that was removed
2943 fSelStart = fSelEnd = fromOffset;
2944 } else if (fSelStart >= fromOffset && fSelEnd > toOffset) {
2945 // the selection starts within and ends after the range
2946 // the remaining part is the part that was after the range
2947 fSelStart = fromOffset;
2948 fSelEnd = fromOffset + fSelEnd - toOffset;
2949 } else if (fSelStart < fromOffset && fSelEnd < toOffset) {
2950 // the selection starts before, but ends within the range
2951 fSelEnd = fromOffset;
2952 } else if (fSelStart < fromOffset && fSelEnd >= toOffset) {
2953 // the selection starts before and ends after the range
2954 fSelEnd -= range;
2959 /*! Undoes the last changes.
2961 \param clipboard A \a clipboard to use for the undo operation.
2963 void
2964 BTextView::Undo(BClipboard* clipboard)
2966 if (fUndo)
2967 fUndo->Undo(clipboard);
2971 undo_state
2972 BTextView::UndoState(bool* isRedo) const
2974 return fUndo == NULL ? B_UNDO_UNAVAILABLE : fUndo->State(isRedo);
2978 // #pragma mark - GetDragParameters() is protected
2981 void
2982 BTextView::GetDragParameters(BMessage* drag, BBitmap** bitmap, BPoint* point,
2983 BHandler** handler)
2985 CALLED();
2986 if (drag == NULL)
2987 return;
2989 // Add originator and action
2990 drag->AddPointer("be:originator", this);
2991 drag->AddInt32("be_actions", B_TRASH_TARGET);
2993 // add the text
2994 int32 numBytes = fSelEnd - fSelStart;
2995 const char* text = fText->GetString(fSelStart, &numBytes);
2996 drag->AddData("text/plain", B_MIME_TYPE, text, numBytes);
2998 // add the corresponding styles
2999 int32 size = 0;
3000 text_run_array* styles = RunArray(fSelStart, fSelEnd, &size);
3002 if (styles != NULL) {
3003 drag->AddData("application/x-vnd.Be-text_run_array", B_MIME_TYPE,
3004 styles, size);
3006 FreeRunArray(styles);
3009 if (bitmap != NULL)
3010 *bitmap = NULL;
3012 if (handler != NULL)
3013 *handler = NULL;
3017 // #pragma mark - FBC padding and forbidden methods
3020 void BTextView::_ReservedTextView3() {}
3021 void BTextView::_ReservedTextView4() {}
3022 void BTextView::_ReservedTextView5() {}
3023 void BTextView::_ReservedTextView6() {}
3024 void BTextView::_ReservedTextView7() {}
3025 void BTextView::_ReservedTextView8() {}
3026 void BTextView::_ReservedTextView9() {}
3027 void BTextView::_ReservedTextView10() {}
3028 void BTextView::_ReservedTextView11() {}
3029 void BTextView::_ReservedTextView12() {}
3032 // #pragma mark - Private methods
3035 /*! Inits the BTextView object.
3037 \param textRect The BTextView's text rect.
3038 \param initialFont The font which the BTextView will use.
3039 \param initialColor The initial color of the text.
3041 void
3042 BTextView::_InitObject(BRect textRect, const BFont* initialFont,
3043 const rgb_color* initialColor)
3045 BFont font;
3046 if (initialFont == NULL)
3047 GetFont(&font);
3048 else
3049 font = *initialFont;
3051 _NormalizeFont(&font);
3053 if (initialColor == NULL)
3054 initialColor = &kBlackColor;
3056 fText = new BPrivate::TextGapBuffer;
3057 fLines = new LineBuffer;
3058 fStyles = new StyleBuffer(&font, initialColor);
3060 fInstalledNavigateCommandWordwiseShortcuts = false;
3061 fInstalledNavigateOptionWordwiseShortcuts = false;
3062 fInstalledNavigateOptionLinewiseShortcuts = false;
3063 fInstalledNavigateHomeEndDocwiseShortcuts = false;
3065 fInstalledSelectCommandWordwiseShortcuts = false;
3066 fInstalledSelectOptionWordwiseShortcuts = false;
3067 fInstalledSelectOptionLinewiseShortcuts = false;
3068 fInstalledSelectHomeEndDocwiseShortcuts = false;
3070 // We put these here instead of in the constructor initializer list
3071 // to have less code duplication, and a single place where to do changes
3072 // if needed.
3073 fTextRect = textRect;
3074 // NOTE: The only places where text rect is changed:
3075 // * width is possibly adjusted in _AutoResize(),
3076 // * height is adjusted in _RecalculateLineBreaks().
3077 // When used within the layout management framework, the
3078 // text rect is changed to maintain constant insets.
3079 fMinTextRectWidth = fTextRect.Width();
3080 // see SetTextRect()
3081 fSelStart = fSelEnd = 0;
3082 fCaretVisible = false;
3083 fCaretTime = 0;
3084 fCaretOffset = 0;
3085 fClickCount = 0;
3086 fClickTime = 0;
3087 fDragOffset = -1;
3088 fCursor = 0;
3089 fActive = false;
3090 fStylable = false;
3091 fTabWidth = 28.0;
3092 fSelectable = true;
3093 fEditable = true;
3094 fWrap = true;
3095 fMaxBytes = INT32_MAX;
3096 fDisallowedChars = NULL;
3097 fAlignment = B_ALIGN_LEFT;
3098 fAutoindent = false;
3099 fOffscreen = NULL;
3100 fColorSpace = B_CMAP8;
3101 fResizable = false;
3102 fContainerView = NULL;
3103 fUndo = NULL;
3104 fInline = NULL;
3105 fDragRunner = NULL;
3106 fClickRunner = NULL;
3107 fTrackingMouse = NULL;
3109 fLayoutData = new LayoutData;
3110 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), fTextRect);
3112 fLastClickOffset = -1;
3114 SetDoesUndo(true);
3118 //! Handles when Backspace key is pressed.
3119 void
3120 BTextView::_HandleBackspace()
3122 if (fUndo) {
3123 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(
3124 fUndo);
3125 if (!undoBuffer) {
3126 delete fUndo;
3127 fUndo = undoBuffer = new TypingUndoBuffer(this);
3129 undoBuffer->BackwardErase();
3132 if (fSelStart == fSelEnd) {
3133 if (fSelStart == 0)
3134 return;
3135 else
3136 fSelStart = _PreviousInitialByte(fSelStart);
3137 } else
3138 Highlight(fSelStart, fSelEnd);
3140 DeleteText(fSelStart, fSelEnd);
3141 fCaretOffset = fSelEnd = fSelStart;
3143 _Refresh(fSelStart, fSelEnd, true);
3147 //! Handles when an arrow key is pressed.
3148 void
3149 BTextView::_HandleArrowKey(uint32 arrowKey, int32 modifiers)
3151 // return if there's nowhere to go
3152 if (fText->Length() == 0)
3153 return;
3155 int32 selStart = fSelStart;
3156 int32 selEnd = fSelEnd;
3158 if (modifiers < 0) {
3159 BMessage* currentMessage = Window()->CurrentMessage();
3160 if (currentMessage == NULL
3161 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) {
3162 modifiers = 0;
3166 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0;
3167 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0;
3168 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0;
3169 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0;
3171 int32 lastClickOffset = fCaretOffset;
3173 switch (arrowKey) {
3174 case B_LEFT_ARROW:
3175 if (!fEditable)
3176 _ScrollBy(-1 * kHorizontalScrollBarStep, 0);
3177 else if (fSelStart != fSelEnd && !shiftKeyDown)
3178 fCaretOffset = fSelStart;
3179 else {
3180 if ((commandKeyDown || optionKeyDown) && !controlKeyDown)
3181 fCaretOffset = _PreviousWordStart(fCaretOffset - 1);
3182 else
3183 fCaretOffset = _PreviousInitialByte(fCaretOffset);
3185 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3186 if (fCaretOffset < fSelStart) {
3187 // extend selection to the left
3188 selStart = fCaretOffset;
3189 if (lastClickOffset > fSelStart) {
3190 // caret has jumped across "anchor"
3191 selEnd = fSelStart;
3193 } else {
3194 // shrink selection from the right
3195 selEnd = fCaretOffset;
3199 break;
3201 case B_RIGHT_ARROW:
3202 if (!fEditable)
3203 _ScrollBy(kHorizontalScrollBarStep, 0);
3204 else if (fSelStart != fSelEnd && !shiftKeyDown)
3205 fCaretOffset = fSelEnd;
3206 else {
3207 if ((commandKeyDown || optionKeyDown) && !controlKeyDown)
3208 fCaretOffset = _NextWordEnd(fCaretOffset);
3209 else
3210 fCaretOffset = _NextInitialByte(fCaretOffset);
3212 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3213 if (fCaretOffset > fSelEnd) {
3214 // extend selection to the right
3215 selEnd = fCaretOffset;
3216 if (lastClickOffset < fSelEnd) {
3217 // caret has jumped across "anchor"
3218 selStart = fSelEnd;
3220 } else {
3221 // shrink selection from the left
3222 selStart = fCaretOffset;
3226 break;
3228 case B_UP_ARROW:
3230 if (!fEditable)
3231 _ScrollBy(0, -1 * kVerticalScrollBarStep);
3232 else if (fSelStart != fSelEnd && !shiftKeyDown)
3233 fCaretOffset = fSelStart;
3234 else {
3235 if (optionKeyDown && !commandKeyDown && !controlKeyDown)
3236 fCaretOffset = _PreviousLineStart(fCaretOffset);
3237 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3238 _ScrollTo(0, 0);
3239 fCaretOffset = 0;
3240 } else {
3241 float height;
3242 BPoint point = PointAt(fCaretOffset, &height);
3243 // find the caret position on the previous
3244 // line by gently stepping onto this line
3245 for (int i = 1; i <= height; i++) {
3246 point.y--;
3247 int32 offset = OffsetAt(point);
3248 if (offset < fCaretOffset || i == height) {
3249 fCaretOffset = offset;
3250 break;
3255 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3256 if (fCaretOffset < fSelStart) {
3257 // extend selection to the top
3258 selStart = fCaretOffset;
3259 if (lastClickOffset > fSelStart) {
3260 // caret has jumped across "anchor"
3261 selEnd = fSelStart;
3263 } else {
3264 // shrink selection from the bottom
3265 selEnd = fCaretOffset;
3269 break;
3272 case B_DOWN_ARROW:
3274 if (!fEditable)
3275 _ScrollBy(0, kVerticalScrollBarStep);
3276 else if (fSelStart != fSelEnd && !shiftKeyDown)
3277 fCaretOffset = fSelEnd;
3278 else {
3279 if (optionKeyDown && !commandKeyDown && !controlKeyDown)
3280 fCaretOffset = _NextLineEnd(fCaretOffset);
3281 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3282 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset);
3283 fCaretOffset = fText->Length();
3284 } else {
3285 float height;
3286 BPoint point = PointAt(fCaretOffset, &height);
3287 point.y += height;
3288 fCaretOffset = OffsetAt(point);
3291 if (shiftKeyDown && fCaretOffset != lastClickOffset) {
3292 if (fCaretOffset > fSelEnd) {
3293 // extend selection to the bottom
3294 selEnd = fCaretOffset;
3295 if (lastClickOffset < fSelEnd) {
3296 // caret has jumped across "anchor"
3297 selStart = fSelEnd;
3299 } else {
3300 // shrink selection from the top
3301 selStart = fCaretOffset;
3305 break;
3309 fStyles->InvalidateNullStyle();
3311 if (fEditable) {
3312 if (shiftKeyDown)
3313 Select(selStart, selEnd);
3314 else
3315 Select(fCaretOffset, fCaretOffset);
3317 // scroll if needed
3318 ScrollToOffset(fCaretOffset);
3323 //! Handles when the Delete key is pressed.
3324 void
3325 BTextView::_HandleDelete()
3327 if (fUndo) {
3328 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(
3329 fUndo);
3330 if (!undoBuffer) {
3331 delete fUndo;
3332 fUndo = undoBuffer = new TypingUndoBuffer(this);
3334 undoBuffer->ForwardErase();
3337 if (fSelStart == fSelEnd) {
3338 if (fSelEnd == fText->Length())
3339 return;
3340 else
3341 fSelEnd = _NextInitialByte(fSelEnd);
3342 } else
3343 Highlight(fSelStart, fSelEnd);
3345 DeleteText(fSelStart, fSelEnd);
3346 fCaretOffset = fSelEnd = fSelStart;
3348 _Refresh(fSelStart, fSelEnd, true);
3352 //! Handles when the Page Up, Page Down, Home, or End key is pressed.
3353 void
3354 BTextView::_HandlePageKey(uint32 pageKey, int32 modifiers)
3356 if (modifiers < 0) {
3357 BMessage* currentMessage = Window()->CurrentMessage();
3358 if (currentMessage == NULL
3359 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) {
3360 modifiers = 0;
3364 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0;
3365 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0;
3366 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0;
3367 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0;
3369 STELine* line = NULL;
3370 int32 selStart = fSelStart;
3371 int32 selEnd = fSelEnd;
3373 int32 lastClickOffset = fCaretOffset;
3374 switch (pageKey) {
3375 case B_HOME:
3376 if (!fEditable) {
3377 fCaretOffset = 0;
3378 _ScrollTo(0, 0);
3379 break;
3380 } else {
3381 if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3382 _ScrollTo(0, 0);
3383 fCaretOffset = 0;
3384 } else {
3385 // get the start of the last line if caret is on it
3386 line = (*fLines)[_LineAt(lastClickOffset)];
3387 fCaretOffset = line->offset;
3390 if (!shiftKeyDown)
3391 selStart = selEnd = fCaretOffset;
3392 else if (fCaretOffset != lastClickOffset) {
3393 if (fCaretOffset < fSelStart) {
3394 // extend selection to the left
3395 selStart = fCaretOffset;
3396 if (lastClickOffset > fSelStart) {
3397 // caret has jumped across "anchor"
3398 selEnd = fSelStart;
3400 } else {
3401 // shrink selection from the right
3402 selEnd = fCaretOffset;
3406 break;
3408 case B_END:
3409 if (!fEditable) {
3410 fCaretOffset = fText->Length();
3411 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset);
3412 break;
3413 } else {
3414 if (commandKeyDown && !optionKeyDown && !controlKeyDown) {
3415 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset);
3416 fCaretOffset = fText->Length();
3417 } else {
3418 // If we are on the last line, just go to the last
3419 // character in the buffer, otherwise get the starting
3420 // offset of the next line, and go to the previous character
3421 int32 currentLine = _LineAt(lastClickOffset);
3422 if (currentLine + 1 < fLines->NumLines()) {
3423 line = (*fLines)[currentLine + 1];
3424 fCaretOffset = _PreviousInitialByte(line->offset);
3425 } else {
3426 // This check is needed to avoid moving the cursor
3427 // when the cursor is on the last line, and that line
3428 // is empty
3429 if (fCaretOffset != fText->Length()) {
3430 fCaretOffset = fText->Length();
3431 if (ByteAt(fCaretOffset - 1) == B_ENTER)
3432 fCaretOffset--;
3437 if (!shiftKeyDown)
3438 selStart = selEnd = fCaretOffset;
3439 else if (fCaretOffset != lastClickOffset) {
3440 if (fCaretOffset > fSelEnd) {
3441 // extend selection to the right
3442 selEnd = fCaretOffset;
3443 if (lastClickOffset < fSelEnd) {
3444 // caret has jumped across "anchor"
3445 selStart = fSelEnd;
3447 } else {
3448 // shrink selection from the left
3449 selStart = fCaretOffset;
3453 break;
3455 case B_PAGE_UP:
3457 float lineHeight;
3458 BPoint currentPos = PointAt(fCaretOffset, &lineHeight);
3459 BPoint nextPos(currentPos.x,
3460 currentPos.y + lineHeight - Bounds().Height());
3461 fCaretOffset = OffsetAt(nextPos);
3462 nextPos = PointAt(fCaretOffset);
3463 _ScrollBy(0, nextPos.y - currentPos.y);
3465 if (!fEditable)
3466 break;
3468 if (!shiftKeyDown)
3469 selStart = selEnd = fCaretOffset;
3470 else if (fCaretOffset != lastClickOffset) {
3471 if (fCaretOffset < fSelStart) {
3472 // extend selection to the top
3473 selStart = fCaretOffset;
3474 if (lastClickOffset > fSelStart) {
3475 // caret has jumped across "anchor"
3476 selEnd = fSelStart;
3478 } else {
3479 // shrink selection from the bottom
3480 selEnd = fCaretOffset;
3484 break;
3487 case B_PAGE_DOWN:
3489 BPoint currentPos = PointAt(fCaretOffset);
3490 BPoint nextPos(currentPos.x, currentPos.y + Bounds().Height());
3491 fCaretOffset = OffsetAt(nextPos);
3492 nextPos = PointAt(fCaretOffset);
3493 _ScrollBy(0, nextPos.y - currentPos.y);
3495 if (!fEditable)
3496 break;
3498 if (!shiftKeyDown)
3499 selStart = selEnd = fCaretOffset;
3500 else if (fCaretOffset != lastClickOffset) {
3501 if (fCaretOffset > fSelEnd) {
3502 // extend selection to the bottom
3503 selEnd = fCaretOffset;
3504 if (lastClickOffset < fSelEnd) {
3505 // caret has jumped across "anchor"
3506 selStart = fSelEnd;
3508 } else {
3509 // shrink selection from the top
3510 selStart = fCaretOffset;
3514 break;
3518 if (fEditable) {
3519 if (shiftKeyDown)
3520 Select(selStart, selEnd);
3521 else
3522 Select(fCaretOffset, fCaretOffset);
3524 ScrollToOffset(fCaretOffset);
3529 /*! Handles when an alpha-numeric key is pressed.
3531 \param bytes The string or character associated with the key.
3532 \param numBytes The amount of bytes containes in "bytes".
3534 void
3535 BTextView::_HandleAlphaKey(const char* bytes, int32 numBytes)
3537 // TODO: block input if not editable (Andrew)
3538 if (fUndo) {
3539 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(fUndo);
3540 if (!undoBuffer) {
3541 delete fUndo;
3542 fUndo = undoBuffer = new TypingUndoBuffer(this);
3544 undoBuffer->InputCharacter(numBytes);
3547 if (fSelStart != fSelEnd) {
3548 Highlight(fSelStart, fSelEnd);
3549 DeleteText(fSelStart, fSelEnd);
3552 if (fAutoindent && numBytes == 1 && *bytes == B_ENTER) {
3553 int32 start, offset;
3554 start = offset = OffsetAt(_LineAt(fSelStart));
3556 while (ByteAt(offset) != '\0' &&
3557 (ByteAt(offset) == B_TAB || ByteAt(offset) == B_SPACE)
3558 && offset < fSelStart)
3559 offset++;
3561 _DoInsertText(bytes, numBytes, fSelStart, NULL);
3562 if (start != offset)
3563 _DoInsertText(Text() + start, offset - start, fSelStart, NULL);
3564 } else
3565 _DoInsertText(bytes, numBytes, fSelStart, NULL);
3567 fCaretOffset = fSelEnd;
3569 ScrollToOffset(fCaretOffset);
3573 /*! Redraw the text between the two given offsets, recalculating line-breaks
3574 if needed.
3576 \param fromOffset The offset from where to refresh.
3577 \param toOffset The offset where to refresh to.
3578 \param scroll If \c true, scroll the view to the end offset.
3580 void
3581 BTextView::_Refresh(int32 fromOffset, int32 toOffset, bool scroll)
3583 // TODO: Cleanup
3584 float saveHeight = fTextRect.Height();
3585 float saveWidth = fTextRect.Width();
3586 int32 fromLine = _LineAt(fromOffset);
3587 int32 toLine = _LineAt(toOffset);
3588 int32 saveFromLine = fromLine;
3589 int32 saveToLine = toLine;
3591 _RecalculateLineBreaks(&fromLine, &toLine);
3593 // TODO: Maybe there is still something we can do without a window...
3594 if (!Window())
3595 return;
3597 BRect bounds = Bounds();
3598 float newHeight = fTextRect.Height();
3600 // if the line breaks have changed, force an erase
3601 if (fromLine != saveFromLine || toLine != saveToLine
3602 || newHeight != saveHeight) {
3603 fromOffset = -1;
3606 if (newHeight != saveHeight) {
3607 // the text area has changed
3608 if (newHeight < saveHeight)
3609 toLine = _LineAt(BPoint(0.0f, saveHeight + fTextRect.top));
3610 else
3611 toLine = _LineAt(BPoint(0.0f, newHeight + fTextRect.top));
3614 // draw only those lines that are visible
3615 int32 fromVisible = _LineAt(BPoint(0.0f, bounds.top));
3616 int32 toVisible = _LineAt(BPoint(0.0f, bounds.bottom));
3617 fromLine = max_c(fromVisible, fromLine);
3618 toLine = min_c(toLine, toVisible);
3620 _AutoResize(false);
3622 _RequestDrawLines(fromLine, toLine);
3624 // erase the area below the text
3625 BRect eraseRect = bounds;
3626 eraseRect.top = fTextRect.top + (*fLines)[fLines->NumLines()]->origin;
3627 eraseRect.bottom = fTextRect.top + saveHeight;
3628 if (eraseRect.bottom > eraseRect.top && eraseRect.Intersects(bounds)) {
3629 SetLowColor(ViewColor());
3630 FillRect(eraseRect, B_SOLID_LOW);
3633 // update the scroll bars if the text area has changed
3634 if (newHeight != saveHeight || fMinTextRectWidth != saveWidth)
3635 _UpdateScrollbars();
3637 if (scroll)
3638 ScrollToOffset(fSelEnd);
3640 Flush();
3644 /*! Recalculate line breaks between two lines.
3646 \param startLine The line number to start recalculating line breaks.
3647 \param endLine The line number to stop recalculating line breaks.
3649 void
3650 BTextView::_RecalculateLineBreaks(int32* startLine, int32* endLine)
3652 CALLED();
3654 // are we insane?
3655 *startLine = (*startLine < 0) ? 0 : *startLine;
3656 *endLine = (*endLine > fLines->NumLines() - 1) ? fLines->NumLines() - 1
3657 : *endLine;
3659 int32 textLength = fText->Length();
3660 int32 lineIndex = (*startLine > 0) ? *startLine - 1 : 0;
3661 int32 recalThreshold = (*fLines)[*endLine + 1]->offset;
3662 float width = max_c(fTextRect.Width(), 10);
3663 // TODO: The minimum width of 10 is a work around for the following
3664 // problem: If the text rect is too small, we are not calculating any
3665 // line heights, not even for the first line. Maybe this is a bug
3666 // in the algorithm, but other places in the class rely on at least
3667 // the first line to return a valid height. Maybe "10" should really
3668 // be the width of the very first glyph instead.
3669 STELine* curLine = (*fLines)[lineIndex];
3670 STELine* nextLine = curLine + 1;
3672 do {
3673 float ascent, descent;
3674 int32 fromOffset = curLine->offset;
3675 int32 toOffset = _FindLineBreak(fromOffset, &ascent, &descent, &width);
3677 curLine->ascent = ascent;
3678 curLine->width = width;
3680 // we want to advance at least by one character
3681 int32 nextOffset = _NextInitialByte(fromOffset);
3682 if (toOffset < nextOffset && fromOffset < textLength)
3683 toOffset = nextOffset;
3685 lineIndex++;
3686 STELine saveLine = *nextLine;
3687 if (lineIndex > fLines->NumLines() || toOffset < nextLine->offset) {
3688 // the new line comes before the old line start, add a line
3689 STELine newLine;
3690 newLine.offset = toOffset;
3691 newLine.origin = ceilf(curLine->origin + ascent + descent) + 1;
3692 newLine.ascent = 0;
3693 fLines->InsertLine(&newLine, lineIndex);
3694 } else {
3695 // update the existing line
3696 nextLine->offset = toOffset;
3697 nextLine->origin = ceilf(curLine->origin + ascent + descent) + 1;
3699 // remove any lines that start before the current line
3700 while (lineIndex < fLines->NumLines()
3701 && toOffset >= ((*fLines)[lineIndex] + 1)->offset) {
3702 fLines->RemoveLines(lineIndex + 1);
3705 nextLine = (*fLines)[lineIndex];
3706 if (nextLine->offset == saveLine.offset) {
3707 if (nextLine->offset >= recalThreshold) {
3708 if (nextLine->origin != saveLine.origin)
3709 fLines->BumpOrigin(nextLine->origin - saveLine.origin,
3710 lineIndex + 1);
3711 break;
3713 } else {
3714 if (lineIndex > 0 && lineIndex == *startLine)
3715 *startLine = lineIndex - 1;
3719 curLine = (*fLines)[lineIndex];
3720 nextLine = curLine + 1;
3721 } while (curLine->offset < textLength);
3723 // make sure that the sentinel line (which starts at the end of the buffer)
3724 // has always a width of 0
3725 (*fLines)[fLines->NumLines()]->width = 0;
3727 // update the text rect
3728 float newHeight = TextHeight(0, fLines->NumLines() - 1);
3729 fTextRect.bottom = fTextRect.top + newHeight;
3730 if (!fWrap) {
3731 fMinTextRectWidth = fLines->MaxWidth();
3732 fTextRect.right = ceilf(fTextRect.left + fMinTextRectWidth);
3735 *endLine = lineIndex - 1;
3736 *startLine = min_c(*startLine, *endLine);
3740 int32
3741 BTextView::_FindLineBreak(int32 fromOffset, float* _ascent, float* _descent,
3742 float* inOutWidth)
3744 *_ascent = 0.0;
3745 *_descent = 0.0;
3747 const int32 limit = fText->Length();
3749 // is fromOffset at the end?
3750 if (fromOffset >= limit) {
3751 // try to return valid height info anyway
3752 if (fStyles->NumRuns() > 0) {
3753 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent,
3754 _descent);
3755 } else {
3756 if (fStyles->IsValidNullStyle()) {
3757 const BFont* font = NULL;
3758 fStyles->GetNullStyle(&font, NULL);
3760 font_height fh;
3761 font->GetHeight(&fh);
3762 *_ascent = fh.ascent;
3763 *_descent = fh.descent + fh.leading;
3766 *inOutWidth = 0;
3768 return limit;
3771 int32 offset = fromOffset;
3773 if (!fWrap) {
3774 // Text wrapping is turned off.
3775 // Just find the offset of the first \n character
3776 offset = limit - fromOffset;
3777 fText->FindChar(B_ENTER, fromOffset, &offset);
3778 offset += fromOffset;
3779 int32 toOffset = (offset < limit) ? offset : limit;
3781 *inOutWidth = _TabExpandedStyledWidth(fromOffset, toOffset - fromOffset,
3782 _ascent, _descent);
3784 return offset < limit ? offset + 1 : limit;
3787 bool done = false;
3788 float ascent = 0.0;
3789 float descent = 0.0;
3790 int32 delta = 0;
3791 float deltaWidth = 0.0;
3792 float strWidth = 0.0;
3793 uchar theChar;
3795 // wrap the text
3796 while (offset < limit && !done) {
3797 // find the next line break candidate
3798 for (; (offset + delta) < limit; delta++) {
3799 if (CanEndLine(offset + delta)) {
3800 theChar = fText->RealCharAt(offset + delta);
3801 if (theChar != B_SPACE && theChar != B_TAB
3802 && theChar != B_ENTER) {
3803 // we are scanning for trailing whitespace below, so we
3804 // have to skip non-whitespace characters, that can end
3805 // the line, here
3806 delta++;
3808 break;
3812 int32 deltaBeforeWhitespace = delta;
3813 // now skip over trailing whitespace, if any
3814 for (; (offset + delta) < limit; delta++) {
3815 theChar = fText->RealCharAt(offset + delta);
3816 if (theChar == B_ENTER) {
3817 // found a newline, we're done!
3818 done = true;
3819 delta++;
3820 break;
3821 } else if (theChar != B_SPACE && theChar != B_TAB) {
3822 // stop at anything else than trailing whitespace
3823 break;
3827 delta = max_c(delta, 1);
3829 // do not include B_ENTER-terminator into width & height calculations
3830 deltaWidth = _TabExpandedStyledWidth(offset,
3831 done ? delta - 1 : delta, &ascent, &descent);
3832 strWidth += deltaWidth;
3834 if (strWidth >= *inOutWidth) {
3835 // we've found where the line will wrap
3836 done = true;
3838 // we have included trailing whitespace in the width computation
3839 // above, but that is not being shown anyway, so we try again
3840 // without the trailing whitespace
3841 if (delta == deltaBeforeWhitespace) {
3842 // there is no trailing whitespace, no point in trying
3843 break;
3846 // reset string width to start of current run ...
3847 strWidth -= deltaWidth;
3849 // ... and compute the resulting width (of visible characters)
3850 strWidth += _StyledWidth(offset, deltaBeforeWhitespace, NULL, NULL);
3851 if (strWidth >= *inOutWidth) {
3852 // width of visible characters exceeds line, we need to wrap
3853 // before the current "word"
3854 break;
3858 *_ascent = max_c(ascent, *_ascent);
3859 *_descent = max_c(descent, *_descent);
3861 offset += delta;
3862 delta = 0;
3865 if (offset - fromOffset < 1) {
3866 // there weren't any words that fit entirely in this line
3867 // force a break in the middle of a word
3868 *_ascent = 0.0;
3869 *_descent = 0.0;
3870 strWidth = 0.0;
3872 int32 current = fromOffset;
3873 for (offset = _NextInitialByte(current); current < limit;
3874 current = offset, offset = _NextInitialByte(offset)) {
3875 strWidth += _StyledWidth(current, offset - current, &ascent,
3876 &descent);
3877 if (strWidth >= *inOutWidth) {
3878 offset = _PreviousInitialByte(offset);
3879 break;
3882 *_ascent = max_c(ascent, *_ascent);
3883 *_descent = max_c(descent, *_descent);
3887 return min_c(offset, limit);
3891 int32
3892 BTextView::_PreviousLineStart(int32 offset)
3894 if (offset <= 0)
3895 return 0;
3897 while (offset > 0) {
3898 offset = _PreviousInitialByte(offset);
3899 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE
3900 && ByteAt(offset) == B_ENTER) {
3901 return offset + 1;
3905 return offset;
3909 int32
3910 BTextView::_NextLineEnd(int32 offset)
3912 int32 textLen = fText->Length();
3913 if (offset >= textLen)
3914 return textLen;
3916 while (offset < textLen) {
3917 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE
3918 && ByteAt(offset) == B_ENTER) {
3919 break;
3921 offset = _NextInitialByte(offset);
3924 return offset;
3928 int32
3929 BTextView::_PreviousWordBoundary(int32 offset)
3931 uint32 charType = _CharClassification(offset);
3932 int32 previous;
3933 while (offset > 0) {
3934 previous = _PreviousInitialByte(offset);
3935 if (_CharClassification(previous) != charType)
3936 break;
3937 offset = previous;
3940 return offset;
3944 int32
3945 BTextView::_NextWordBoundary(int32 offset)
3947 int32 textLen = fText->Length();
3948 uint32 charType = _CharClassification(offset);
3949 while (offset < textLen) {
3950 offset = _NextInitialByte(offset);
3951 if (_CharClassification(offset) != charType)
3952 break;
3955 return offset;
3959 int32
3960 BTextView::_PreviousWordStart(int32 offset)
3962 if (offset <= 1)
3963 return 0;
3965 --offset;
3966 // need to look at previous char
3967 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) {
3968 // skip non-word characters
3969 while (offset > 0) {
3970 offset = _PreviousInitialByte(offset);
3971 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT)
3972 break;
3975 while (offset > 0) {
3976 // skip to start of word
3977 int32 previous = _PreviousInitialByte(offset);
3978 if (_CharClassification(previous) != CHAR_CLASS_DEFAULT)
3979 break;
3980 offset = previous;
3983 return offset;
3987 int32
3988 BTextView::_NextWordEnd(int32 offset)
3990 int32 textLen = fText->Length();
3991 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) {
3992 // skip non-word characters
3993 while (offset < textLen) {
3994 offset = _NextInitialByte(offset);
3995 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT)
3996 break;
3999 while (offset < textLen) {
4000 // skip to end of word
4001 offset = _NextInitialByte(offset);
4002 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT)
4003 break;
4006 return offset;
4010 /*! Returns the width used by the characters starting at the given
4011 offset with the given length, expanding all tab characters as needed.
4013 float
4014 BTextView::_TabExpandedStyledWidth(int32 offset, int32 length, float* _ascent,
4015 float* _descent) const
4017 float ascent = 0.0;
4018 float descent = 0.0;
4019 float maxAscent = 0.0;
4020 float maxDescent = 0.0;
4022 float width = 0.0;
4023 int32 numBytes = length;
4024 bool foundTab = false;
4025 do {
4026 foundTab = fText->FindChar(B_TAB, offset, &numBytes);
4027 width += _StyledWidth(offset, numBytes, &ascent, &descent);
4029 if (maxAscent < ascent)
4030 maxAscent = ascent;
4031 if (maxDescent < descent)
4032 maxDescent = descent;
4034 if (foundTab) {
4035 width += _ActualTabWidth(width);
4036 numBytes++;
4039 offset += numBytes;
4040 length -= numBytes;
4041 numBytes = length;
4042 } while (foundTab && length > 0);
4044 if (_ascent != NULL)
4045 *_ascent = maxAscent;
4046 if (_descent != NULL)
4047 *_descent = maxDescent;
4049 return width;
4053 /*! Calculate the width of the text within the given limits.
4055 \param fromOffset The offset where to start.
4056 \param length The length of the text to examine.
4057 \param _ascent A pointer to a float which will contain the maximum ascent.
4058 \param _descent A pointer to a float which will contain the maximum descent.
4060 \return The width for the text within the given limits.
4062 float
4063 BTextView::_StyledWidth(int32 fromOffset, int32 length, float* _ascent,
4064 float* _descent) const
4066 if (length == 0) {
4067 // determine height of char at given offset, but return empty width
4068 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent,
4069 _descent);
4070 return 0.0;
4073 float result = 0.0;
4074 float ascent = 0.0;
4075 float descent = 0.0;
4076 float maxAscent = 0.0;
4077 float maxDescent = 0.0;
4079 // iterate through the style runs
4080 const BFont* font = NULL;
4081 int32 numBytes;
4082 while ((numBytes = fStyles->Iterate(fromOffset, length, fInline, &font,
4083 NULL, &ascent, &descent)) != 0) {
4084 maxAscent = max_c(ascent, maxAscent);
4085 maxDescent = max_c(descent, maxDescent);
4087 #if USE_WIDTHBUFFER
4088 // Use _BWidthBuffer_ if possible
4089 if (BPrivate::gWidthBuffer != NULL) {
4090 result += BPrivate::gWidthBuffer->StringWidth(*fText, fromOffset,
4091 numBytes, font);
4092 } else {
4093 #endif
4094 const char* text = fText->GetString(fromOffset, &numBytes);
4095 result += font->StringWidth(text, numBytes);
4097 #if USE_WIDTHBUFFER
4099 #endif
4101 fromOffset += numBytes;
4102 length -= numBytes;
4105 if (_ascent != NULL)
4106 *_ascent = maxAscent;
4107 if (_descent != NULL)
4108 *_descent = maxDescent;
4110 return result;
4114 //! Calculate the actual tab width for the given location.
4115 float
4116 BTextView::_ActualTabWidth(float location) const
4118 float tabWidth = fTabWidth - fmod(location, fTabWidth);
4119 if (round(tabWidth) == 0)
4120 tabWidth = fTabWidth;
4122 return tabWidth;
4126 void
4127 BTextView::_DoInsertText(const char* text, int32 length, int32 offset,
4128 const text_run_array* runs)
4130 _CancelInputMethod();
4132 if (TextLength() + length > MaxBytes())
4133 return;
4135 if (fSelStart != fSelEnd)
4136 Select(fSelStart, fSelStart);
4138 const int32 textLength = TextLength();
4139 if (offset > textLength)
4140 offset = textLength;
4142 // copy data into buffer
4143 InsertText(text, length, offset, runs);
4145 // recalc line breaks and draw the text
4146 _Refresh(offset, offset + length, false);
4150 void
4151 BTextView::_DoDeleteText(int32 fromOffset, int32 toOffset)
4153 CALLED();
4157 void
4158 BTextView::_DrawLine(BView* view, const int32 &lineNum,
4159 const int32 &startOffset, const bool &erase, BRect &eraseRect,
4160 BRegion &inputRegion)
4162 STELine* line = (*fLines)[lineNum];
4163 float startLeft = fTextRect.left;
4164 if (startOffset != -1) {
4165 if (ByteAt(startOffset) == B_ENTER) {
4166 // StartOffset is a newline
4167 startLeft = PointAt(line->offset).x;
4168 } else
4169 startLeft = PointAt(startOffset).x;
4171 else if (fAlignment != B_ALIGN_LEFT) {
4172 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum);
4173 if (fAlignment == B_ALIGN_CENTER)
4174 alignmentOffset /= 2;
4175 startLeft = fTextRect.left + alignmentOffset;
4178 int32 length = (line + 1)->offset;
4179 if (startOffset != -1)
4180 length -= startOffset;
4181 else
4182 length -= line->offset;
4184 // DrawString() chokes if you draw a newline
4185 if (ByteAt((line + 1)->offset - 1) == B_ENTER)
4186 length--;
4188 view->MovePenTo(startLeft, line->origin + line->ascent + fTextRect.top + 1);
4190 if (erase) {
4191 eraseRect.top = line->origin + fTextRect.top;
4192 eraseRect.bottom = (line + 1)->origin + fTextRect.top;
4193 view->FillRect(eraseRect, B_SOLID_LOW);
4196 // do we have any text to draw?
4197 if (length <= 0)
4198 return;
4200 bool foundTab = false;
4201 int32 tabChars = 0;
4202 int32 numTabs = 0;
4203 int32 offset = startOffset != -1 ? startOffset : line->offset;
4204 const BFont* font = NULL;
4205 const rgb_color* color = NULL;
4206 int32 numBytes;
4207 drawing_mode defaultTextRenderingMode = DrawingMode();
4208 // iterate through each style on this line
4209 while ((numBytes = fStyles->Iterate(offset, length, fInline, &font,
4210 &color)) != 0) {
4211 view->SetFont(font);
4212 view->SetHighColor(*color);
4214 tabChars = min_c(numBytes, length);
4215 do {
4216 foundTab = fText->FindChar(B_TAB, offset, &tabChars);
4217 if (foundTab) {
4218 do {
4219 numTabs++;
4220 if (ByteAt(offset + tabChars + numTabs) != B_TAB)
4221 break;
4222 } while ((tabChars + numTabs) < numBytes);
4225 drawing_mode textRenderingMode = defaultTextRenderingMode;
4227 if (inputRegion.CountRects() > 0
4228 && ((offset <= fInline->Offset()
4229 && fInline->Offset() < offset + tabChars)
4230 || (fInline->Offset() <= offset
4231 && offset < fInline->Offset() + fInline->Length()))) {
4233 textRenderingMode = B_OP_OVER;
4235 BRegion textRegion;
4236 GetTextRegion(offset, offset + length, &textRegion);
4238 textRegion.IntersectWith(&inputRegion);
4239 view->PushState();
4241 // Highlight in blue the inputted text
4242 view->SetHighColor(kBlueInputColor);
4243 view->FillRect(textRegion.Frame());
4245 // Highlight in red the selected part
4246 if (fInline->SelectionLength() > 0) {
4247 BRegion selectedRegion;
4248 GetTextRegion(fInline->Offset()
4249 + fInline->SelectionOffset(), fInline->Offset()
4250 + fInline->SelectionOffset()
4251 + fInline->SelectionLength(), &selectedRegion);
4253 textRegion.IntersectWith(&selectedRegion);
4255 view->SetHighColor(kRedInputColor);
4256 view->FillRect(textRegion.Frame());
4259 view->PopState();
4262 int32 returnedBytes = tabChars;
4263 const char* stringToDraw = fText->GetString(offset, &returnedBytes);
4264 view->SetDrawingMode(textRenderingMode);
4265 view->DrawString(stringToDraw, returnedBytes);
4266 if (foundTab) {
4267 float penPos = PenLocation().x - fTextRect.left;
4268 float tabWidth = _ActualTabWidth(penPos);
4269 if (numTabs > 1)
4270 tabWidth += ((numTabs - 1) * fTabWidth);
4272 view->MovePenBy(tabWidth, 0.0);
4273 tabChars += numTabs;
4276 offset += tabChars;
4277 length -= tabChars;
4278 numBytes -= tabChars;
4279 tabChars = min_c(numBytes, length);
4280 numTabs = 0;
4281 } while (foundTab && tabChars > 0);
4286 void
4287 BTextView::_DrawLines(int32 startLine, int32 endLine, int32 startOffset,
4288 bool erase)
4290 if (!Window())
4291 return;
4293 // clip the text
4294 BRect textRect(fTextRect);
4295 float minWidth
4296 = Bounds().Width() - fLayoutData->leftInset - fLayoutData->rightInset;
4297 if (textRect.Width() < minWidth)
4298 textRect.right = textRect.left + minWidth;
4299 BRect clipRect = Bounds() & textRect;
4300 clipRect.InsetBy(-1, -1);
4302 BRegion newClip;
4303 newClip.Set(clipRect);
4304 ConstrainClippingRegion(&newClip);
4306 // set the low color to the view color so that
4307 // drawing to a non-white background will work
4308 SetLowColor(ViewColor());
4310 BView* view = NULL;
4311 if (fOffscreen == NULL)
4312 view = this;
4313 else {
4314 fOffscreen->Lock();
4315 view = fOffscreen->ChildAt(0);
4316 view->SetLowColor(ViewColor());
4317 view->FillRect(view->Bounds(), B_SOLID_LOW);
4320 long maxLine = fLines->NumLines() - 1;
4321 if (startLine < 0)
4322 startLine = 0;
4323 if (endLine > maxLine)
4324 endLine = maxLine;
4326 // TODO: See if we can avoid this
4327 if (fAlignment != B_ALIGN_LEFT)
4328 erase = true;
4330 BRect eraseRect = clipRect;
4331 int32 startEraseLine = startLine;
4332 STELine* line = (*fLines)[startLine];
4334 if (erase && startOffset != -1 && fAlignment == B_ALIGN_LEFT) {
4335 // erase only to the right of startOffset
4336 startEraseLine++;
4337 int32 startErase = startOffset;
4339 BPoint erasePoint = PointAt(startErase);
4340 eraseRect.left = erasePoint.x;
4341 eraseRect.top = erasePoint.y;
4342 eraseRect.bottom = (line + 1)->origin + fTextRect.top;
4344 view->FillRect(eraseRect, B_SOLID_LOW);
4346 eraseRect = clipRect;
4349 BRegion inputRegion;
4350 if (fInline != NULL && fInline->IsActive()) {
4351 GetTextRegion(fInline->Offset(), fInline->Offset() + fInline->Length(),
4352 &inputRegion);
4355 //BPoint leftTop(startLeft, line->origin);
4356 for (int32 lineNum = startLine; lineNum <= endLine; lineNum++) {
4357 const bool eraseThisLine = erase && lineNum >= startEraseLine;
4358 _DrawLine(view, lineNum, startOffset, eraseThisLine, eraseRect,
4359 inputRegion);
4360 startOffset = -1;
4361 // Set this to -1 so the next iteration will use the line offset
4364 // draw the caret/hilite the selection
4365 if (fActive) {
4366 if (fSelStart != fSelEnd) {
4367 if (fSelectable)
4368 Highlight(fSelStart, fSelEnd);
4369 } else {
4370 if (fCaretVisible)
4371 _DrawCaret(fSelStart, true);
4375 if (fOffscreen != NULL) {
4376 view->Sync();
4377 /*BPoint penLocation = view->PenLocation();
4378 BRect drawRect(leftTop.x, leftTop.y, penLocation.x, penLocation.y);
4379 DrawBitmap(fOffscreen, drawRect, drawRect);*/
4380 fOffscreen->Unlock();
4383 ConstrainClippingRegion(NULL);
4387 void
4388 BTextView::_RequestDrawLines(int32 startLine, int32 endLine)
4390 if (!Window())
4391 return;
4393 long maxLine = fLines->NumLines() - 1;
4395 STELine* from = (*fLines)[startLine];
4396 STELine* to = endLine == maxLine ? NULL : (*fLines)[endLine + 1];
4397 BRect invalidRect(Bounds().left, from->origin + fTextRect.top,
4398 Bounds().right,
4399 to != NULL ? to->origin + fTextRect.top : fTextRect.bottom);
4400 Invalidate(invalidRect);
4401 Window()->UpdateIfNeeded();
4405 void
4406 BTextView::_DrawCaret(int32 offset, bool visible)
4408 float lineHeight;
4409 BPoint caretPoint = PointAt(offset, &lineHeight);
4410 caretPoint.x = min_c(caretPoint.x, fTextRect.right);
4412 BRect caretRect;
4413 caretRect.left = caretRect.right = caretPoint.x;
4414 caretRect.top = caretPoint.y;
4415 caretRect.bottom = caretPoint.y + lineHeight - 1;
4417 if (visible)
4418 InvertRect(caretRect);
4419 else
4420 Invalidate(caretRect);
4424 inline void
4425 BTextView::_ShowCaret()
4427 if (fActive && !fCaretVisible && fEditable && fSelStart == fSelEnd)
4428 _InvertCaret();
4432 inline void
4433 BTextView::_HideCaret()
4435 if (fCaretVisible && fSelStart == fSelEnd)
4436 _InvertCaret();
4440 //! Hides the caret if it is being shown, and if it's hidden, shows it.
4441 void
4442 BTextView::_InvertCaret()
4444 fCaretVisible = !fCaretVisible;
4445 _DrawCaret(fSelStart, fCaretVisible);
4446 fCaretTime = system_time();
4450 /*! Place the dragging caret at the given offset.
4452 \param offset The offset (zero based within the object's text) where to
4453 place the dragging caret. If it's -1, hide the caret.
4455 void
4456 BTextView::_DragCaret(int32 offset)
4458 // does the caret need to move?
4459 if (offset == fDragOffset)
4460 return;
4462 // hide the previous drag caret
4463 if (fDragOffset != -1)
4464 _DrawCaret(fDragOffset, false);
4466 // do we have a new location?
4467 if (offset != -1) {
4468 if (fActive) {
4469 // ignore if offset is within active selection
4470 if (offset >= fSelStart && offset <= fSelEnd) {
4471 fDragOffset = -1;
4472 return;
4476 _DrawCaret(offset, true);
4479 fDragOffset = offset;
4483 void
4484 BTextView::_StopMouseTracking()
4486 delete fTrackingMouse;
4487 fTrackingMouse = NULL;
4491 bool
4492 BTextView::_PerformMouseUp(BPoint where)
4494 if (fTrackingMouse == NULL)
4495 return false;
4497 if (fTrackingMouse->selectionRect.IsValid())
4498 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset);
4500 _StopMouseTracking();
4501 // adjust cursor if necessary
4502 _TrackMouse(where, NULL, true);
4504 return true;
4508 bool
4509 BTextView::_PerformMouseMoved(BPoint where, uint32 code)
4511 fWhere = where;
4513 if (fTrackingMouse == NULL)
4514 return false;
4516 int32 currentOffset = OffsetAt(where);
4517 if (fTrackingMouse->selectionRect.IsValid()) {
4518 // we are tracking the mouse for drag action, if the mouse has moved
4519 // to another index or more than three pixels from where it was clicked,
4520 // we initiate a drag now:
4521 if (currentOffset != fTrackingMouse->clickOffset
4522 || fabs(fTrackingMouse->where.x - where.x) > 3
4523 || fabs(fTrackingMouse->where.y - where.y) > 3) {
4524 _StopMouseTracking();
4525 _InitiateDrag();
4526 return true;
4528 return false;
4531 switch (fClickCount) {
4532 case 3:
4533 // triple click, extend selection linewise
4534 if (currentOffset <= fTrackingMouse->anchor) {
4535 fTrackingMouse->selStart
4536 = (*fLines)[_LineAt(currentOffset)]->offset;
4537 fTrackingMouse->selEnd = fTrackingMouse->shiftDown
4538 ? fSelEnd
4539 : (*fLines)[_LineAt(fTrackingMouse->anchor) + 1]->offset;
4540 } else {
4541 fTrackingMouse->selStart
4542 = fTrackingMouse->shiftDown
4543 ? fSelStart
4544 : (*fLines)[_LineAt(fTrackingMouse->anchor)]->offset;
4545 fTrackingMouse->selEnd
4546 = (*fLines)[_LineAt(currentOffset) + 1]->offset;
4548 break;
4550 case 2:
4551 // double click, extend selection wordwise
4552 if (currentOffset <= fTrackingMouse->anchor) {
4553 fTrackingMouse->selStart = _PreviousWordBoundary(currentOffset);
4554 fTrackingMouse->selEnd
4555 = fTrackingMouse->shiftDown
4556 ? fSelEnd
4557 : _NextWordBoundary(fTrackingMouse->anchor);
4558 } else {
4559 fTrackingMouse->selStart
4560 = fTrackingMouse->shiftDown
4561 ? fSelStart
4562 : _PreviousWordBoundary(fTrackingMouse->anchor);
4563 fTrackingMouse->selEnd = _NextWordBoundary(currentOffset);
4565 break;
4567 default:
4568 // new click, extend selection char by char
4569 if (currentOffset <= fTrackingMouse->anchor) {
4570 fTrackingMouse->selStart = currentOffset;
4571 fTrackingMouse->selEnd
4572 = fTrackingMouse->shiftDown
4573 ? fSelEnd : fTrackingMouse->anchor;
4574 } else {
4575 fTrackingMouse->selStart
4576 = fTrackingMouse->shiftDown
4577 ? fSelStart : fTrackingMouse->anchor;
4578 fTrackingMouse->selEnd = currentOffset;
4580 break;
4583 // position caret to follow the direction of the selection
4584 if (fTrackingMouse->selEnd != fSelEnd)
4585 fCaretOffset = fTrackingMouse->selEnd;
4586 else if (fTrackingMouse->selStart != fSelStart)
4587 fCaretOffset = fTrackingMouse->selStart;
4589 Select(fTrackingMouse->selStart, fTrackingMouse->selEnd);
4590 _TrackMouse(where, NULL);
4592 return true;
4596 /*! Tracks the mouse position, doing special actions like changing the
4597 view cursor.
4599 \param where The point where the mouse has moved.
4600 \param message The dragging message, if there is any.
4601 \param force Passed as second parameter of SetViewCursor()
4603 void
4604 BTextView::_TrackMouse(BPoint where, const BMessage* message, bool force)
4606 BRegion textRegion;
4607 GetTextRegion(fSelStart, fSelEnd, &textRegion);
4609 if (message && AcceptsDrop(message))
4610 _TrackDrag(where);
4611 else if ((fSelectable || fEditable)
4612 && (fTrackingMouse != NULL || !textRegion.Contains(where))) {
4613 SetViewCursor(B_CURSOR_I_BEAM, force);
4614 } else
4615 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT, force);
4619 //! Tracks the mouse position when the user is dragging some data.
4620 void
4621 BTextView::_TrackDrag(BPoint where)
4623 CALLED();
4624 if (Bounds().Contains(where))
4625 _DragCaret(OffsetAt(where));
4629 //! Initiates a drag operation.
4630 void
4631 BTextView::_InitiateDrag()
4633 BMessage dragMessage(B_MIME_DATA);
4634 BBitmap* dragBitmap = NULL;
4635 BPoint bitmapPoint;
4636 BHandler* dragHandler = NULL;
4638 GetDragParameters(&dragMessage, &dragBitmap, &bitmapPoint, &dragHandler);
4639 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
4641 if (dragBitmap != NULL)
4642 DragMessage(&dragMessage, dragBitmap, bitmapPoint, dragHandler);
4643 else {
4644 BRegion region;
4645 GetTextRegion(fSelStart, fSelEnd, &region);
4646 BRect bounds = Bounds();
4647 BRect dragRect = region.Frame();
4648 if (!bounds.Contains(dragRect))
4649 dragRect = bounds & dragRect;
4651 DragMessage(&dragMessage, dragRect, dragHandler);
4654 BMessenger messenger(this);
4655 BMessage message(_DISPOSE_DRAG_);
4656 fDragRunner = new (nothrow) BMessageRunner(messenger, &message, 100000);
4660 //! Handles when some data is dropped on the view.
4661 bool
4662 BTextView::_MessageDropped(BMessage* message, BPoint where, BPoint offset)
4664 ASSERT(message);
4666 void* from = NULL;
4667 bool internalDrop = false;
4668 if (message->FindPointer("be:originator", &from) == B_OK
4669 && from == this && fSelEnd != fSelStart)
4670 internalDrop = true;
4672 _DragCaret(-1);
4674 delete fDragRunner;
4675 fDragRunner = NULL;
4677 _TrackMouse(where, NULL);
4679 // are we sure we like this message?
4680 if (!AcceptsDrop(message))
4681 return false;
4683 int32 dropOffset = OffsetAt(where);
4684 if (dropOffset > TextLength())
4685 dropOffset = TextLength();
4687 // if this view initiated the drag, move instead of copy
4688 if (internalDrop) {
4689 // dropping onto itself?
4690 if (dropOffset >= fSelStart && dropOffset <= fSelEnd)
4691 return true;
4694 ssize_t dataLength = 0;
4695 const char* text = NULL;
4696 entry_ref ref;
4697 if (message->FindData("text/plain", B_MIME_TYPE, (const void**)&text,
4698 &dataLength) == B_OK) {
4699 text_run_array* runArray = NULL;
4700 ssize_t runLength = 0;
4701 if (fStylable) {
4702 message->FindData("application/x-vnd.Be-text_run_array",
4703 B_MIME_TYPE, (const void**)&runArray, &runLength);
4706 _FilterDisallowedChars((char*)text, dataLength, runArray);
4708 if (dataLength < 1) {
4709 beep();
4710 return true;
4713 if (fUndo) {
4714 delete fUndo;
4715 fUndo = new DropUndoBuffer(this, text, dataLength, runArray,
4716 runLength, dropOffset, internalDrop);
4719 if (internalDrop) {
4720 if (dropOffset > fSelEnd)
4721 dropOffset -= dataLength;
4722 Delete();
4725 Insert(dropOffset, text, dataLength, runArray);
4728 return true;
4732 void
4733 BTextView::_PerformAutoScrolling()
4735 // Scroll the view a bit if mouse is outside the view bounds
4736 BRect bounds = Bounds();
4737 BPoint scrollBy(B_ORIGIN);
4739 // R5 does a pretty soft auto-scroll, we try to do the same by
4740 // simply scrolling the distance between cursor and border
4741 if (fWhere.x > bounds.right) {
4742 scrollBy.x = fWhere.x - bounds.right;
4743 } else if (fWhere.x < bounds.left) {
4744 scrollBy.x = fWhere.x - bounds.left; // negative value
4747 // prevent from scrolling out of view
4748 if (scrollBy.x != 0.0) {
4749 float rightMax = floorf(fTextRect.right + fLayoutData->rightInset);
4750 if (bounds.right + scrollBy.x > rightMax)
4751 scrollBy.x = rightMax - bounds.right;
4752 if (bounds.left + scrollBy.x < 0)
4753 scrollBy.x = -bounds.left;
4756 if (CountLines() > 1) {
4757 // scroll in Y only if multiple lines!
4758 if (fWhere.y > bounds.bottom) {
4759 scrollBy.y = fWhere.y - bounds.bottom;
4760 } else if (fWhere.y < bounds.top) {
4761 scrollBy.y = fWhere.y - bounds.top; // negative value
4764 // prevent from scrolling out of view
4765 if (scrollBy.y != 0.0) {
4766 float bottomMax = floorf(fTextRect.bottom
4767 + fLayoutData->bottomInset);
4768 if (bounds.bottom + scrollBy.y > bottomMax)
4769 scrollBy.y = bottomMax - bounds.bottom;
4770 if (bounds.top + scrollBy.y < 0)
4771 scrollBy.y = -bounds.top;
4775 if (scrollBy != B_ORIGIN)
4776 ScrollBy(scrollBy.x, scrollBy.y);
4780 //! Updates the scrollbars associated with the object (if any).
4781 void
4782 BTextView::_UpdateScrollbars()
4784 BRect bounds(Bounds());
4785 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL);
4786 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL);
4788 // do we have a horizontal scroll bar?
4789 if (horizontalScrollBar != NULL) {
4790 long viewWidth = bounds.IntegerWidth();
4791 long dataWidth = (long)ceilf(fTextRect.IntegerWidth()
4792 + fLayoutData->leftInset + fLayoutData->rightInset);
4794 long maxRange = dataWidth - viewWidth;
4795 maxRange = max_c(maxRange, 0);
4797 horizontalScrollBar->SetRange(0, (float)maxRange);
4798 horizontalScrollBar->SetProportion((float)viewWidth / (float)dataWidth);
4799 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10);
4802 // how about a vertical scroll bar?
4803 if (verticalScrollBar != NULL) {
4804 long viewHeight = bounds.IntegerHeight();
4805 long dataHeight = (long)ceilf(fTextRect.IntegerHeight()
4806 + fLayoutData->topInset + fLayoutData->bottomInset);
4808 long maxRange = dataHeight - viewHeight;
4809 maxRange = max_c(maxRange, 0);
4811 verticalScrollBar->SetRange(0, maxRange);
4812 verticalScrollBar->SetProportion((float)viewHeight / (float)dataHeight);
4813 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight);
4818 //! Scrolls by the given offsets
4819 void
4820 BTextView::_ScrollBy(float horizontal, float vertical)
4822 BRect bounds = Bounds();
4823 _ScrollTo(bounds.left + horizontal, bounds.top + vertical);
4827 //! Scrolls to the given position, making sure not to scroll out of bounds.
4828 void
4829 BTextView::_ScrollTo(float x, float y)
4831 BRect bounds = Bounds();
4832 long viewWidth = bounds.IntegerWidth();
4833 long viewHeight = bounds.IntegerHeight();
4835 if (x > fTextRect.right - viewWidth)
4836 x = fTextRect.right - viewWidth;
4837 if (x < 0.0)
4838 x = 0.0;
4840 if (y > fTextRect.bottom + fLayoutData->bottomInset - viewHeight)
4841 y = fTextRect.bottom + fLayoutData->bottomInset - viewHeight;
4842 if (y < 0.0)
4843 y = 0.0;
4845 ScrollTo(x, y);
4849 //! Autoresizes the view to fit the contained text.
4850 void
4851 BTextView::_AutoResize(bool redraw)
4853 if (!fResizable)
4854 return;
4856 BRect bounds = Bounds();
4857 float oldWidth = bounds.Width();
4858 float newWidth = ceilf(fLayoutData->leftInset + fTextRect.Width()
4859 + fLayoutData->rightInset);
4861 if (fContainerView != NULL) {
4862 // NOTE: This container view thing is only used by Tracker.
4863 // move container view if not left aligned
4864 if (fAlignment == B_ALIGN_CENTER) {
4865 if (fmod(ceilf(newWidth - oldWidth), 2.0) != 0.0)
4866 newWidth += 1;
4867 fContainerView->MoveBy(ceilf(oldWidth - newWidth) / 2, 0);
4868 } else if (fAlignment == B_ALIGN_RIGHT) {
4869 fContainerView->MoveBy(ceilf(oldWidth - newWidth), 0);
4871 // resize container view
4872 fContainerView->ResizeBy(ceilf(newWidth - oldWidth), 0);
4876 if (redraw)
4877 _RequestDrawLines(0, 0);
4879 // erase any potential left over outside the text rect
4880 // (can only be on right hand side)
4881 BRect dirty(fTextRect.right + 1, fTextRect.top, bounds.right,
4882 fTextRect.bottom);
4883 if (dirty.IsValid()) {
4884 SetLowColor(ViewColor());
4885 FillRect(dirty, B_SOLID_LOW);
4890 //! Creates a new offscreen BBitmap with an associated BView.
4891 void
4892 BTextView::_NewOffscreen(float padding)
4894 if (fOffscreen != NULL)
4895 _DeleteOffscreen();
4897 #if USE_DOUBLEBUFFERING
4898 BRect bitmapRect(0, 0, fTextRect.Width() + padding, fTextRect.Height());
4899 fOffscreen = new BBitmap(bitmapRect, fColorSpace, true, false);
4900 if (fOffscreen != NULL && fOffscreen->Lock()) {
4901 BView* bufferView = new BView(bitmapRect, "drawing view", 0, 0);
4902 fOffscreen->AddChild(bufferView);
4903 fOffscreen->Unlock();
4905 #endif
4909 //! Deletes the textview's offscreen bitmap, if any.
4910 void
4911 BTextView::_DeleteOffscreen()
4913 if (fOffscreen != NULL && fOffscreen->Lock()) {
4914 delete fOffscreen;
4915 fOffscreen = NULL;
4920 /*! Creates a new offscreen bitmap, highlight the selection, and set the
4921 cursor to \c B_CURSOR_I_BEAM.
4923 void
4924 BTextView::_Activate()
4926 fActive = true;
4928 // Create a new offscreen BBitmap
4929 _NewOffscreen();
4931 if (fSelStart != fSelEnd) {
4932 if (fSelectable)
4933 Highlight(fSelStart, fSelEnd);
4934 } else
4935 _ShowCaret();
4937 BPoint where;
4938 uint32 buttons;
4939 GetMouse(&where, &buttons, false);
4940 if (Bounds().Contains(where))
4941 _TrackMouse(where, NULL);
4943 if (Window() != NULL) {
4944 BMessage* message;
4946 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY)
4947 && !Window()->HasShortcut(B_RIGHT_ARROW, B_COMMAND_KEY)) {
4948 message = new BMessage(kMsgNavigateArrow);
4949 message->AddInt32("key", B_LEFT_ARROW);
4950 message->AddInt32("modifiers", B_COMMAND_KEY);
4951 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY, message, this);
4953 message = new BMessage(kMsgNavigateArrow);
4954 message->AddInt32("key", B_RIGHT_ARROW);
4955 message->AddInt32("modifiers", B_COMMAND_KEY);
4956 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY, message, this);
4958 fInstalledNavigateCommandWordwiseShortcuts = true;
4960 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY)
4961 && !Window()->HasShortcut(B_RIGHT_ARROW,
4962 B_COMMAND_KEY | B_SHIFT_KEY)) {
4963 message = new BMessage(kMsgNavigateArrow);
4964 message->AddInt32("key", B_LEFT_ARROW);
4965 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
4966 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY,
4967 message, this);
4969 message = new BMessage(kMsgNavigateArrow);
4970 message->AddInt32("key", B_RIGHT_ARROW);
4971 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
4972 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY,
4973 message, this);
4975 fInstalledSelectCommandWordwiseShortcuts = true;
4978 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY)
4979 && !Window()->HasShortcut(B_RIGHT_ARROW, B_OPTION_KEY)) {
4980 message = new BMessage(kMsgNavigateArrow);
4981 message->AddInt32("key", B_LEFT_ARROW);
4982 message->AddInt32("modifiers", B_OPTION_KEY);
4983 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY, message, this);
4985 message = new BMessage(kMsgNavigateArrow);
4986 message->AddInt32("key", B_RIGHT_ARROW);
4987 message->AddInt32("modifiers", B_OPTION_KEY);
4988 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY, message, this);
4990 fInstalledNavigateOptionWordwiseShortcuts = true;
4992 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY)
4993 && !Window()->HasShortcut(B_RIGHT_ARROW,
4994 B_OPTION_KEY | B_SHIFT_KEY)) {
4995 message = new BMessage(kMsgNavigateArrow);
4996 message->AddInt32("key", B_LEFT_ARROW);
4997 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
4998 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
4999 message, this);
5001 message = new BMessage(kMsgNavigateArrow);
5002 message->AddInt32("key", B_RIGHT_ARROW);
5003 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5004 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5005 message, this);
5007 fInstalledSelectOptionWordwiseShortcuts = true;
5010 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY)
5011 && !Window()->HasShortcut(B_DOWN_ARROW, B_OPTION_KEY)) {
5012 message = new BMessage(kMsgNavigateArrow);
5013 message->AddInt32("key", B_UP_ARROW);
5014 message->AddInt32("modifiers", B_OPTION_KEY);
5015 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY, message, this);
5017 message = new BMessage(kMsgNavigateArrow);
5018 message->AddInt32("key", B_DOWN_ARROW);
5019 message->AddInt32("modifiers", B_OPTION_KEY);
5020 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY, message, this);
5022 fInstalledNavigateOptionLinewiseShortcuts = true;
5024 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY)
5025 && !Window()->HasShortcut(B_DOWN_ARROW,
5026 B_OPTION_KEY | B_SHIFT_KEY)) {
5027 message = new BMessage(kMsgNavigateArrow);
5028 message->AddInt32("key", B_UP_ARROW);
5029 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5030 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5031 message, this);
5033 message = new BMessage(kMsgNavigateArrow);
5034 message->AddInt32("key", B_DOWN_ARROW);
5035 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY);
5036 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY,
5037 message, this);
5039 fInstalledSelectOptionLinewiseShortcuts = true;
5042 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY)
5043 && !Window()->HasShortcut(B_END, B_COMMAND_KEY)) {
5044 message = new BMessage(kMsgNavigatePage);
5045 message->AddInt32("key", B_HOME);
5046 message->AddInt32("modifiers", B_COMMAND_KEY);
5047 Window()->AddShortcut(B_HOME, B_COMMAND_KEY, message, this);
5049 message = new BMessage(kMsgNavigatePage);
5050 message->AddInt32("key", B_END);
5051 message->AddInt32("modifiers", B_COMMAND_KEY);
5052 Window()->AddShortcut(B_END, B_COMMAND_KEY, message, this);
5054 fInstalledNavigateHomeEndDocwiseShortcuts = true;
5056 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY)
5057 && !Window()->HasShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY)) {
5058 message = new BMessage(kMsgNavigatePage);
5059 message->AddInt32("key", B_HOME);
5060 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
5061 Window()->AddShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY,
5062 message, this);
5064 message = new BMessage(kMsgNavigatePage);
5065 message->AddInt32("key", B_END);
5066 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY);
5067 Window()->AddShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY,
5068 message, this);
5070 fInstalledSelectHomeEndDocwiseShortcuts = true;
5076 //! Unhilights the selection, set the cursor to \c B_CURSOR_SYSTEM_DEFAULT.
5077 void
5078 BTextView::_Deactivate()
5080 fActive = false;
5082 _CancelInputMethod();
5083 _DeleteOffscreen();
5085 if (fSelStart != fSelEnd) {
5086 if (fSelectable)
5087 Highlight(fSelStart, fSelEnd);
5088 } else
5089 _HideCaret();
5091 if (Window() != NULL) {
5092 if (fInstalledNavigateCommandWordwiseShortcuts) {
5093 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY);
5094 Window()->RemoveShortcut(B_RIGHT_ARROW, B_COMMAND_KEY);
5095 fInstalledNavigateCommandWordwiseShortcuts = false;
5097 if (fInstalledSelectCommandWordwiseShortcuts) {
5098 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY);
5099 Window()->RemoveShortcut(B_RIGHT_ARROW,
5100 B_COMMAND_KEY | B_SHIFT_KEY);
5101 fInstalledSelectCommandWordwiseShortcuts = false;
5104 if (fInstalledNavigateOptionWordwiseShortcuts) {
5105 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY);
5106 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY);
5107 fInstalledNavigateOptionWordwiseShortcuts = false;
5109 if (fInstalledSelectOptionWordwiseShortcuts) {
5110 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5111 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5112 fInstalledSelectOptionWordwiseShortcuts = false;
5115 if (fInstalledNavigateOptionLinewiseShortcuts) {
5116 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY);
5117 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY);
5118 fInstalledNavigateOptionLinewiseShortcuts = false;
5120 if (fInstalledSelectOptionLinewiseShortcuts) {
5121 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5122 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY);
5123 fInstalledSelectOptionLinewiseShortcuts = false;
5126 if (fInstalledNavigateHomeEndDocwiseShortcuts) {
5127 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY);
5128 Window()->RemoveShortcut(B_END, B_COMMAND_KEY);
5129 fInstalledNavigateHomeEndDocwiseShortcuts = false;
5131 if (fInstalledSelectHomeEndDocwiseShortcuts) {
5132 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY);
5133 Window()->RemoveShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY);
5134 fInstalledSelectHomeEndDocwiseShortcuts = false;
5140 /*! Changes the passed in font to be displayable by the object.
5142 Set font rotation to 0, removes any font flag, set font spacing
5143 to \c B_BITMAP_SPACING and font encoding to \c B_UNICODE_UTF8.
5145 void
5146 BTextView::_NormalizeFont(BFont* font)
5148 if (font) {
5149 font->SetRotation(0.0f);
5150 font->SetFlags(0);
5151 font->SetSpacing(B_BITMAP_SPACING);
5152 font->SetEncoding(B_UNICODE_UTF8);
5157 void
5158 BTextView::_SetRunArray(int32 startOffset, int32 endOffset,
5159 const text_run_array* runs)
5161 const int32 numStyles = runs->count;
5162 if (numStyles > 0) {
5163 const text_run* theRun = &runs->runs[0];
5164 for (int32 index = 0; index < numStyles; index++) {
5165 int32 fromOffset = theRun->offset + startOffset;
5166 int32 toOffset = endOffset;
5167 if (index + 1 < numStyles) {
5168 toOffset = (theRun + 1)->offset + startOffset;
5169 toOffset = (toOffset > endOffset) ? endOffset : toOffset;
5172 _ApplyStyleRange(fromOffset, toOffset, B_FONT_ALL, &theRun->font,
5173 &theRun->color, false);
5175 theRun++;
5177 fStyles->InvalidateNullStyle();
5182 /*! Returns the character class of the character at the given offset.
5184 \param offset The offset where the wanted character can be found.
5186 \return A value which represents the character's classification.
5188 uint32
5189 BTextView::_CharClassification(int32 offset) const
5191 // TODO: Should check against a list of characters containing also
5192 // japanese word breakers.
5193 // And what about other languages ? Isn't there a better way to check
5194 // for separator characters ?
5195 // Andrew suggested to have a look at UnicodeBlockObject.h
5196 switch (fText->RealCharAt(offset)) {
5197 case '\0':
5198 return CHAR_CLASS_END_OF_TEXT;
5200 case B_SPACE:
5201 case B_TAB:
5202 case B_ENTER:
5203 return CHAR_CLASS_WHITESPACE;
5205 case '=':
5206 case '+':
5207 case '@':
5208 case '#':
5209 case '$':
5210 case '%':
5211 case '^':
5212 case '&':
5213 case '*':
5214 case '\\':
5215 case '|':
5216 case '<':
5217 case '>':
5218 case '/':
5219 case '~':
5220 return CHAR_CLASS_GRAPHICAL;
5222 case '\'':
5223 case '"':
5224 return CHAR_CLASS_QUOTE;
5226 case ',':
5227 case '.':
5228 case '?':
5229 case '!':
5230 case ';':
5231 case ':':
5232 case '-':
5233 return CHAR_CLASS_PUNCTUATION;
5235 case '(':
5236 case '[':
5237 case '{':
5238 return CHAR_CLASS_PARENS_OPEN;
5240 case ')':
5241 case ']':
5242 case '}':
5243 return CHAR_CLASS_PARENS_CLOSE;
5245 default:
5246 return CHAR_CLASS_DEFAULT;
5251 /*! Returns the offset of the next UTF-8 character.
5253 \param offset The offset where to start looking.
5255 \return The offset of the next UTF-8 character.
5257 int32
5258 BTextView::_NextInitialByte(int32 offset) const
5260 if (offset >= fText->Length())
5261 return offset;
5263 for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset)
5266 return offset;
5270 /*! Returns the offset of the previous UTF-8 character.
5272 \param offset The offset where to start looking.
5274 \return The offset of the previous UTF-8 character.
5276 int32
5277 BTextView::_PreviousInitialByte(int32 offset) const
5279 if (offset <= 0)
5280 return 0;
5282 int32 count = 6;
5284 for (--offset; offset > 0 && count; --offset, --count) {
5285 if ((ByteAt(offset) & 0xC0) != 0x80)
5286 break;
5289 return count ? offset : 0;
5293 bool
5294 BTextView::_GetProperty(BMessage* specifier, int32 form, const char* property,
5295 BMessage* reply)
5297 CALLED();
5298 if (strcmp(property, "selection") == 0) {
5299 reply->what = B_REPLY;
5300 reply->AddInt32("result", fSelStart);
5301 reply->AddInt32("result", fSelEnd);
5302 reply->AddInt32("error", B_OK);
5304 return true;
5305 } else if (strcmp(property, "Text") == 0) {
5306 if (IsTypingHidden()) {
5307 // Do not allow stealing passwords via scripting
5308 beep();
5309 return false;
5312 int32 index, range;
5313 specifier->FindInt32("index", &index);
5314 specifier->FindInt32("range", &range);
5316 char* buffer = new char[range + 1];
5317 GetText(index, range, buffer);
5319 reply->what = B_REPLY;
5320 reply->AddString("result", buffer);
5321 reply->AddInt32("error", B_OK);
5323 delete[] buffer;
5325 return true;
5326 } else if (strcmp(property, "text_run_array") == 0)
5327 return false;
5329 return false;
5333 bool
5334 BTextView::_SetProperty(BMessage* specifier, int32 form, const char* property,
5335 BMessage* reply)
5337 CALLED();
5338 if (strcmp(property, "selection") == 0) {
5339 int32 index, range;
5341 specifier->FindInt32("index", &index);
5342 specifier->FindInt32("range", &range);
5344 Select(index, index + range);
5346 reply->what = B_REPLY;
5347 reply->AddInt32("error", B_OK);
5349 return true;
5350 } else if (strcmp(property, "Text") == 0) {
5351 int32 index, range;
5352 specifier->FindInt32("index", &index);
5353 specifier->FindInt32("range", &range);
5355 const char* buffer = NULL;
5356 if (specifier->FindString("data", &buffer) == B_OK)
5357 InsertText(buffer, range, index, NULL);
5358 else
5359 DeleteText(index, range);
5361 reply->what = B_REPLY;
5362 reply->AddInt32("error", B_OK);
5364 return true;
5365 } else if (strcmp(property, "text_run_array") == 0)
5366 return false;
5368 return false;
5372 bool
5373 BTextView::_CountProperties(BMessage* specifier, int32 form,
5374 const char* property, BMessage* reply)
5376 CALLED();
5377 if (strcmp(property, "Text") == 0) {
5378 reply->what = B_REPLY;
5379 reply->AddInt32("result", TextLength());
5380 reply->AddInt32("error", B_OK);
5381 return true;
5384 return false;
5388 //! Called when the object receives a \c B_INPUT_METHOD_CHANGED message.
5389 void
5390 BTextView::_HandleInputMethodChanged(BMessage* message)
5392 // TODO: block input if not editable (Andrew)
5393 ASSERT(fInline != NULL);
5395 const char* string = NULL;
5396 if (message->FindString("be:string", &string) < B_OK || string == NULL)
5397 return;
5399 _HideCaret();
5401 if (IsFocus())
5402 be_app->ObscureCursor();
5404 // If we find the "be:confirmed" boolean (and the boolean is true),
5405 // it means it's over for now, so the current InlineInput object
5406 // should become inactive. We will probably receive a
5407 // B_INPUT_METHOD_STOPPED message after this one.
5408 bool confirmed;
5409 if (message->FindBool("be:confirmed", &confirmed) != B_OK)
5410 confirmed = false;
5412 // Delete the previously inserted text (if any)
5413 if (fInline->IsActive()) {
5414 const int32 oldOffset = fInline->Offset();
5415 DeleteText(oldOffset, oldOffset + fInline->Length());
5416 if (confirmed)
5417 fInline->SetActive(false);
5418 fCaretOffset = fSelStart = fSelEnd = oldOffset;
5421 const int32 stringLen = strlen(string);
5423 fInline->SetOffset(fSelStart);
5424 fInline->SetLength(stringLen);
5425 fInline->ResetClauses();
5427 if (!confirmed && !fInline->IsActive())
5428 fInline->SetActive(true);
5430 // Get the clauses, and pass them to the InlineInput object
5431 // TODO: Find out if what we did it's ok, currently we don't consider
5432 // clauses at all, while the bebook says we should; though the visual
5433 // effect we obtained seems correct. Weird.
5434 int32 clauseCount = 0;
5435 int32 clauseStart;
5436 int32 clauseEnd;
5437 while (message->FindInt32("be:clause_start", clauseCount, &clauseStart)
5438 == B_OK
5439 && message->FindInt32("be:clause_end", clauseCount, &clauseEnd)
5440 == B_OK) {
5441 if (!fInline->AddClause(clauseStart, clauseEnd))
5442 break;
5443 clauseCount++;
5446 if (confirmed) {
5447 _Refresh(fSelStart, fSelEnd, true);
5448 _ShowCaret();
5450 // now we need to feed ourselves the individual characters as if the
5451 // user would have pressed them now - this lets KeyDown() pick out all
5452 // the special characters like B_BACKSPACE, cursor keys and the like:
5453 const char* currPos = string;
5454 const char* prevPos = currPos;
5455 while (*currPos != '\0') {
5456 if ((*currPos & 0xC0) == 0xC0) {
5457 // found the start of an UTF-8 char, we collect while it lasts
5458 ++currPos;
5459 while ((*currPos & 0xC0) == 0x80)
5460 ++currPos;
5461 } else if ((*currPos & 0xC0) == 0x80) {
5462 // illegal: character starts with utf-8 intermediate byte,
5463 // skip it
5464 prevPos = ++currPos;
5465 } else {
5466 // single byte character/code, just feed that
5467 ++currPos;
5469 KeyDown(prevPos, currPos - prevPos);
5470 prevPos = currPos;
5473 _Refresh(fSelStart, fSelEnd, true);
5474 } else {
5475 // temporarily show transient state of inline input
5476 int32 selectionStart = 0;
5477 int32 selectionEnd = 0;
5478 message->FindInt32("be:selection", 0, &selectionStart);
5479 message->FindInt32("be:selection", 1, &selectionEnd);
5481 fInline->SetSelectionOffset(selectionStart);
5482 fInline->SetSelectionLength(selectionEnd - selectionStart);
5484 const int32 inlineOffset = fInline->Offset();
5485 InsertText(string, stringLen, fSelStart, NULL);
5487 _Refresh(inlineOffset, fSelEnd, true);
5488 _ShowCaret();
5494 /*! Called when the object receives a \c B_INPUT_METHOD_LOCATION_REQUEST
5495 message.
5497 void
5498 BTextView::_HandleInputMethodLocationRequest()
5500 ASSERT(fInline != NULL);
5502 int32 offset = fInline->Offset();
5503 const int32 limit = offset + fInline->Length();
5505 BMessage message(B_INPUT_METHOD_EVENT);
5506 message.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST);
5508 // Add the location of the UTF8 characters
5509 while (offset < limit) {
5510 float height;
5511 BPoint where = PointAt(offset, &height);
5512 ConvertToScreen(&where);
5514 message.AddPoint("be:location_reply", where);
5515 message.AddFloat("be:height_reply", height);
5517 offset = _NextInitialByte(offset);
5520 fInline->Method()->SendMessage(&message);
5524 //! Tells the Input Server method add-on to stop the current transaction.
5525 void
5526 BTextView::_CancelInputMethod()
5528 if (!fInline)
5529 return;
5531 InlineInput* inlineInput = fInline;
5532 fInline = NULL;
5534 if (inlineInput->IsActive() && Window()) {
5535 _Refresh(inlineInput->Offset(), fText->Length() - inlineInput->Offset(),
5536 false);
5538 BMessage message(B_INPUT_METHOD_EVENT);
5539 message.AddInt32("be:opcode", B_INPUT_METHOD_STOPPED);
5540 inlineInput->Method()->SendMessage(&message);
5543 delete inlineInput;
5547 /*! Returns the line number of the character at the given \a offset.
5549 \note This will never return the last line (use LineAt() if you
5550 need to be correct about that.) N.B.
5552 \param offset The offset of the wanted character.
5554 \return The line number of the character at the given \a offset as an int32.
5556 int32
5557 BTextView::_LineAt(int32 offset) const
5559 return fLines->OffsetToLine(offset);
5563 /*! Returns the line number that the given \a point is on.
5565 \note This will never return the last line (use LineAt() if you
5566 need to be correct about that.) N.B.
5568 \param point The \a point the get the line number of.
5570 \return The line number of the given \a point as an int32.
5572 int32
5573 BTextView::_LineAt(const BPoint& point) const
5575 return fLines->PixelToLine(point.y - fTextRect.top);
5579 /*! Returns whether or not the given \a offset is on the empty line at the end
5580 of the buffer.
5582 bool
5583 BTextView::_IsOnEmptyLastLine(int32 offset) const
5585 return (offset == TextLength() && offset > 0
5586 && fText->RealCharAt(offset - 1) == B_ENTER);
5590 void
5591 BTextView::_ApplyStyleRange(int32 fromOffset, int32 toOffset, uint32 mode,
5592 const BFont* font, const rgb_color* color, bool syncNullStyle)
5594 if (font != NULL) {
5595 // if a font has been given, normalize it
5596 BFont normalized = *font;
5597 _NormalizeFont(&normalized);
5598 font = &normalized;
5601 if (!fStylable) {
5602 // always apply font and color to full range for non-stylable textviews
5603 fromOffset = 0;
5604 toOffset = fText->Length();
5607 if (syncNullStyle)
5608 fStyles->SyncNullStyle(fromOffset);
5610 fStyles->SetStyleRange(fromOffset, toOffset, fText->Length(), mode,
5611 font, color);
5615 float
5616 BTextView::_NullStyleHeight() const
5618 const BFont* font = NULL;
5619 fStyles->GetNullStyle(&font, NULL);
5621 font_height fontHeight;
5622 font->GetHeight(&fontHeight);
5623 return ceilf(fontHeight.ascent + fontHeight.descent + 1);
5627 void
5628 BTextView::_ShowContextMenu(BPoint where)
5630 bool isRedo;
5631 undo_state state = UndoState(&isRedo);
5632 bool isUndo = state != B_UNDO_UNAVAILABLE && !isRedo;
5634 int32 start;
5635 int32 finish;
5636 GetSelection(&start, &finish);
5638 bool canEdit = IsEditable();
5639 int32 length = TextLength();
5641 BPopUpMenu* menu = new BPopUpMenu(B_EMPTY_STRING, false, false);
5643 BLayoutBuilder::Menu<>(menu)
5644 .AddItem(TRANSLATE("Undo"), B_UNDO/*, 'Z'*/)
5645 .SetEnabled(canEdit && isUndo)
5646 .AddItem(TRANSLATE("Redo"), B_UNDO/*, 'Z', B_SHIFT_KEY*/)
5647 .SetEnabled(canEdit && isRedo)
5648 .AddSeparator()
5649 .AddItem(TRANSLATE("Cut"), B_CUT, 'X')
5650 .SetEnabled(canEdit && start != finish)
5651 .AddItem(TRANSLATE("Copy"), B_COPY, 'C')
5652 .SetEnabled(start != finish)
5653 .AddItem(TRANSLATE("Paste"), B_PASTE, 'V')
5654 .SetEnabled(canEdit && be_clipboard->SystemCount() > 0)
5655 .AddSeparator()
5656 .AddItem(TRANSLATE("Select all"), B_SELECT_ALL, 'A')
5657 .SetEnabled(!(start == 0 && finish == length))
5660 menu->SetTargetForItems(this);
5661 ConvertToScreen(&where);
5662 menu->Go(where, true, true, true);
5666 void
5667 BTextView::_FilterDisallowedChars(char* text, ssize_t& length,
5668 text_run_array* runArray)
5670 if (!fDisallowedChars)
5671 return;
5673 if (fDisallowedChars->IsEmpty() || !text)
5674 return;
5676 ssize_t stringIndex = 0;
5677 if (runArray) {
5678 ssize_t remNext = 0;
5680 for (int i = 0; i < runArray->count; i++) {
5681 runArray->runs[i].offset -= remNext;
5682 while (stringIndex < runArray->runs[i].offset
5683 && stringIndex < length) {
5684 if (fDisallowedChars->HasItem(
5685 reinterpret_cast<void*>(text[stringIndex]))) {
5686 memmove(text + stringIndex, text + stringIndex + 1,
5687 length - stringIndex - 1);
5688 length--;
5689 runArray->runs[i].offset--;
5690 remNext++;
5691 } else
5692 stringIndex++;
5697 while (stringIndex < length) {
5698 if (fDisallowedChars->HasItem(
5699 reinterpret_cast<void*>(text[stringIndex]))) {
5700 memmove(text + stringIndex, text + stringIndex + 1,
5701 length - stringIndex - 1);
5702 length--;
5703 } else
5704 stringIndex++;
5709 // #pragma mark - BTextView::TextTrackState
5712 BTextView::TextTrackState::TextTrackState(BMessenger messenger)
5714 clickOffset(0),
5715 shiftDown(false),
5716 anchor(0),
5717 selStart(0),
5718 selEnd(0),
5719 fRunner(NULL)
5721 BMessage message(_PING_);
5722 const bigtime_t scrollSpeed = 25 * 1000; // 40 scroll steps per second
5723 fRunner = new (nothrow) BMessageRunner(messenger, &message, scrollSpeed);
5727 BTextView::TextTrackState::~TextTrackState()
5729 delete fRunner;
5733 void
5734 BTextView::TextTrackState::SimulateMouseMovement(BTextView* textView)
5736 BPoint where;
5737 uint32 buttons;
5738 // When the mouse cursor is still and outside the textview,
5739 // no B_MOUSE_MOVED message are sent, obviously. But scrolling
5740 // has to work neverthless, so we "fake" a MouseMoved() call here.
5741 textView->GetMouse(&where, &buttons);
5742 textView->_PerformMouseMoved(where, B_INSIDE_VIEW);
5746 // #pragma mark - Binary ABI compat
5749 extern "C" void
5750 B_IF_GCC_2(InvalidateLayout__9BTextViewb, _ZN9BTextView16InvalidateLayoutEb)(
5751 BTextView* view, bool descendants)
5753 perform_data_layout_invalidated data;
5754 data.descendants = descendants;
5756 view->Perform(PERFORM_CODE_LAYOUT_INVALIDATED, &data);