1 /************************************************************************* 2 * 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * Copyright 2000, 2010 Oracle and/or its affiliates. 6 * 7 * OpenOffice.org - a multi-platform office productivity suite 8 * 9 * This file is part of OpenOffice.org. 10 * 11 * OpenOffice.org is free software: you can redistribute it and/or modify 12 * it under the terms of the GNU Lesser General Public License version 3 13 * only, as published by the Free Software Foundation. 14 * 15 * OpenOffice.org is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU Lesser General Public License version 3 for more details 19 * (a copy is included in the LICENSE file that accompanied this code). 20 * 21 * You should have received a copy of the GNU Lesser General Public License 22 * version 3 along with OpenOffice.org. If not, see 23 * <http://www.openoffice.org/license.html> 24 * for a copy of the LGPLv3 License. 25 * 26 ************************************************************************/ 27 28 // MARKER(update_precomp.py): autogen include statement, do not remove 29 #include "precompiled_sw.hxx" 30 #ifdef SW_DLLIMPLEMENTATION 31 #undef SW_DLLIMPLEMENTATION 32 #endif 33 34 35 36 #define _SVSTDARR_STRINGS 37 #include <tools/urlobj.hxx> 38 #include <tools/stream.hxx> 39 #ifndef _MSGBOX_HXX //autogen 40 #include <vcl/msgbox.hxx> 41 #endif 42 #include <vcl/help.hxx> 43 #include <unotools/transliterationwrapper.hxx> 44 #include <unotools/tempfile.hxx> 45 46 #include <svl/svstdarr.hxx> 47 #include <unotools/pathoptions.hxx> 48 #include <swtypes.hxx> 49 #include <glosbib.hxx> 50 #include <gloshdl.hxx> 51 #include <actctrl.hxx> 52 #include <glossary.hxx> 53 #include <glosdoc.hxx> 54 #include <swunohelper.hxx> 55 56 #ifndef _GLOSBIB_HRC 57 #include <glosbib.hrc> 58 #endif 59 #ifndef _MISC_HRC 60 #include <misc.hrc> 61 #endif 62 #ifndef _HELPID_H 63 #include <helpid.h> 64 #endif 65 66 67 #define PATH_CASE_SENSITIVE 0x01 68 #define PATH_READONLY 0x02 69 70 #define RENAME_TOKEN_DELIM (sal_Unicode)1 71 72 /*-----------------09.06.97 13:05------------------- 73 74 --------------------------------------------------*/ 75 SwGlossaryGroupDlg::SwGlossaryGroupDlg(Window * pParent, 76 const SvStrings* pPathArr, 77 SwGlossaryHdl *pHdl) : 78 SvxStandardDialog(pParent, SW_RES(DLG_BIB_BASE)), 79 aBibFT( this, SW_RES(FT_BIB)), 80 aNameED( this, SW_RES(ED_NAME)), 81 aPathFT( this, SW_RES(FT_PATH)), 82 aPathLB( this, SW_RES(LB_PATH)), 83 aSelectFT( this, SW_RES(FT_SELECT)), 84 aGroupTLB( this, SW_RES(TLB_GROUPS)), 85 86 aOkPB( this, SW_RES(BT_OK)), 87 aCancelPB( this, SW_RES(BT_CANCEL)), 88 aHelpPB( this, SW_RES(BT_HELP)), 89 aNewPB( this, SW_RES(PB_NEW)), 90 aDelPB( this, SW_RES(PB_DELETE)), 91 aRenamePB( this, SW_RES(PB_RENAME)), 92 93 pRemovedArr(0), 94 pInsertedArr(0), 95 pRenamedArr(0), 96 pGlosHdl(pHdl) 97 { 98 sal_uInt16 i; 99 100 FreeResource(); 101 102 long nTabs[] = 103 { 2, // Number of Tabs 104 0, 160 105 }; 106 107 aGroupTLB.SetHelpId(HID_GLOS_GROUP_TREE); 108 aGroupTLB.SetTabs( &nTabs[0], MAP_APPFONT ); 109 aGroupTLB.SetStyle(aGroupTLB.GetStyle()|WB_HSCROLL|WB_CLIPCHILDREN|WB_SORT); 110 aGroupTLB.SetSelectHdl(LINK(this, SwGlossaryGroupDlg, SelectHdl)); 111 aGroupTLB.GetModel()->SetSortMode(SortAscending); 112 aNewPB.SetClickHdl(LINK(this, SwGlossaryGroupDlg, NewHdl)); 113 aDelPB.SetClickHdl(LINK(this, SwGlossaryGroupDlg, DeleteHdl)); 114 aNameED.SetModifyHdl(LINK(this, SwGlossaryGroupDlg, ModifyHdl)); 115 aPathLB.SetSelectHdl(LINK(this, SwGlossaryGroupDlg, ModifyHdl)); 116 aRenamePB.SetClickHdl(LINK(this, SwGlossaryGroupDlg, RenameHdl)); 117 for( i = 0; i < pPathArr->Count(); i++) 118 { 119 String sPath(*(*pPathArr)[i]); 120 INetURLObject aTempURL(sPath); 121 sPath = aTempURL.GetMainURL(INetURLObject::DECODE_WITH_CHARSET ); 122 aPathLB.InsertEntry(sPath); 123 sal_uLong nCaseReadonly = 0; 124 utl::TempFile aTempFile(&sPath); 125 aTempFile.EnableKillingFile(); 126 if(!aTempFile.IsValid()) 127 nCaseReadonly |= PATH_READONLY; 128 else if( SWUnoHelper::UCB_IsCaseSensitiveFileName( aTempFile.GetURL())) 129 nCaseReadonly |= PATH_CASE_SENSITIVE; 130 aPathLB.SetEntryData(i, (void*)nCaseReadonly); 131 } 132 aPathLB.SelectEntryPos(0); 133 aPathLB.Enable(sal_True); 134 135 const sal_uInt16 nCount = pHdl->GetGroupCnt(); 136 for(i = 0; i < nCount; ++i) 137 { 138 String sTitle; 139 String sGroup = pHdl->GetGroupName(i, &sTitle); 140 if(!sGroup.Len()) 141 continue; 142 GlosBibUserData* pData = new GlosBibUserData; 143 pData->sGroupName = sGroup; 144 pData->sGroupTitle = sTitle; 145 String sTemp(sTitle); 146 //sGroup.GetToken(0, GLOS_DELIM) 147 sTemp += '\t'; 148 pData->sPath = aPathLB.GetEntry((sal_uInt16)sGroup.GetToken(1, GLOS_DELIM).ToInt32()); 149 sTemp += pData->sPath; 150 SvLBoxEntry* pEntry = aGroupTLB.InsertEntry(sTemp); 151 pEntry->SetUserData(pData); 152 153 } 154 aGroupTLB.GetModel()->Resort(); 155 } 156 157 /*-----------------09.06.97 13:05------------------- 158 159 --------------------------------------------------*/ 160 SwGlossaryGroupDlg::~SwGlossaryGroupDlg() 161 { 162 163 if(pInsertedArr) 164 { 165 pInsertedArr->DeleteAndDestroy(0, pInsertedArr->Count()); 166 delete pInsertedArr; 167 } 168 if(pRemovedArr) 169 { 170 pRemovedArr->DeleteAndDestroy(0, pRemovedArr->Count()); 171 delete pRemovedArr; 172 } 173 if(pRenamedArr) 174 { 175 pRenamedArr->DeleteAndDestroy(0, pRenamedArr->Count()); 176 delete pRenamedArr; 177 } 178 179 } 180 181 /*-----------------09.06.97 13:11------------------- 182 183 --------------------------------------------------*/ 184 185 void __EXPORT SwGlossaryGroupDlg::Apply() 186 { 187 if(aNewPB.IsEnabled()) 188 NewHdl(&aNewPB); 189 190 String aActGroup = SwGlossaryDlg::GetCurrGroup(); 191 192 if(pRemovedArr && pRemovedArr->Count()) 193 { 194 sal_uInt16 nCount = pRemovedArr->Count(); 195 for(sal_uInt16 i = 0; i < nCount; ++i) 196 { 197 const String* pDelEntry = (*pRemovedArr)[i]; 198 const String sDelGroup = pDelEntry->GetToken(0, '\t'); 199 if( sDelGroup == aActGroup ) 200 { 201 //soll die aktuelle Gruppe geloescht werden, muss die akt. Gruppe 202 //umgesetzt werden 203 if(aGroupTLB.GetEntryCount()) 204 { 205 SvLBoxEntry* pFirst = aGroupTLB.First(); 206 GlosBibUserData* pUserData = (GlosBibUserData*)pFirst->GetUserData(); 207 pGlosHdl->SetCurGroup(pUserData->sGroupName); 208 } 209 } 210 String sMsg(SW_RES(STR_QUERY_DELETE_GROUP1)); 211 String sTitle(pDelEntry->GetToken(1, '\t')); 212 if(sTitle.Len()) 213 sMsg += sTitle; 214 else 215 sDelGroup.GetToken(1, GLOS_DELIM); 216 sMsg += SW_RESSTR(STR_QUERY_DELETE_GROUP2); 217 QueryBox aQuery(this->GetParent(), WB_YES_NO|WB_DEF_NO, sMsg ); 218 if(RET_YES == aQuery.Execute()) 219 pGlosHdl->DelGroup( sDelGroup ); 220 } 221 222 } 223 //erst umbenennen, falls es schon eins gab 224 if(pRenamedArr && pRenamedArr->Count()) 225 { 226 sal_uInt16 nCount = pRenamedArr->Count(); 227 for(sal_uInt16 i = 0; i < nCount; ++i) 228 { 229 String * pEntry = (*pRenamedArr)[i]; 230 xub_StrLen nStrSttPos = 0; 231 String sOld( pEntry->GetToken(0, RENAME_TOKEN_DELIM, nStrSttPos ) ); 232 String sNew( pEntry->GetToken(0, RENAME_TOKEN_DELIM, nStrSttPos) ); 233 String sTitle( pEntry->GetToken(0, RENAME_TOKEN_DELIM, nStrSttPos) ); 234 pGlosHdl->RenameGroup(sOld, sNew, sTitle); 235 if(!i) 236 sCreatedGroup = sNew; 237 } 238 } 239 if(pInsertedArr && pInsertedArr->Count()) 240 { 241 sal_uInt16 nCount = pInsertedArr->Count(); 242 for(sal_uInt16 i = 0; i < nCount; ++i) 243 { 244 String sNewGroup = *(*pInsertedArr)[i]; 245 String sNewTitle = sNewGroup.GetToken(0, GLOS_DELIM); 246 if( *(*pInsertedArr)[i] != aActGroup ) 247 { 248 pGlosHdl->NewGroup(sNewGroup, sNewTitle); 249 if(!sCreatedGroup.Len()) 250 sCreatedGroup = sNewGroup; 251 } 252 } 253 } 254 } 255 /*-----------------09.06.97 13:12------------------- 256 257 --------------------------------------------------*/ 258 IMPL_LINK( SwGlossaryGroupDlg, SelectHdl, SvTabListBox*, EMPTYARG ) 259 { 260 aNewPB.Enable(sal_False); 261 SvLBoxEntry* pFirstEntry = aGroupTLB.FirstSelected(); 262 if(pFirstEntry) 263 { 264 GlosBibUserData* pUserData = (GlosBibUserData*)pFirstEntry->GetUserData(); 265 String sEntry(pUserData->sGroupName); 266 String sName(aNameED.GetText()); 267 sal_Bool bExists = sal_False; 268 sal_uLong nPos = aGroupTLB.GetEntryPos(sName, 0); 269 if( 0xffffffff > nPos) 270 { 271 SvLBoxEntry* pEntry = aGroupTLB.GetEntry(nPos); 272 GlosBibUserData* pFoundData = (GlosBibUserData*)pEntry->GetUserData(); 273 String sGroup = pFoundData->sGroupName; 274 bExists = sGroup == sEntry; 275 } 276 277 aRenamePB.Enable(!bExists && sName.Len()); 278 aDelPB.Enable(IsDeleteAllowed(sEntry)); 279 } 280 return 0; 281 } 282 283 /*-----------------09.06.97 13:22------------------- 284 285 --------------------------------------------------*/ 286 IMPL_LINK( SwGlossaryGroupDlg, NewHdl, Button*, EMPTYARG ) 287 { 288 String sGroup(aNameED.GetText()); 289 // sGroup.ToLower(); 290 sGroup += GLOS_DELIM; 291 sGroup += String::CreateFromInt32(aPathLB.GetSelectEntryPos()); 292 DBG_ASSERT(!pGlosHdl->FindGroupName(sGroup), "Gruppe bereits vorhanden!"); 293 if(!pInsertedArr) 294 pInsertedArr = new SvStrings; 295 pInsertedArr->Insert(new String(sGroup), pInsertedArr->Count()); 296 String sTemp(aNameED.GetText()); 297 // sTemp.ToLower(); 298 sTemp += '\t'; 299 sTemp += aPathLB.GetSelectEntry(); 300 SvLBoxEntry* pEntry = aGroupTLB.InsertEntry(sTemp); 301 GlosBibUserData* pData = new GlosBibUserData; 302 pData->sPath = aPathLB.GetSelectEntry(); 303 pData->sGroupName = sGroup; 304 pData->sGroupTitle = aNameED.GetText(); 305 pEntry->SetUserData(pData); 306 aGroupTLB.Select(pEntry); 307 aGroupTLB.MakeVisible(pEntry); 308 aGroupTLB.GetModel()->Resort(); 309 310 return 0; 311 } 312 /*-----------------09.06.97 13:22------------------- 313 314 --------------------------------------------------*/ 315 IMPL_LINK( SwGlossaryGroupDlg, DeleteHdl, Button*, pButton ) 316 { 317 SvLBoxEntry* pEntry = aGroupTLB.FirstSelected(); 318 if(!pEntry) 319 { 320 pButton->Enable(sal_False); 321 return 0; 322 } 323 GlosBibUserData* pUserData = (GlosBibUserData*)pEntry->GetUserData(); 324 String sEntry(pUserData->sGroupName); 325 // befindet sich der zu loeschende Name schon unter den 326 // den neuen - dann weg damit 327 sal_Bool bDelete = sal_True; 328 if(pInsertedArr && pInsertedArr->Count()) 329 { 330 sal_uInt16 nCount = pInsertedArr->Count(); 331 for(sal_uInt16 i = 0; i < nCount; ++i) 332 { 333 const String* pTemp = (*pInsertedArr)[i]; 334 if(*pTemp == sEntry) 335 { 336 pInsertedArr->Remove(i); 337 bDelete = sal_False; 338 break; 339 } 340 341 } 342 } 343 // moeglicherweise sollte es schon umbenannt werden? 344 if(bDelete) 345 { 346 if(pRenamedArr && pRenamedArr->Count()) 347 { 348 sal_uInt16 nCount = pRenamedArr->Count(); 349 for(sal_uInt16 i = 0; i < nCount; ++i) 350 { 351 const String* pTemp = (*pRenamedArr)[i]; 352 String sTemp( pTemp->GetToken(0, RENAME_TOKEN_DELIM )); 353 if(sTemp == sEntry) 354 { 355 pRenamedArr->Remove(i); 356 bDelete = sal_False; 357 break; 358 } 359 } 360 } 361 } 362 if(bDelete) 363 { 364 if(!pRemovedArr) 365 pRemovedArr = new SvStrings; 366 String sGroupEntry(pUserData->sGroupName); 367 sGroupEntry += '\t'; 368 sGroupEntry += pUserData->sGroupTitle; 369 pRemovedArr->Insert(new String(sGroupEntry), pRemovedArr->Count()); 370 } 371 delete pUserData; 372 aGroupTLB.GetModel()->Remove(pEntry); 373 if(!aGroupTLB.First()) 374 pButton->Enable(sal_False); 375 //the content must be deleted - otherwise the new handler would be called in Apply() 376 aNameED.SetText(aEmptyStr); 377 return 0; 378 } 379 380 /* -----------------23.11.98 12:26------------------- 381 * 382 * --------------------------------------------------*/ 383 IMPL_LINK( SwGlossaryGroupDlg, RenameHdl, Button *, EMPTYARG ) 384 { 385 SvLBoxEntry* pEntry = aGroupTLB.FirstSelected(); 386 GlosBibUserData* pUserData = (GlosBibUserData*)pEntry->GetUserData(); 387 String sEntryText(aGroupTLB.GetEntryText(pEntry)); 388 String sEntry(pUserData->sGroupName); 389 390 String sNewName(aNameED.GetText()); 391 String sNewTitle(sNewName); 392 393 sNewName += GLOS_DELIM; 394 sNewName += String::CreateFromInt32(aPathLB.GetSelectEntryPos()); 395 DBG_ASSERT(!pGlosHdl->FindGroupName(sNewName), "Gruppe bereits vorhanden!"); 396 397 // befindet sich der umzubenennende Name unter den 398 // den neuen - dann austauschen 399 sal_Bool bDone = sal_False; 400 if(pInsertedArr && pInsertedArr->Count()) 401 { 402 sal_uInt16 nCount = pInsertedArr->Count(); 403 for(sal_uInt16 i = 0; i < nCount; ++i) 404 { 405 const String* pTemp = (*pInsertedArr)[i]; 406 if(*pTemp == sEntry) 407 { 408 pInsertedArr->Remove(i); 409 pInsertedArr->Insert(new String(sNewName), pInsertedArr->Count()); 410 bDone = sal_True; 411 break; 412 } 413 } 414 } 415 if(!bDone) 416 { 417 if(!pRenamedArr) 418 pRenamedArr = new SvStrings; 419 sEntry += RENAME_TOKEN_DELIM; 420 sEntry += sNewName; 421 sEntry += RENAME_TOKEN_DELIM; 422 sEntry += sNewTitle; 423 pRenamedArr->Insert(new String(sEntry), pRenamedArr->Count()); 424 } 425 delete (GlosBibUserData*)pEntry->GetUserData(); 426 aGroupTLB.GetModel()->Remove(pEntry); 427 String sTemp(aNameED.GetText()); 428 // sTemp.ToLower(); 429 sTemp += '\t'; 430 sTemp += aPathLB.GetSelectEntry(); 431 pEntry = aGroupTLB.InsertEntry(sTemp); 432 GlosBibUserData* pData = new GlosBibUserData; 433 pData->sPath = aPathLB.GetSelectEntry(); 434 pData->sGroupName = sNewName; 435 pData->sGroupTitle = sNewTitle; 436 pEntry->SetUserData(pData); 437 aGroupTLB.Select(pEntry); 438 aGroupTLB.MakeVisible(pEntry); 439 aGroupTLB.GetModel()->Resort(); 440 return 0; 441 } 442 /*-----------------09.06.97 13:42------------------- 443 444 --------------------------------------------------*/ 445 IMPL_LINK( SwGlossaryGroupDlg, ModifyHdl, Edit*, EMPTYARG ) 446 { 447 String sEntry(aNameED.GetText()); 448 // sEntry.ToLower(); 449 sal_Bool bEnableNew = sal_True; 450 sal_Bool bEnableDel = sal_False; 451 sal_uLong nCaseReadonly = 452 (sal_uLong)aPathLB.GetEntryData(aPathLB.GetSelectEntryPos()); 453 sal_Bool bDirReadonly = 0 != (nCaseReadonly&PATH_READONLY); 454 455 if(!sEntry.Len() || bDirReadonly) 456 bEnableNew = sal_False; 457 else if(sEntry.Len()) 458 { 459 sal_uLong nPos = 0xffffffff; 460 461 462 nPos = aGroupTLB.GetEntryPos(sEntry, 0); 463 //ist es nicht case sensitive muss man selbst suchen 464 if( 0xffffffff == nPos) 465 { 466 const ::utl::TransliterationWrapper& rSCmp = GetAppCmpStrIgnore(); 467 for(sal_uInt16 i = 0; i < aGroupTLB.GetEntryCount(); i++) 468 { 469 String sTemp = aGroupTLB.GetEntryText( i, 0 ); 470 nCaseReadonly = (sal_uLong)aPathLB.GetEntryData( 471 aPathLB.GetEntryPos(aGroupTLB.GetEntryText(i,1))); 472 sal_Bool bCase = 0 != (nCaseReadonly & PATH_CASE_SENSITIVE); 473 474 if( !bCase && rSCmp.isEqual( sTemp, sEntry )) 475 { 476 nPos = i; 477 break; 478 } 479 } 480 } 481 if( 0xffffffff > nPos) 482 { 483 bEnableNew = sal_False; 484 aGroupTLB.Select(aGroupTLB.GetEntry( nPos )); 485 aGroupTLB.MakeVisible(aGroupTLB.GetEntry( nPos )); 486 } 487 } 488 SvLBoxEntry* pEntry = aGroupTLB.FirstSelected(); 489 if(pEntry) 490 { 491 GlosBibUserData* pUserData = (GlosBibUserData*)pEntry->GetUserData(); 492 bEnableDel = IsDeleteAllowed(pUserData->sGroupName); 493 494 // String sGroup = aGroupTLB.GetEntryText(pEntry, 0); 495 // sGroup += GLOS_DELIM; 496 // sGroup += String::CreateFromInt32(aPathLB.GetEntryPos(aGroupTLB.GetEntryText(pEntry, 1))); 497 // bEnableDel = IsDeleteAllowed(sGroup); 498 } 499 500 aDelPB.Enable(bEnableDel); 501 aNewPB.Enable(bEnableNew); 502 aRenamePB.Enable(bEnableNew && pEntry); 503 return 0; 504 } 505 506 /*------------------------------------------------------------------------ 507 Beschreibung: 508 ------------------------------------------------------------------------*/ 509 510 sal_Bool SwGlossaryGroupDlg::IsDeleteAllowed(const String &rGroup) 511 { 512 sal_Bool bDel = (!pGlosHdl->IsReadOnly(&rGroup)); 513 514 // OM: befindet sich der Name unter den den neuen Bereichsnamen, 515 // dann ist er auch loeschbar! Bei noch nicht existenten Bereichsnamen 516 // liefert ReadOnly naemlich sal_True. 517 518 if(pInsertedArr && pInsertedArr->Count()) 519 { 520 sal_uInt16 nCount = pInsertedArr->Count(); 521 for(sal_uInt16 i = 0; i < nCount; ++i) 522 { 523 const String* pTemp = (*pInsertedArr)[i]; 524 if(*pTemp == rGroup) 525 { 526 bDel = sal_True; 527 break; 528 } 529 } 530 } 531 532 return bDel; 533 } 534 535 /*-----------------18.07.97 19:06------------------- 536 537 --------------------------------------------------*/ 538 void FEdit::KeyInput( const KeyEvent& rKEvent ) 539 { 540 KeyCode aCode = rKEvent.GetKeyCode(); 541 if( KEYGROUP_CURSOR == aCode.GetGroup() || 542 ( KEYGROUP_MISC == aCode.GetGroup() && 543 KEY_DELETE >= aCode.GetCode() ) || 544 SVT_SEARCHPATH_DELIMITER != rKEvent.GetCharCode() ) 545 Edit::KeyInput( rKEvent ); 546 } 547 /* -----------------------------08.02.00 15:07-------------------------------- 548 549 ---------------------------------------------------------------------------*/ 550 void SwGlossaryGroupTLB::RequestHelp( const HelpEvent& rHEvt ) 551 { 552 Point aPos( ScreenToOutputPixel( rHEvt.GetMousePosPixel() )); 553 SvLBoxEntry* pEntry = GetEntry( aPos ); 554 if(pEntry) 555 { 556 SvLBoxTab* pTab; 557 SvLBoxItem* pItem = GetItem( pEntry, aPos.X(), &pTab ); 558 if(pItem) 559 { 560 aPos = GetEntryPosition( pEntry ); 561 Size aSize(pItem->GetSize( this, pEntry )); 562 aPos.X() = GetTabPos( pEntry, pTab ); 563 564 if((aPos.X() + aSize.Width()) > GetSizePixel().Width()) 565 aSize.Width() = GetSizePixel().Width() - aPos.X(); 566 aPos = OutputToScreenPixel(aPos); 567 Rectangle aItemRect( aPos, aSize ); 568 String sMsg; 569 GlosBibUserData* pData = (GlosBibUserData*)pEntry->GetUserData(); 570 sMsg = pData->sPath; 571 sMsg += INET_PATH_TOKEN; 572 sMsg += pData->sGroupName.GetToken(0, GLOS_DELIM); 573 sMsg += SwGlossaries::GetExtension(); 574 575 Help::ShowQuickHelp( this, aItemRect, sMsg, 576 QUICKHELP_LEFT|QUICKHELP_VCENTER ); 577 } 578 } 579 } 580