PageRenderTime 57ms CodeModel.GetById 13ms app.highlight 38ms RepoModel.GetById 1ms app.codeStats 0ms

/indra/newview/lltoastnotifypanel.cpp

https://bitbucket.org/lindenlab/viewer-beta/
C++ | 602 lines | 434 code | 69 blank | 99 comment | 80 complexity | d2c143ee18a848199eb8afdedaa144d9 MD5 | raw file
  1/**
  2 * @file lltoastnotifypanel.cpp
  3 * @brief Panel for notify toasts.
  4 *
  5 * $LicenseInfo:firstyear=2001&license=viewerlgpl$
  6 * Second Life Viewer Source Code
  7 * Copyright (C) 2010, Linden Research, Inc.
  8 * 
  9 * This library is free software; you can redistribute it and/or
 10 * modify it under the terms of the GNU Lesser General Public
 11 * License as published by the Free Software Foundation;
 12 * version 2.1 of the License only.
 13 * 
 14 * This library is distributed in the hope that it will be useful,
 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 17 * Lesser General Public License for more details.
 18 * 
 19 * You should have received a copy of the GNU Lesser General Public
 20 * License along with this library; if not, write to the Free Software
 21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 22 * 
 23 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 24 * $/LicenseInfo$
 25 */
 26
 27#include "llviewerprecompiledheaders.h"
 28
 29#include "lltoastnotifypanel.h"
 30
 31// project includes
 32#include "llviewercontrol.h"
 33
 34// library includes
 35#include "lldbstrings.h"
 36#include "lllslconstants.h"
 37#include "llnotifications.h"
 38#include "lluiconstants.h"
 39#include "llrect.h"
 40#include "lltrans.h"
 41#include "llnotificationsutil.h"
 42#include "llviewermessage.h"
 43#include "llimfloater.h"
 44
 45const S32 BOTTOM_PAD = VPAD * 3;
 46const S32 IGNORE_BTN_TOP_DELTA = 3*VPAD;//additional ignore_btn padding
 47S32 BUTTON_WIDTH = 90;
 48
 49//static
 50const LLFontGL* LLToastNotifyPanel::sFont = NULL;
 51const LLFontGL* LLToastNotifyPanel::sFontSmall = NULL;
 52
 53LLToastNotifyPanel::button_click_signal_t LLToastNotifyPanel::sButtonClickSignal;
 54
 55LLToastNotifyPanel::LLToastNotifyPanel(LLNotificationPtr& notification, const LLRect& rect, bool show_images) : 
 56LLToastPanel(notification),
 57mTextBox(NULL),
 58mInfoPanel(NULL),
 59mControlPanel(NULL),
 60mNumOptions(0),
 61mNumButtons(0),
 62mAddedDefaultBtn(false),
 63mCloseNotificationOnDestroy(true)
 64{
 65	buildFromFile( "panel_notification.xml");
 66	if(rect != LLRect::null)
 67	{
 68		this->setShape(rect);
 69	}		 
 70	mInfoPanel = getChild<LLPanel>("info_panel");
 71	mControlPanel = getChild<LLPanel>("control_panel");
 72	BUTTON_WIDTH = gSavedSettings.getS32("ToastButtonWidth");
 73	// customize panel's attributes
 74	// is it intended for displaying a tip?
 75	mIsTip = notification->getType() == "notifytip";
 76	// is it a script dialog?
 77	mIsScriptDialog = (notification->getName() == "ScriptDialog" || notification->getName() == "ScriptDialogGroup");
 78	// is it a caution?
 79	//
 80	// caution flag can be set explicitly by specifying it in the notification payload, or it can be set implicitly if the
 81	// notify xml template specifies that it is a caution
 82	// tip-style notification handle 'caution' differently -they display the tip in a different color
 83	mIsCaution = notification->getPriority() >= NOTIFICATION_PRIORITY_HIGH;
 84
 85	// setup parameters
 86	// get a notification message
 87	mMessage = notification->getMessage();
 88	// init font variables
 89	if (!sFont)
 90	{
 91		sFont = LLFontGL::getFontSansSerif();
 92		sFontSmall = LLFontGL::getFontSansSerifSmall();
 93	}
 94	// initialize
 95	setFocusRoot(!mIsTip);
 96	// get a form for the notification
 97	LLNotificationFormPtr form(notification->getForm());
 98	// get number of elements
 99	mNumOptions = form->getNumElements();
100
101	// customize panel's outfit
102	// preliminary adjust panel's layout
103	//move to the end 
104	//mIsTip ? adjustPanelForTipNotice() : adjustPanelForScriptNotice(form);
105
106	// adjust text options according to the notification type
107	// add a caution textbox at the top of a caution notification
108	if (mIsCaution && !mIsTip)
109	{
110		mTextBox = getChild<LLTextBox>("caution_text_box");
111	}
112	else
113	{
114		mTextBox = getChild<LLTextEditor>("text_editor_box"); 
115	}
116
117	// *TODO: magic numbers(???) - copied from llnotify.cpp(250)
118	const S32 MAX_LENGTH = 512 + 20 + DB_FIRST_NAME_BUF_SIZE + DB_LAST_NAME_BUF_SIZE + DB_INV_ITEM_NAME_BUF_SIZE; 
119
120	mTextBox->setMaxTextLength(MAX_LENGTH);
121	mTextBox->setVisible(TRUE);
122	mTextBox->setPlainText(!show_images);
123	mTextBox->setValue(notification->getMessage());
124
125	// add buttons for a script notification
126	if (mIsTip)
127	{
128		adjustPanelForTipNotice();
129	}
130	else
131	{
132		std::vector<index_button_pair_t> buttons;
133		buttons.reserve(mNumOptions);
134		S32 buttons_width = 0;
135		// create all buttons and accumulate they total width to reshape mControlPanel
136		for (S32 i = 0; i < mNumOptions; i++)
137		{
138			LLSD form_element = form->getElement(i);
139			if (form_element["type"].asString() != "button")
140			{
141				// not a button.
142				continue;
143			}
144			if (form_element["name"].asString() == TEXTBOX_MAGIC_TOKEN)
145			{
146				// a textbox pretending to be a button.
147				continue;
148			}
149			LLButton* new_button = createButton(form_element, TRUE);
150			buttons_width += new_button->getRect().getWidth();
151			S32 index = form_element["index"].asInteger();
152			buttons.push_back(index_button_pair_t(index,new_button));
153		}
154		if (buttons.empty())
155		{
156			addDefaultButton();
157		}
158		else
159		{
160			const S32 button_panel_width = mControlPanel->getRect().getWidth();// do not change width of the panel
161			S32 button_panel_height = mControlPanel->getRect().getHeight();
162			//try get an average h_pad to spread out buttons
163			S32 h_pad = (button_panel_width - buttons_width) / (S32(buttons.size()));
164			if(h_pad < 2*HPAD)
165			{
166				/*
167				 * Probably it is a scriptdialog toast
168				 * for a scriptdialog toast h_pad can be < 2*HPAD if we have a lot of buttons.
169				 * In last case set default h_pad to avoid heaping of buttons 
170				 */
171				S32 button_per_row = button_panel_width / BUTTON_WIDTH;
172				h_pad = (button_panel_width % BUTTON_WIDTH) / (button_per_row - 1);// -1  because we do not need space after last button in a row   
173				if(h_pad < 2*HPAD) // still not enough space between buttons ?
174				{
175					h_pad = 2*HPAD;
176				}
177			}
178			if (mIsScriptDialog)
179			{
180				// we are using default width for script buttons so we can determinate button_rows
181				//to get a number of rows we divide the required width of the buttons to button_panel_width
182				S32 button_rows = llceil(F32(buttons.size() - 1) * (BUTTON_WIDTH + h_pad) / button_panel_width);
183				//S32 button_rows = (buttons.size() - 1) * (BUTTON_WIDTH + h_pad) / button_panel_width;
184				//reserve one row for the ignore_btn
185				button_rows++;
186				//calculate required panel height for scripdialog notification.
187				button_panel_height = button_rows * (BTN_HEIGHT + VPAD)	+ IGNORE_BTN_TOP_DELTA + BOTTOM_PAD;
188			}
189			else
190			{
191				// in common case buttons can have different widths so we need to calculate button_rows according to buttons_width
192				//S32 button_rows = llceil(F32(buttons.size()) * (buttons_width + h_pad) / button_panel_width);
193				S32 button_rows = llceil(F32((buttons.size() - 1) * h_pad + buttons_width) / button_panel_width);
194				//calculate required panel height 
195				button_panel_height = button_rows * (BTN_HEIGHT + VPAD)	+ BOTTOM_PAD;
196			}
197		
198			// we need to keep min width and max height to make visible all buttons, because width of the toast can not be changed
199			adjustPanelForScriptNotice(button_panel_width, button_panel_height);
200			updateButtonsLayout(buttons, h_pad);
201			// save buttons for later use in disableButtons()
202			mButtons.assign(buttons.begin(), buttons.end());
203		}
204	}
205	// adjust panel's height to the text size
206	mInfoPanel->setFollowsAll();
207	snapToMessageHeight(mTextBox, MAX_LENGTH);
208
209	if(notification->isReusable())
210	{
211		mButtonClickConnection = sButtonClickSignal.connect(
212			boost::bind(&LLToastNotifyPanel::onToastPanelButtonClicked, this, _1, _2));
213
214		if(notification->isRespondedTo())
215		{
216			// User selected an option in toast, now disable required buttons in IM window
217			disableRespondedOptions(notification);
218		}
219	}
220}
221void LLToastNotifyPanel::addDefaultButton()
222{
223	LLSD form_element;
224	form_element.with("name", "OK").with("text", LLTrans::getString("ok")).with("default", true);
225	LLButton* ok_btn = createButton(form_element, FALSE);
226	LLRect new_btn_rect(ok_btn->getRect());
227
228	new_btn_rect.setOriginAndSize(llabs(getRect().getWidth() - BUTTON_WIDTH)/ 2, BOTTOM_PAD,
229			//auto_size for ok button makes it very small, so let's make it wider
230			BUTTON_WIDTH, new_btn_rect.getHeight());
231	ok_btn->setRect(new_btn_rect);
232	addChild(ok_btn, -1);
233	mNumButtons = 1;
234	mAddedDefaultBtn = true;
235}
236LLButton* LLToastNotifyPanel::createButton(const LLSD& form_element, BOOL is_option)
237{
238
239	InstanceAndS32* userdata = new InstanceAndS32;
240	userdata->mSelf = this;
241	userdata->mButtonName = is_option ? form_element["name"].asString() : "";
242
243	mBtnCallbackData.push_back(userdata);
244
245	LLButton::Params p;
246	bool is_ignore_btn = form_element["index"].asInteger() == -1;
247	const LLFontGL* font = is_ignore_btn ? sFontSmall: sFont; // for ignore button in script dialog
248	p.name(form_element["name"].asString());
249	p.label(form_element["text"].asString());
250	p.font(font);
251	p.rect.height = BTN_HEIGHT;
252	p.click_callback.function(boost::bind(&LLToastNotifyPanel::onClickButton, userdata));
253	p.rect.width = BUTTON_WIDTH;
254	p.auto_resize = false;
255	p.follows.flags(FOLLOWS_LEFT | FOLLOWS_BOTTOM);
256	if (mIsCaution)
257	{
258		p.image_color(LLUIColorTable::instance().getColor("ButtonCautionImageColor"));
259		p.image_color_disabled(LLUIColorTable::instance().getColor("ButtonCautionImageColor"));
260	}
261	// for the scriptdialog buttons we use fixed button size. This  is a limit!
262	if (!mIsScriptDialog && font->getWidth(form_element["text"].asString()) > BUTTON_WIDTH)
263	{
264		p.rect.width = 1;
265		p.auto_resize = true;
266	}
267	else if (mIsScriptDialog && is_ignore_btn)
268	{
269		// this is ignore button, make it smaller
270		p.rect.height = BTN_HEIGHT_SMALL;
271		p.rect.width = 1;
272		p.auto_resize = true;
273	}
274	LLButton* btn = LLUICtrlFactory::create<LLButton>(p);
275	mNumButtons++;
276	btn->autoResize();
277	if (form_element["default"].asBoolean())
278	{
279		setDefaultBtn(btn);
280	}
281
282	return btn;
283}
284
285LLToastNotifyPanel::~LLToastNotifyPanel() 
286{
287	mButtonClickConnection.disconnect();
288
289	std::for_each(mBtnCallbackData.begin(), mBtnCallbackData.end(), DeletePointer());
290	if (mCloseNotificationOnDestroy && LLNotificationsUtil::find(mNotification->getID()) != NULL)
291	{
292		// let reusable notification be deleted
293		mNotification->setReusable(false);
294		if (!mNotification->isPersistent())
295		{
296			LLNotifications::getInstance()->cancel(mNotification);
297		}
298	}
299}
300
301void LLToastNotifyPanel::updateButtonsLayout(const std::vector<index_button_pair_t>& buttons, S32 h_pad)
302{
303	S32 left = 0;
304	//reserve place for ignore button
305	S32 bottom_offset = mIsScriptDialog ? (BTN_HEIGHT + IGNORE_BTN_TOP_DELTA + BOTTOM_PAD) : BOTTOM_PAD;
306	S32 max_width = mControlPanel->getRect().getWidth();
307	LLButton* ignore_btn = NULL;
308	LLButton* mute_btn = NULL;
309	for (std::vector<index_button_pair_t>::const_iterator it = buttons.begin(); it != buttons.end(); it++)
310	{
311		if (-2 == it->first)
312		{
313			mute_btn = it->second;
314			continue;
315		}
316		if (it->first == -1)
317		{
318			ignore_btn = it->second;
319			continue;
320		}
321		LLButton* btn = it->second;
322		LLRect btn_rect(btn->getRect());
323		if (left + btn_rect.getWidth() > max_width)// whether there is still some place for button+h_pad in the mControlPanel
324		{
325			// looks like we need to add button to the next row
326			left = 0;
327			bottom_offset += (BTN_HEIGHT + VPAD);
328		}
329		//we arrange buttons from bottom to top for backward support of old script
330		btn_rect.setOriginAndSize(left, bottom_offset, btn_rect.getWidth(),	btn_rect.getHeight());
331		btn->setRect(btn_rect);
332		left = btn_rect.mLeft + btn_rect.getWidth() + h_pad;
333		mControlPanel->addChild(btn, -1);
334	}
335
336	U32 ignore_btn_width = 0;
337	if (mIsScriptDialog && ignore_btn != NULL)
338	{
339		LLRect ignore_btn_rect(ignore_btn->getRect());
340		S32 buttons_per_row = max_width / BUTTON_WIDTH; //assume that h_pad far less than BUTTON_WIDTH
341		S32 ignore_btn_left = buttons_per_row * BUTTON_WIDTH + (buttons_per_row	- 1) * h_pad - ignore_btn_rect.getWidth();
342		if (ignore_btn_left + ignore_btn_rect.getWidth() > max_width)// make sure that the ignore button is in panel
343		{
344			ignore_btn_left = max_width - ignore_btn_rect.getWidth() - 2 * HPAD;
345		}
346		ignore_btn_rect.setOriginAndSize(ignore_btn_left, BOTTOM_PAD,// always move ignore button at the bottom
347				ignore_btn_rect.getWidth(), ignore_btn_rect.getHeight());
348		ignore_btn->setRect(ignore_btn_rect);
349		ignore_btn_width = ignore_btn_rect.getWidth();
350		mControlPanel->addChild(ignore_btn, -1);
351	}
352
353	if (mIsScriptDialog && mute_btn != NULL)
354	{
355		LLRect mute_btn_rect(mute_btn->getRect());
356		S32 buttons_per_row = max_width / BUTTON_WIDTH; //assume that h_pad far less than BUTTON_WIDTH
357		// Place mute (Block) button to the left of the ignore button.
358		S32 mute_btn_left = buttons_per_row * BUTTON_WIDTH + (buttons_per_row	- 1) * h_pad - mute_btn_rect.getWidth() - ignore_btn_width - (h_pad / 2);
359		if (mute_btn_left + mute_btn_rect.getWidth() > max_width) // make sure that the mute button is in panel
360		{
361			mute_btn_left = max_width - mute_btn_rect.getWidth() - 2 * HPAD;
362		}
363		mute_btn_rect.setOriginAndSize(mute_btn_left, BOTTOM_PAD,// always move mute button at the bottom
364				mute_btn_rect.getWidth(), mute_btn_rect.getHeight());
365		mute_btn->setRect(mute_btn_rect);
366		mControlPanel->addChild(mute_btn);
367	}
368}
369
370void LLToastNotifyPanel::adjustPanelForScriptNotice(S32 button_panel_width, S32 button_panel_height)
371{
372	//adjust layout
373	// we need to keep min width and max height to make visible all buttons, because width of the toast can not be changed
374	reshape(getRect().getWidth(), mInfoPanel->getRect().getHeight() + button_panel_height + VPAD);
375	mControlPanel->reshape( button_panel_width, button_panel_height);
376}
377
378void LLToastNotifyPanel::adjustPanelForTipNotice()
379{
380	LLRect info_rect = mInfoPanel->getRect();
381	LLRect this_rect = getRect();
382	//we don't need display ControlPanel for tips because they doesn't contain any buttons. 
383	mControlPanel->setVisible(FALSE);
384	reshape(getRect().getWidth(), mInfoPanel->getRect().getHeight());
385
386	if (mNotification->getPayload().has("respond_on_mousedown")
387		&& mNotification->getPayload()["respond_on_mousedown"] )
388	{
389		mInfoPanel->setMouseDownCallback(
390			boost::bind(&LLNotification::respond,
391						mNotification,
392						mNotification->getResponseTemplate()));
393	}
394}
395
396typedef std::set<std::string> button_name_set_t;
397typedef std::map<std::string, button_name_set_t> disable_button_map_t;
398
399disable_button_map_t initUserGiveItemDisableButtonMap()
400{
401	// see EXT-5905 for disable rules
402
403	disable_button_map_t disable_map;
404	button_name_set_t buttons;
405
406	buttons.insert("Show");
407	disable_map.insert(std::make_pair("Show", buttons));
408
409	buttons.insert("Discard");
410	disable_map.insert(std::make_pair("Discard", buttons));
411
412	buttons.insert("Mute");
413	disable_map.insert(std::make_pair("Mute", buttons));
414
415	return disable_map;
416}
417
418disable_button_map_t initTeleportOfferedDisableButtonMap()
419{
420	disable_button_map_t disable_map;
421	button_name_set_t buttons;
422
423	buttons.insert("Teleport");
424	buttons.insert("Cancel");
425
426	disable_map.insert(std::make_pair("Teleport", buttons));
427	disable_map.insert(std::make_pair("Cancel", buttons));
428
429	return disable_map;
430}
431
432disable_button_map_t initFriendshipOfferedDisableButtonMap()
433{
434	disable_button_map_t disable_map;
435	button_name_set_t buttons;
436
437	buttons.insert("Accept");
438	buttons.insert("Decline");
439
440	disable_map.insert(std::make_pair("Accept", buttons));
441	disable_map.insert(std::make_pair("Decline", buttons));
442
443	return disable_map;
444}
445
446button_name_set_t getButtonDisableList(const std::string& notification_name, const std::string& button_name)
447{
448	static disable_button_map_t user_give_item_disable_map = initUserGiveItemDisableButtonMap();
449	static disable_button_map_t teleport_offered_disable_map = initTeleportOfferedDisableButtonMap();
450	static disable_button_map_t friendship_offered_disable_map = initFriendshipOfferedDisableButtonMap();
451
452	disable_button_map_t::const_iterator it;
453	disable_button_map_t::const_iterator it_end;
454	disable_button_map_t search_map;
455
456	if("UserGiveItem" == notification_name)
457	{
458		search_map = user_give_item_disable_map;
459	}
460	else if("TeleportOffered" == notification_name)
461	{
462		search_map = teleport_offered_disable_map;
463	}
464	else if("OfferFriendship" == notification_name)
465	{
466		search_map = friendship_offered_disable_map;
467	}
468
469	it = search_map.find(button_name);
470	it_end = search_map.end();
471
472	if(it_end != it)
473	{
474		return it->second;
475	}
476	return button_name_set_t();
477}
478
479void LLToastNotifyPanel::disableButtons(const std::string& notification_name, const std::string& selected_button)
480{
481	button_name_set_t buttons = getButtonDisableList(notification_name, selected_button);
482
483	std::vector<index_button_pair_t>::const_iterator it = mButtons.begin();
484	for ( ; it != mButtons.end(); it++)
485	{
486		LLButton* btn = it->second;
487		if(buttons.find(btn->getName()) != buttons.end())
488		{
489			btn->setEnabled(FALSE);
490		}
491	}
492}
493
494// static
495void LLToastNotifyPanel::onClickButton(void* data)
496{
497	InstanceAndS32* self_and_button = (InstanceAndS32*)data;
498	LLToastNotifyPanel* self = self_and_button->mSelf;
499	std::string button_name = self_and_button->mButtonName;
500
501	LLSD response = self->mNotification->getResponseTemplate();
502	if (!self->mAddedDefaultBtn && !button_name.empty())
503	{
504		response[button_name] = true;
505	}
506	
507	bool is_reusable = self->mNotification->isReusable();
508	// When we call respond(), LLOfferInfo will delete itself in inventory_offer_callback(), 
509	// lets copy it while it's still valid.
510	LLOfferInfo* old_info = static_cast<LLOfferInfo*>(self->mNotification->getResponder());
511	LLOfferInfo* new_info = NULL;
512	if(is_reusable && old_info)
513	{
514		new_info = new LLOfferInfo(*old_info);
515		self->mNotification->setResponder(new_info);
516	}
517
518	self->mNotification->respond(response);
519
520	if(is_reusable)
521	{
522		sButtonClickSignal(self->mNotification->getID(), button_name);
523	}
524	else
525	{
526		// disable all buttons
527		self->mControlPanel->setEnabled(FALSE);
528	}
529}
530
531void LLToastNotifyPanel::onToastPanelButtonClicked(const LLUUID& notification_id, const std::string btn_name)
532{
533	if(mNotification->getID() == notification_id)
534	{
535		disableButtons(mNotification->getName(), btn_name);
536	}
537}
538
539void LLToastNotifyPanel::disableRespondedOptions(LLNotificationPtr& notification)
540{
541	LLSD response = notification->getResponse();
542	for (LLSD::map_const_iterator response_it = response.beginMap(); 
543		response_it != response.endMap(); ++response_it)
544	{
545		if (response_it->second.isBoolean() && response_it->second.asBoolean())
546		{
547			// that after multiple responses there can be many pressed buttons
548			// need to process them all
549			disableButtons(notification->getName(), response_it->first);
550		}
551	}
552}
553
554
555//////////////////////////////////////////////////////////////////////////
556
557LLIMToastNotifyPanel::LLIMToastNotifyPanel(LLNotificationPtr& pNotification, const LLUUID& session_id, const LLRect& rect /* = LLRect::null */,
558										   bool show_images /* = true */)
559 : mSessionID(session_id), LLToastNotifyPanel(pNotification, rect, show_images)
560{
561	mTextBox->setFollowsAll();
562}
563
564LLIMToastNotifyPanel::~LLIMToastNotifyPanel()
565{
566	// We shouldn't delete notification when IM floater exists
567	// since that notification will be reused by IM floater.
568	// This may happened when IM floater reloads messages, exactly when user
569	// changes layout of IM chat log(disable/enable plaintext mode).
570	// See EXT-6500
571	LLIMFloater* im_floater = LLIMFloater::findInstance(mSessionID);
572	if (im_floater != NULL && !im_floater->isDead())
573	{
574		mCloseNotificationOnDestroy = false;
575	}
576}
577
578void LLIMToastNotifyPanel::reshape(S32 width, S32 height, BOOL called_from_parent /* = TRUE */)
579{
580	S32 text_height = mTextBox->getTextBoundingRect().getHeight();
581	S32 widget_height = mTextBox->getRect().getHeight();
582	S32 delta = text_height - widget_height;
583	LLRect rc = getRect();
584
585	rc.setLeftTopAndSize(rc.mLeft, rc.mTop, width, height + delta);
586	height = rc.getHeight();
587	width = rc.getWidth();
588
589	bool is_width_changed = width != getRect().getWidth();
590
591	LLToastPanel::reshape(width, height, called_from_parent);
592
593	// Notification height required to display the text message depends on
594	// the width of the text box thus if panel width is changed the text box
595	// width is also changed then reshape() is called to adjust proper height.
596	if (is_width_changed)
597	{
598		reshape(width, height, called_from_parent);
599	}
600}
601
602// EOF