1//--------------------------------------------------------------------
2//
3//	MenuView.cpp
4//
5//	Written by: Owen Smith
6//
7//--------------------------------------------------------------------
8
9/*
10	Copyright 1999, Be Incorporated.   All Rights Reserved.
11	This file may be used under the terms of the Be Sample Code License.
12*/
13
14#include <Alert.h>
15#include <Button.h>
16#include <CheckBox.h>
17#include <Menu.h>
18#include <MenuItem.h>
19#include <OutlineListView.h>
20#include <ScrollView.h>
21#include <TextControl.h>
22#include <List.h>
23#include <string.h>
24
25#include "constants.h"
26#include "MenuView.h"
27#include "MenuWindow.h"
28#include "PostDispatchInvoker.h"
29#include "stddlg.h"
30#include "ViewLayoutFactory.h"
31
32
33//====================================================================
34//	MenuView Implementation
35
36
37//--------------------------------------------------------------------
38//	MenuView constructors, destructors, operators
39
40MenuView::MenuView(uint32 resizingMode)
41	: BView(BRect(0, 0, 0, 0), "Menu View", resizingMode,
42		B_WILL_DRAW)
43{
44	ViewLayoutFactory aFactory; // for semi-intelligent layout
45
46	SetViewColor(BKG_GREY);
47
48	// "hide user menus" check box
49	float fCheck_x = 20.0f;
50	float fCheck_y = 100.0f;
51	m_pHideUserCheck = aFactory.MakeCheckBox("Hide User Menus",
52		STR_HIDE_USER_MENUS, MSG_WIN_HIDE_USER_MENUS,
53		BPoint(fCheck_x, fCheck_y));
54	m_pHideUserCheck->SetValue(B_CONTROL_OFF);
55	AddChild(m_pHideUserCheck);
56
57	// "large test icons" check box
58	fCheck_y = m_pHideUserCheck->Frame().bottom + 10;
59	m_pLargeTestIconCheck = aFactory.MakeCheckBox("Large Test Icons",
60		STR_LARGE_TEST_ICONS, MSG_WIN_LARGE_TEST_ICONS,
61		BPoint(fCheck_x, fCheck_y));
62	m_pLargeTestIconCheck->SetValue(B_CONTROL_OFF);
63	AddChild(m_pLargeTestIconCheck);
64
65	// "add menu", "add item", "delete" buttons
66	BList buttons;
67	float fButton_x = m_pHideUserCheck->Frame().right + 15;
68	float fButton_y = m_pHideUserCheck->Frame().top;
69
70	m_pAddMenuButton = aFactory.MakeButton("Add Menu Bar", STR_ADD_MENU,
71		MSG_VIEW_ADD_MENU, BPoint(fButton_x, fButton_y));
72	AddChild(m_pAddMenuButton);
73	buttons.AddItem(m_pAddMenuButton);
74
75	// for purposes of size calculation, use the longest piece
76	// of text we're going to stuff into the button
77	const char* addItemText;
78	float itemLen, sepLen;
79	itemLen = be_plain_font->StringWidth(STR_ADD_ITEM);
80	sepLen = be_plain_font->StringWidth(STR_ADD_SEP);
81	addItemText = (itemLen > sepLen) ? STR_ADD_ITEM : STR_ADD_SEP;
82	m_pAddItemButton = aFactory.MakeButton("Add Item To Menu",
83		addItemText, MSG_VIEW_ADD_ITEM,
84		BPoint(fButton_x, fButton_y));
85	m_pAddItemButton->SetEnabled(false);
86	AddChild(m_pAddItemButton);
87	buttons.AddItem(m_pAddItemButton);
88
89	m_pDelButton = aFactory.MakeButton("Delete Menu Bar", STR_DELETE_MENU,
90		MSG_VIEW_DELETE_MENU, BPoint(fButton_x, fButton_y));
91	m_pDelButton->SetEnabled(false);
92	AddChild(m_pDelButton);
93	buttons.AddItem(m_pDelButton);
94
95	// now resize list of buttons to max width
96	aFactory.ResizeToListMax(buttons, RECT_WIDTH);
97
98	// now align buttons along left side
99	aFactory.Align(buttons, ALIGN_LEFT,
100		m_pAddItemButton->Frame().Height() + 15);
101
102	// now make add menu the default, AFTER we've
103	// laid out the buttons, since the button bloats
104	// when it's the default
105	m_pAddMenuButton->MakeDefault(true);
106
107	// item "label" control
108	float fEdit_left = 20.0f, fEdit_bottom = m_pHideUserCheck->Frame().top - 20;
109	float fEdit_right = m_pAddItemButton->Frame().right;
110
111	m_pLabelCtrl = aFactory.MakeTextControl("Menu Bar Control",
112		STR_LABEL_CTRL, NULL, BPoint(fEdit_left, fEdit_bottom),
113		fEdit_right - fEdit_left, CORNER_BOTTOMLEFT);
114	AddChild(m_pLabelCtrl);
115
116	// menu outline view
117	BRect r;
118	r.left = m_pAddItemButton->Frame().right + 30;
119	r.top = 20.0f;
120	r.right = r.left + 200 - B_V_SCROLL_BAR_WIDTH;
121	// API quirk here: <= 12 (hscroll height - 2) height
122	// results in scrollbar drawing error
123	r.bottom = r.top + 100 - B_H_SCROLL_BAR_HEIGHT;
124
125	m_pMenuOutlineView = new BOutlineListView(r, "Menu Outline",
126		B_SINGLE_SELECTION_LIST, B_FOLLOW_ALL);
127	m_pMenuOutlineView->SetSelectionMessage(
128		new BMessage(MSG_MENU_OUTLINE_SEL));
129
130	// wrap outline view in scroller
131	m_pScrollView = new BScrollView("Menu Outline Scroller",
132		m_pMenuOutlineView, B_FOLLOW_LEFT | B_FOLLOW_TOP, 0,
133		true, true);
134	m_pScrollView->SetViewColor(BKG_GREY);
135	AddChild(m_pScrollView);
136
137}
138
139
140
141//--------------------------------------------------------------------
142//	MenuView virtual function overrides
143
144void MenuView::MessageReceived(BMessage* message)
145{
146	switch (message->what) {
147	case MSG_VIEW_ADD_MENU:
148		AddMenu(message);
149		break;
150	case MSG_VIEW_DELETE_MENU:
151		DeleteMenu(message);
152		break;
153	case MSG_VIEW_ADD_ITEM:
154		AddMenuItem(message);
155		break;
156	case MSG_MENU_OUTLINE_SEL:
157		MenuSelectionChanged(message);
158		break;
159	case MSG_LABEL_EDIT:
160		SetButtonState();
161		break;
162	default:
163		BView::MessageReceived(message);
164		break;
165	}
166}
167
168void MenuView::AllAttached(void)
169{
170	if (! Valid()) {
171		return;
172	}
173
174	// Set button and view targets (now that window looper is available).
175	m_pAddMenuButton->SetTarget(this);
176	m_pDelButton->SetTarget(this);
177	m_pAddItemButton->SetTarget(this);
178	m_pMenuOutlineView->SetTarget(this);
179
180	// Set button's initial state
181	SetButtonState();
182
183	// Wrap a message filter/invoker around the label control's text field
184	// that will post us B_LABEL_EDIT msg after the text field processes a
185	// B_KEY_DOWN message.
186	m_pLabelCtrl->TextView()->AddFilter(
187		new PostDispatchInvoker(B_KEY_DOWN, new BMessage(MSG_LABEL_EDIT), this));
188
189	// Get one item's height by adding a dummy item to
190	// the BOutlineListView and calculating its height.
191	m_pMenuOutlineView->AddItem(new BStringItem("Dummy"));
192	float itemHeight = m_pMenuOutlineView->ItemFrame(0).Height();
193	itemHeight++;	// account for 1-pixel offset between items
194					// which eliminates overlap
195	delete m_pMenuOutlineView->RemoveItem((int32)0);
196
197	// Resize outline list view to integral item height.
198	// fudge factor of 4 comes from 2-pixel space between
199	// scrollview and outline list view on top & bottom
200	float viewHeight = 16*itemHeight;
201	m_pScrollView->ResizeTo(m_pScrollView->Frame().Width(),
202		viewHeight + B_H_SCROLL_BAR_HEIGHT + 4);
203	BScrollBar *pBar = m_pScrollView->ScrollBar(B_HORIZONTAL);
204	if (pBar) {
205		pBar->SetRange(0, 300);
206	}
207
208	// Resize view to surround contents.
209	ViewLayoutFactory aFactory;
210	aFactory.ResizeAroundChildren(*this, BPoint(20,20));
211}
212
213
214
215//--------------------------------------------------------------------
216//	MenuView operations
217
218void MenuView::PopulateUserMenu(BMenu* pMenu, int32 index)
219{
220	if ((! pMenu) || (! Valid())) {
221		return;
222	}
223
224	// blow away all menu items
225	BMenuItem* pMenuItem = pMenu->RemoveItem((int32)0);
226	while(pMenuItem) {
227		delete pMenuItem;
228		pMenuItem = pMenu->RemoveItem((int32)0);
229	}
230
231	// build menu up from outline list view
232	BListItem* pListItem = m_pMenuOutlineView->ItemUnderAt(NULL, true, index);
233
234	// recursive buildup of menu items
235	BuildMenuItems(pMenu, pListItem, m_pMenuOutlineView);
236}
237
238
239
240//--------------------------------------------------------------------
241//	MenuView message handlers
242
243void MenuView::AddMenu(BMessage* message)
244{
245	if (! Valid()) {
246		return;
247	}
248
249	const char* menuName = m_pLabelCtrl->Text();
250	if ((! menuName) || (! *menuName)) {
251		BAlert* pAlert = new BAlert("Add menu alert",
252			"Please specify the menu name first.", "OK");
253		pAlert->Go();
254		return;
255	}
256
257	m_pMenuOutlineView->AddItem(new BStringItem(menuName));
258
259	// add some info and pass to window
260	BMessage newMsg(MSG_WIN_ADD_MENU);
261	newMsg.AddString("Menu Name", menuName);
262	BWindow* pWin = Window();
263	if (pWin) {
264		pWin->PostMessage(&newMsg);
265	}
266
267	// Reset the label control and buttons
268	m_pLabelCtrl->SetText("");
269	SetButtonState();
270}
271
272void MenuView::DeleteMenu(BMessage* message)
273{
274	if (! Valid())
275		return;
276
277	int32 itemCount;
278	int32 selected = m_pMenuOutlineView->CurrentSelection();
279	if (selected < 0)
280		return;
281
282	BStringItem* pSelItem = dynamic_cast<BStringItem*>
283		(m_pMenuOutlineView->ItemAt(selected));
284	if (! pSelItem)
285		return;
286
287	if (pSelItem->OutlineLevel() == 0) {
288		// get index of item among items in 0 level
289		itemCount = m_pMenuOutlineView->CountItemsUnder(NULL, true);
290		int32 i;
291		for (i=0; i<itemCount; i++) {
292			BListItem* pItem = m_pMenuOutlineView->ItemUnderAt(NULL, true, i);
293			if (pItem == pSelItem) {
294				break;
295			}
296		}
297
298		// Stuff the index into the message and pass along to
299		// the window.
300		BMessage newMsg(MSG_WIN_DELETE_MENU);
301		newMsg.AddInt32("Menu Index", i);
302		BWindow* pWin = Window();
303		if (pWin) {
304			pWin->PostMessage(&newMsg);
305		}
306	}
307
308	// Find & record all subitems of selection, because we'll
309	// have to delete them after they're removed from the list.
310	BList subItems;
311	int32 j;
312	itemCount = m_pMenuOutlineView->CountItemsUnder(pSelItem, false);
313	for (j=0; j<itemCount; j++) {
314		BListItem* pItem = m_pMenuOutlineView->ItemUnderAt(pSelItem, false, j);
315		subItems.AddItem(pItem);
316	}
317
318	// Note the superitem for reference just below
319	BStringItem* pSuperitem = dynamic_cast<BStringItem*>
320		(m_pMenuOutlineView->Superitem(pSelItem));
321
322	// Remove selected item and all subitems from
323	// the outline list view.
324	m_pMenuOutlineView->RemoveItem(pSelItem); // removes super- and subitems
325
326	// Update window status
327	MenuWindow* pWin = dynamic_cast<MenuWindow*>(Window());
328	if (pWin) {
329		const char* itemName = pSelItem->Text();
330		if (strcmp(itemName, STR_SEPARATOR)) {
331			pWin->UpdateStatus(STR_STATUS_DELETE_ITEM, itemName);
332		} else {
333			pWin->UpdateStatus(STR_STATUS_DELETE_SEPARATOR);
334		}
335	}
336
337	// Delete the selected item and all subitems
338	for (j=0; j<itemCount; j++) {
339		BListItem* pItem = reinterpret_cast<BListItem*>(subItems.ItemAt(j));
340		delete pItem; // deletes subitems
341	}
342	delete pSelItem; // deletes superitem
343
344	if (pSuperitem) {
345		// There's a bug in outline list view which does not
346		// update the superitem correctly. The only way to do so
347		// is to remove and add a brand-new superitem afresh.
348		if (! m_pMenuOutlineView->CountItemsUnder(pSuperitem, true))
349		{
350			int32 index = m_pMenuOutlineView->FullListIndexOf(pSuperitem);
351			m_pMenuOutlineView->RemoveItem(pSuperitem);
352			BStringItem* pCloneItem = new BStringItem(
353				pSuperitem->Text(), pSuperitem->OutlineLevel());
354			m_pMenuOutlineView->AddItem(pCloneItem, index);
355			delete pSuperitem;
356		}
357	}
358}
359
360void MenuView::AddMenuItem(BMessage* message)
361{
362	if (! Valid()) {
363		return;
364	}
365
366	// Add item to outline list view but DON'T update the
367	// window right away; the actual menu items will be
368	// created dynamically later on.
369	int32 selected = m_pMenuOutlineView->CurrentSelection();
370	if (selected >= 0) {
371		BListItem* pSelItem = m_pMenuOutlineView->ItemAt(selected);
372		if (pSelItem) {
373
374			int32 level = pSelItem->OutlineLevel() + 1;
375			int32 index = m_pMenuOutlineView->FullListIndexOf(pSelItem)
376				+ m_pMenuOutlineView->CountItemsUnder(pSelItem, false) + 1;
377			const char* itemName = m_pLabelCtrl->Text();
378			bool bIsSeparator = IsSeparator(itemName);
379
380			if (bIsSeparator) {
381				m_pMenuOutlineView->AddItem(new BStringItem(STR_SEPARATOR, level),
382					index);
383			} else {
384				m_pMenuOutlineView->AddItem(new BStringItem(itemName, level),
385					index);
386			}
387
388			MenuWindow* pWin = dynamic_cast<MenuWindow*>(Window());
389			if (pWin) {
390				if (! bIsSeparator) {
391					pWin->UpdateStatus(STR_STATUS_ADD_ITEM, itemName);
392				} else {
393					pWin->UpdateStatus(STR_STATUS_ADD_SEPARATOR);
394				}
395			}
396
397			m_pMenuOutlineView->Invalidate();	// outline view doesn't
398												// invalidate correctly
399
400			// Reset the label control the buttons
401			m_pLabelCtrl->SetText("");
402			SetButtonState();
403		}
404	}
405}
406
407void MenuView::MenuSelectionChanged(BMessage* message)
408{
409	SetButtonState();
410}
411
412
413
414//--------------------------------------------------------------------
415//	MenuView implementation member functions
416
417void MenuView::BuildMenuItems(BMenu* pMenu, BListItem* pSuperitem,
418	BOutlineListView* pView)
419{
420	if ((! pMenu) || (! pSuperitem) || (! pView)) {
421		return;
422	}
423
424	int32 len = pView->CountItemsUnder(pSuperitem, true);
425	if (len == 0) {
426		BMenuItem* pEmptyItem = new BMenuItem(STR_MNU_EMPTY_ITEM, NULL);
427		pEmptyItem->SetEnabled(false);
428		pMenu->AddItem(pEmptyItem);
429	}
430
431	for (int32 i=0; i<len; i++) {
432		BStringItem* pItem = dynamic_cast<BStringItem*>
433			(pView->ItemUnderAt(pSuperitem, true, i));
434		if (pItem) {
435			if (pView->CountItemsUnder(pItem, true) > 0) {
436				// add a submenu & fill it
437				BMenu* pNewMenu = new BMenu(pItem->Text());
438				BuildMenuItems(pNewMenu, pItem, pView);
439				pMenu->AddItem(pNewMenu);
440			} else {
441				if (strcmp(pItem->Text(), STR_SEPARATOR)) {
442					// add a string item
443					BMessage* pMsg = new BMessage(MSG_USER_ITEM);
444					pMsg->AddString("Item Name", pItem->Text());
445					pMenu->AddItem(new BMenuItem(pItem->Text(), pMsg));
446				} else {
447					// add a separator item
448					pMenu->AddItem(new BSeparatorItem());
449				}
450			}
451		}
452	}
453}
454
455bool MenuView::IsSeparator(const char* text) const
456{
457	if (! text) {
458		return true;
459	}
460
461	if (! *text) {
462		return true;
463	}
464
465	int32 len = strlen(text);
466	for (int32 i = 0; i < len; i++) {
467		char ch = text[i];
468		if ((ch != ' ') && (ch != '-')) {
469			return false;
470		}
471	}
472
473	return true;
474}
475
476void MenuView::SetButtonState(void)
477{
478	if (! Valid()) {
479		return;
480	}
481
482	// See if an item in the outline list is selected
483	int32 index = m_pMenuOutlineView->CurrentSelection();
484	bool bIsSelected = (index >= 0);
485
486	// If an item is selected, see if the selected
487	// item is a separator
488	bool bSeparatorSelected = false;
489	if (bIsSelected) {
490		BStringItem* pItem = dynamic_cast<BStringItem*>
491			(m_pMenuOutlineView->ItemAt(index));
492		if (pItem) {
493			bSeparatorSelected = (! strcmp(pItem->Text(), STR_SEPARATOR));
494		}
495	}
496
497	// Delete: enable if anything is selected
498	m_pDelButton->SetEnabled(bIsSelected);
499
500	// Add Item: enable if anything but a separator is selected
501	bool bEnableAddItem = bIsSelected && (! bSeparatorSelected);
502	m_pAddItemButton->SetEnabled(bEnableAddItem);
503
504	// Add Menu: enable if there's any text in the label field
505	const char* labelText = m_pLabelCtrl->Text();
506	m_pAddMenuButton->SetEnabled(labelText && (*labelText));
507
508	// Add Item: text says Add Separator if button is enabled
509	// and the label field contains separator text,
510	// Add Item otherwise
511	const char* itemText;
512	if (bEnableAddItem && IsSeparator(labelText)) {
513		itemText = STR_ADD_SEP;
514	} else {
515		itemText = STR_ADD_ITEM;
516	}
517	m_pAddItemButton->SetLabel(itemText);
518
519	// If add item button is enabled, it should be the default
520	if (bEnableAddItem) {
521		m_pAddItemButton->MakeDefault(true);
522	} else {
523		m_pAddMenuButton->MakeDefault(true);
524	}
525}
526
527bool MenuView::Valid(void)
528{
529	if (! m_pLabelCtrl) {
530		ierror(STR_NO_LABEL_CTRL);
531		return false;
532	}
533	if (! m_pHideUserCheck) {
534		ierror(STR_NO_HIDE_USER_CHECK);
535		return false;
536	}
537	if (! m_pLargeTestIconCheck) {
538		ierror(STR_NO_LARGE_ICON_CHECK);
539		return false;
540	}
541	if (! m_pAddMenuButton) {
542		ierror(STR_NO_ADDMENU_BUTTON);
543		return false;
544	}
545	if (! m_pAddItemButton) {
546		ierror(STR_NO_ADDITEM_BUTTON);
547		return false;
548	}
549	if (! m_pDelButton) {
550		ierror(STR_NO_DELETE_BUTTON);
551		return false;
552	}
553	if (! m_pMenuOutlineView) {
554		ierror(STR_NO_MENU_OUTLINE);
555		return false;
556	}
557	if (! m_pScrollView) {
558		ierror(STR_NO_MENU_SCROLL_VIEW);
559		return false;
560	}
561	return true;
562}
563
564