2 * Copyright 2015, Axel Dörfler, axeld@pinc-software.de.
3 * Copyright 2010 Stephan Aßmus <superstippi@gmx.de>
4 * Distributed under the terms of the MIT License.
8 #include "AddressTextControl.h"
13 #include <ControlLook.h>
14 #include <Clipboard.h>
16 #include <LayoutBuilder.h>
18 #include <LayoutUtils.h>
20 #include <PopUpMenu.h>
21 #include <SeparatorView.h>
30 #include "QueryList.h"
31 #include "TextViewCompleter.h"
34 #undef B_TRANSLATION_CONTEXT
35 #define B_TRANSLATION_CONTEXT "AddressTextControl"
38 static const uint32 kMsgAddAddress
= 'adad';
39 static const float kVerticalTextRectInset
= 2.0;
42 class AddressTextControl::TextView
: public BTextView
{
44 static const uint32 MSG_CLEAR
= 'cler';
47 TextView(AddressTextControl
* parent
);
50 virtual void MessageReceived(BMessage
* message
);
51 virtual void FrameResized(float width
, float height
);
52 virtual void KeyDown(const char* bytes
, int32 numBytes
);
53 virtual void MakeFocus(bool focused
= true);
55 virtual BSize
MinSize();
56 virtual BSize
MaxSize();
58 const BMessage
* ModificationMessage() const;
59 void SetModificationMessage(BMessage
* message
);
61 void SetUpdateAutoCompleterChoices(bool update
);
64 virtual void InsertText(const char* text
, int32 length
,
66 const text_run_array
* runs
);
67 virtual void DeleteText(int32 fromOffset
, int32 toOffset
);
70 void _AlignTextRect();
73 AddressTextControl
* fAddressTextControl
;
74 TextViewCompleter
* fAutoCompleter
;
75 BString fPreviousText
;
76 bool fUpdateAutoCompleterChoices
;
77 BMessage
* fModificationMessage
;
81 class AddressPopUpMenu
: public BPopUpMenu
, public QueryListener
{
84 virtual ~AddressPopUpMenu();
87 virtual void EntryCreated(QueryList
& source
,
88 const entry_ref
& ref
, ino_t node
);
89 virtual void EntryRemoved(QueryList
& source
,
90 const node_ref
& nodeRef
);
94 void _AddGroup(const char* label
, const char* group
,
95 PersonList
& peopleList
);
96 void _AddPeople(BMenu
* menu
, PersonList
& peopleList
,
98 bool addSeparator
= false);
99 bool _MatchesGroup(const Person
& person
,
104 class AddressTextControl::PopUpButton
: public BControl
{
107 virtual ~PopUpButton();
109 virtual BSize
MinSize();
110 virtual BSize
PreferredSize();
111 virtual BSize
MaxSize();
113 virtual void MouseDown(BPoint where
);
114 virtual void Draw(BRect updateRect
);
117 AddressPopUpMenu
* fPopUpMenu
;
121 class PeopleChoiceModel
: public BAutoCompleter::ChoiceModel
{
133 virtual void FetchChoicesFor(const BString
& pattern
)
135 // Remove all existing choices
136 fChoices
.MakeEmpty();
138 // Search through the people list for any matches
139 PersonList
& peopleList
= static_cast<TMailApp
*>(be_app
)->People();
140 BAutolock
locker(peopleList
);
142 for (int32 index
= 0; index
< peopleList
.CountPersons(); index
++) {
143 const Person
* person
= peopleList
.PersonAt(index
);
145 const BString
& baseText
= person
->Name();
146 for (int32 addressIndex
= 0;
147 addressIndex
< person
->CountAddresses(); addressIndex
++) {
148 BString choiceText
= baseText
;
149 choiceText
<< " <" << person
->AddressAt(addressIndex
) << ">";
151 int32 match
= choiceText
.IFindFirst(pattern
);
155 fChoices
.AddItem(new BAutoCompleter::Choice(choiceText
,
156 choiceText
, match
, pattern
.Length()));
161 fChoices
.SortItems(_CompareChoices
);
164 virtual int32
CountChoices() const
166 return fChoices
.CountItems();
169 virtual const BAutoCompleter::Choice
* ChoiceAt(int32 index
) const
171 return fChoices
.ItemAt(index
);
174 static int _CompareChoices(const BAutoCompleter::Choice
* a
,
175 const BAutoCompleter::Choice
* b
)
177 return a
->DisplayText().Compare(b
->DisplayText());
181 BObjectList
<BAutoCompleter::Choice
> fChoices
;
185 // #pragma mark - TextView
188 AddressTextControl::TextView::TextView(AddressTextControl
* parent
)
191 fAddressTextControl(parent
),
192 fAutoCompleter(new TextViewCompleter(this,
193 new PeopleChoiceModel())),
195 fUpdateAutoCompleterChoices(true)
199 fAutoCompleter
->SetModificationsReported(true);
203 AddressTextControl::TextView::~TextView()
205 delete fAutoCompleter
;
210 AddressTextControl::TextView::MessageReceived(BMessage
* message
)
212 switch (message
->what
) {
218 BTextView::MessageReceived(message
);
225 AddressTextControl::TextView::FrameResized(float width
, float height
)
227 BTextView::FrameResized(width
, height
);
233 AddressTextControl::TextView::KeyDown(const char* bytes
, int32 numBytes
)
237 BView::KeyDown(bytes
, numBytes
);
241 // Revert to text as it was when we received keyboard focus.
242 SetText(fPreviousText
.String());
247 // Don't let this through to the text view.
251 BTextView::KeyDown(bytes
, numBytes
);
257 AddressTextControl::TextView::MakeFocus(bool focus
)
259 if (focus
== IsFocus())
262 BTextView::MakeFocus(focus
);
265 fPreviousText
= Text();
269 fAddressTextControl
->Invalidate();
274 AddressTextControl::TextView::MinSize()
277 min
.height
= ceilf(LineHeight(0) + kVerticalTextRectInset
);
278 // we always add at least one pixel vertical inset top/bottom for
280 min
.width
= min
.height
* 3;
281 return BLayoutUtils::ComposeSize(ExplicitMinSize(), min
);
286 AddressTextControl::TextView::MaxSize()
288 BSize
max(MinSize());
289 max
.width
= B_SIZE_UNLIMITED
;
290 return BLayoutUtils::ComposeSize(ExplicitMaxSize(), max
);
295 AddressTextControl::TextView::ModificationMessage() const
297 return fModificationMessage
;
302 AddressTextControl::TextView::SetModificationMessage(BMessage
* message
)
304 fModificationMessage
= message
;
309 AddressTextControl::TextView::SetUpdateAutoCompleterChoices(bool update
)
311 fUpdateAutoCompleterChoices
= update
;
316 AddressTextControl::TextView::InsertText(const char* text
,
317 int32 length
, int32 offset
, const text_run_array
* runs
)
319 if (!strncmp(text
, "mailto:", 7)) {
326 // Filter all line breaks, note that text is not terminated.
328 if (*text
== '\n' || *text
== '\r')
329 BTextView::InsertText(" ", 1, offset
, runs
);
331 BTextView::InsertText(text
, 1, offset
, runs
);
333 BString
filteredText(text
, length
);
334 filteredText
.ReplaceAll('\n', ' ');
335 filteredText
.ReplaceAll('\r', ' ');
336 BTextView::InsertText(filteredText
.String(), length
, offset
,
340 // TODO: change E-mail representation
342 // Make the base URL part bold.
343 BString text(Text(), TextLength());
344 int32 baseUrlStart = text.FindFirst("://");
345 if (baseUrlStart >= 0)
349 int32 baseUrlEnd = text.FindFirst("/", baseUrlStart);
351 baseUrlEnd = TextLength();
355 const rgb_color black = (rgb_color) { 0, 0, 0, 255 };
356 const rgb_color gray = (rgb_color) { 60, 60, 60, 255 };
357 if (baseUrlStart > 0)
358 SetFontAndColor(0, baseUrlStart, &font, B_FONT_ALL, &gray);
359 if (baseUrlEnd > baseUrlStart) {
360 font.SetFace(B_BOLD_FACE);
361 SetFontAndColor(baseUrlStart, baseUrlEnd, &font, B_FONT_ALL, &black);
363 if (baseUrlEnd < TextLength()) {
364 font.SetFace(B_REGULAR_FACE);
365 SetFontAndColor(baseUrlEnd, TextLength(), &font, B_FONT_ALL, &gray);
368 fAutoCompleter
->TextModified(fUpdateAutoCompleterChoices
);
369 fAddressTextControl
->InvokeNotify(fModificationMessage
,
375 AddressTextControl::TextView::DeleteText(int32 fromOffset
,
378 BTextView::DeleteText(fromOffset
, toOffset
);
380 fAutoCompleter
->TextModified(fUpdateAutoCompleterChoices
);
381 fAddressTextControl
->InvokeNotify(fModificationMessage
,
387 AddressTextControl::TextView::_AlignTextRect()
389 // Layout the text rect to be in the middle, normally this means there
390 // is one pixel spacing on each side.
391 BRect
textRect(Bounds());
393 float vInset
= max_c(1,
394 floorf((textRect
.Height() - LineHeight(0)) / 2.0 + 0.5));
397 if (be_control_look
!= NULL
)
398 hInset
= be_control_look
->DefaultLabelSpacing();
400 textRect
.InsetBy(hInset
, vInset
);
401 SetTextRect(textRect
);
405 // #pragma mark - PopUpButton
408 AddressTextControl::PopUpButton::PopUpButton()
410 BControl(NULL
, NULL
, NULL
, B_WILL_DRAW
)
412 fPopUpMenu
= new AddressPopUpMenu();
416 AddressTextControl::PopUpButton::~PopUpButton()
423 AddressTextControl::PopUpButton::MinSize()
425 // TODO: BControlLook does not give us any size information!
426 return BSize(10, 10);
431 AddressTextControl::PopUpButton::PreferredSize()
433 return BSize(10, B_SIZE_UNSET
);
438 AddressTextControl::PopUpButton::MaxSize()
440 return BSize(10, B_SIZE_UNLIMITED
);
445 AddressTextControl::PopUpButton::MouseDown(BPoint where
)
447 if (fPopUpMenu
->Parent() != NULL
)
451 fPopUpMenu
->GetPreferredSize(&width
, NULL
);
452 fPopUpMenu
->SetTargetForItems(Parent());
454 BPoint
point(Bounds().Width() - width
, Bounds().Height() + 2);
455 ConvertToScreen(&point
);
456 fPopUpMenu
->Go(point
, true, true, true);
461 AddressTextControl::PopUpButton::Draw(BRect updateRect
)
465 flags
|= BControlLook::B_DISABLED
;
467 if (IsFocus() && Window()->IsActive())
468 flags
|= BControlLook::B_FOCUSED
;
470 rgb_color base
= ui_color(B_MENU_BACKGROUND_COLOR
);
471 BRect rect
= Bounds();
472 be_control_look
->DrawMenuFieldBackground(this, rect
,
473 updateRect
, base
, true, flags
);
477 // #pragma mark - PopUpMenu
480 AddressPopUpMenu::AddressPopUpMenu()
484 static_cast<TMailApp
*>(be_app
)->PeopleQueryList().AddListener(this);
488 AddressPopUpMenu::~AddressPopUpMenu()
490 static_cast<TMailApp
*>(be_app
)->PeopleQueryList().RemoveListener(this);
495 AddressPopUpMenu::EntryCreated(QueryList
& source
,
496 const entry_ref
& ref
, ino_t node
)
503 AddressPopUpMenu::EntryRemoved(QueryList
& source
,
504 const node_ref
& nodeRef
)
511 AddressPopUpMenu::_RebuildMenu()
514 int32 index
= CountItems();
515 while (index
-- > 0) {
516 delete RemoveItem(index
);
520 PersonList
& peopleList
= static_cast<TMailApp
*>(be_app
)->People();
521 BAutolock
locker(peopleList
);
523 if (peopleList
.CountPersons() > 0)
524 _AddGroup(B_TRANSLATE("All people"), NULL
, peopleList
);
526 GroupList
& groupList
= static_cast<TMailApp
*>(be_app
)->PeopleGroups();
527 BAutolock
groupLocker(groupList
);
529 for (int32 index
= 0; index
< groupList
.CountGroups(); index
++) {
530 BString group
= groupList
.GroupAt(index
);
531 _AddGroup(group
, group
, peopleList
);
534 groupLocker
.Unlock();
536 _AddPeople(this, peopleList
, "", true);
541 AddressPopUpMenu::_AddGroup(const char* label
, const char* group
,
542 PersonList
& peopleList
)
544 BMenu
* menu
= new BMenu(label
);
546 menu
->Superitem()->SetMessage(new BMessage(kMsgAddAddress
));
548 _AddPeople(menu
, peopleList
, group
);
553 AddressPopUpMenu::_AddPeople(BMenu
* menu
, PersonList
& peopleList
,
554 const char* group
, bool addSeparator
)
556 for (int32 index
= 0; index
< peopleList
.CountPersons(); index
++) {
557 const Person
* person
= peopleList
.PersonAt(index
);
558 if (!_MatchesGroup(*person
, group
))
561 if (person
->CountAddresses() != 0 && addSeparator
) {
562 menu
->AddSeparatorItem();
563 addSeparator
= false;
566 for (int32 addressIndex
= 0; addressIndex
< person
->CountAddresses();
568 BString email
= person
->Name();
569 email
<< " <" << person
->AddressAt(addressIndex
) << ">";
571 BMessage
* message
= new BMessage(kMsgAddAddress
);
572 message
->AddString("email", email
);
573 menu
->AddItem(new BMenuItem(email
, message
));
575 if (menu
->Superitem() != NULL
)
576 menu
->Superitem()->Message()->AddString("email", email
);
583 AddressPopUpMenu::_MatchesGroup(const Person
& person
, const char* group
)
588 if (group
[0] == '\0')
589 return person
.CountGroups() == 0;
591 return person
.IsInGroup(group
);
598 AddressTextControl::PopUpMenu::_AddPersonItem(const entry_ref *ref, ino_t node, BString &name,
599 BString &email, const char *attr, BMenu *groupMenu, BMenuItem *superItem)
603 // For alphabetical order sorting, usually last name.
605 // if we have no Name, just use the email address
606 if (name.Length() == 0) {
610 // otherwise, pretty-format it
611 label << name << " (" << email << ")";
613 // Extract the last name (last word in the name),
614 // removing trailing and leading spaces.
615 const char *nameStart = name.String();
616 const char *string = nameStart + strlen(nameStart) - 1;
619 while (string >= nameStart && isspace(*string))
621 wordEnd = string + 1; // Points to just after last word.
622 while (string >= nameStart && !isspace(*string))
624 string++; // Point to first letter in the word.
625 if (wordEnd > string)
626 sortKey.SetTo(string, wordEnd - string);
627 else // Blank name, pretend that the last name is after it.
628 string = nameStart + strlen(nameStart);
630 // Append the first names to the end, so that people with the same last
631 // name get sorted by first name. Note no space between the end of the
632 // last name and the start of the first names, but that shouldn't
633 // matter for sorting.
634 sortKey.Append(nameStart, string - nameStart);
639 // #pragma mark - AddressTextControl
642 AddressTextControl::AddressTextControl(const char* name
, BMessage
* message
)
644 BControl(name
, NULL
, message
, B_WILL_DRAW
),
646 fWindowActive(false),
649 fTextView
= new TextView(this);
650 fTextView
->SetExplicitMinSize(BSize(100, B_SIZE_UNSET
));
651 fPopUpButton
= new PopUpButton();
653 BLayoutBuilder::Group
<>(this, B_HORIZONTAL
, 0)
658 SetFlags(Flags() | B_WILL_DRAW
| B_FULL_UPDATE_ON_RESIZE
);
659 SetLowUIColor(ViewUIColor());
660 SetViewUIColor(fTextView
->ViewUIColor());
662 SetExplicitAlignment(BAlignment(B_ALIGN_USE_FULL_WIDTH
,
663 B_ALIGN_VERTICAL_CENTER
));
665 SetEnabled(fEditable
);
666 // Sets the B_NAVIGABLE flag on the TextView
670 AddressTextControl::~AddressTextControl()
676 AddressTextControl::AttachedToWindow()
678 BControl::AttachedToWindow();
679 fWindowActive
= Window()->IsActive();
684 AddressTextControl::WindowActivated(bool active
)
686 BControl::WindowActivated(active
);
687 if (fWindowActive
!= active
) {
688 fWindowActive
= active
;
695 AddressTextControl::Draw(BRect updateRect
)
700 BRect
bounds(Bounds());
701 rgb_color
base(LowColor());
704 flags
|= BControlLook::B_DISABLED
;
705 if (fWindowActive
&& fTextView
->IsFocus())
706 flags
|= BControlLook::B_FOCUSED
;
707 be_control_look
->DrawTextControlBorder(this, bounds
, updateRect
, base
,
713 AddressTextControl::MakeFocus(bool focus
)
715 // Forward this to the text view, we never accept focus ourselves.
716 fTextView
->MakeFocus(focus
);
721 AddressTextControl::SetEnabled(bool enabled
)
723 BControl::SetEnabled(enabled
);
724 fTextView
->MakeEditable(enabled
&& fEditable
);
726 fTextView
->SetFlags(fTextView
->Flags() | B_NAVIGABLE
);
728 fTextView
->SetFlags(fTextView
->Flags() & ~B_NAVIGABLE
);
730 fPopUpButton
->SetEnabled(enabled
);
732 _UpdateTextViewColors();
737 AddressTextControl::MessageReceived(BMessage
* message
)
739 switch (message
->what
) {
744 if (message
->FindInt32("buttons", &buttons
) != B_OK
)
745 buttons
= B_PRIMARY_MOUSE_BUTTON
;
747 if (buttons
!= B_PRIMARY_MOUSE_BUTTON
748 && message
->FindPoint("_drop_point_", &point
) != B_OK
)
751 BMessage
forwardRefs(B_REFS_RECEIVED
);
752 bool forward
= false;
755 for (int32 index
= 0;message
->FindRef("refs", index
, &ref
) == B_OK
; index
++) {
756 BFile
file(&ref
, B_READ_ONLY
);
757 if (file
.InitCheck() == B_NO_ERROR
) {
758 BNodeInfo
info(&file
);
759 char type
[B_FILE_NAME_LENGTH
];
762 if (!strcmp(type
,"application/x-person")) {
763 // add person's E-mail address to the To: field
766 if (buttons
== B_PRIMARY_MOUSE_BUTTON
) {
767 if (message
->FindString("attr", &attr
) < B_OK
)
773 char buffer
[B_ATTR_NAME_LENGTH
];
776 fRefDropMenu
= new BPopUpMenu("RecipientMenu");
778 while (node
.GetNextAttrName(buffer
) == B_OK
) {
779 if (strstr(buffer
, "email") == NULL
)
785 node
.ReadAttrString(buffer
, &address
);
786 if (address
.Length() <= 0)
790 = new BMessage(kMsgAddAddress
);
791 itemMsg
->AddString("email", address
.String());
793 BMenuItem
* item
= new BMenuItem(
794 address
.String(), itemMsg
);
795 fRefDropMenu
->AddItem(item
);
798 if (fRefDropMenu
->CountItems() > 1) {
799 fRefDropMenu
->SetTargetForItems(this);
800 fRefDropMenu
->Go(point
, true, true, true);
809 file
.ReadAttrString(attr
.String(), &email
);
811 // we got something...
812 if (email
.Length() > 0) {
813 // see if we can get a username as well
815 file
.ReadAttrString("META:name", &name
);
818 if (name
.Length() == 0) {
819 // if we have no Name, just use the email address
822 // otherwise, pretty-format it
823 address
<< "\"" << name
<< "\" <" << email
<< ">";
826 _AddAddress(address
);
830 forwardRefs
.AddRef("refs", &ref
);
837 Window()->PostMessage(&forwardRefs
, Parent());
844 BTextView
*textView
= (BTextView
*)ChildAt(0);
845 if (textView
!= NULL
)
846 textView
->Select(0, textView
->TextLength());
853 for (int32 index
= 0;
854 message
->FindString("email", index
++, &email
) == B_OK
;)
860 BControl::MessageReceived(message
);
867 AddressTextControl::ModificationMessage() const
869 return fTextView
->ModificationMessage();
874 AddressTextControl::SetModificationMessage(BMessage
* message
)
876 fTextView
->SetModificationMessage(message
);
881 AddressTextControl::IsEditable() const
888 AddressTextControl::SetEditable(bool editable
)
890 fTextView
->MakeEditable(IsEnabled() && editable
);
891 fEditable
= editable
;
893 if (editable
&& fPopUpButton
->IsHidden(this))
894 fPopUpButton
->Show();
895 else if (!editable
&& !fPopUpButton
->IsHidden(this))
896 fPopUpButton
->Hide();
901 AddressTextControl::SetText(const char* text
)
903 if (text
== NULL
|| Text() == NULL
|| strcmp(Text(), text
) != 0) {
904 fTextView
->SetUpdateAutoCompleterChoices(false);
905 fTextView
->SetText(text
);
906 fTextView
->SetUpdateAutoCompleterChoices(true);
912 AddressTextControl::Text() const
914 return fTextView
->Text();
919 AddressTextControl::TextLength() const
921 return fTextView
->TextLength();
926 AddressTextControl::GetSelection(int32
* start
, int32
* end
) const
928 fTextView
->GetSelection(start
, end
);
933 AddressTextControl::Select(int32 start
, int32 end
)
935 fTextView
->Select(start
, end
);
940 AddressTextControl::SelectAll()
942 fTextView
->Select(0, TextLength());
947 AddressTextControl::HasFocus()
949 return fTextView
->IsFocus();
954 AddressTextControl::_AddAddress(const char* text
)
956 int last
= fTextView
->TextLength();
958 fTextView
->Select(last
, last
);
959 // TODO: test if there is already a ','
960 fTextView
->Insert(", ");
962 fTextView
->Insert(text
);
967 AddressTextControl::_UpdateTextViewColors()
970 fTextView
->GetFontAndColor(0, &font
);
973 if (!IsEditable() || IsEnabled())
974 textColor
= ui_color(B_DOCUMENT_TEXT_COLOR
);
976 textColor
= tint_color(ui_color(B_PANEL_BACKGROUND_COLOR
),
977 B_DISABLED_LABEL_TINT
);
980 fTextView
->SetFontAndColor(&font
, B_FONT_ALL
, &textColor
);
984 color
= ui_color(B_PANEL_BACKGROUND_COLOR
);
985 else if (IsEnabled())
986 color
= ui_color(B_DOCUMENT_BACKGROUND_COLOR
);
988 color
= tint_color(ui_color(B_PANEL_BACKGROUND_COLOR
),
992 fTextView
->SetViewColor(color
);
993 fTextView
->SetLowColor(color
);