1 // RouteWindow.cpp 2 // e.moon 14may99 3 4 #include "RouteApp.h" 5 #include "RouteWindow.h" 6 #include "MediaRoutingView.h" 7 #include "StatusView.h" 8 9 #include "DormantNodeWindow.h" 10 #include "TransportWindow.h" 11 12 #include "RouteAppNodeManager.h" 13 #include "NodeGroup.h" 14 #include "TipManager.h" 15 16 #include <Alert.h> 17 #include <Autolock.h> 18 #include <Debug.h> 19 #include <Font.h> 20 #include <MenuBar.h> 21 #include <Menu.h> 22 #include <MenuItem.h> 23 #include <Message.h> 24 #include <Messenger.h> 25 #include <Roster.h> 26 #include <Screen.h> 27 #include <ScrollView.h> 28 #include <StringView.h> 29 30 #include <algorithm> 31 32 #include "debug_tools.h" 33 #define D_HOOK(x) //PRINT (x) 34 #define D_INTERNAL(x) //PRINT (x) 35 36 __USE_CORTEX_NAMESPACE 37 38 // -------------------------------------------------------- // 39 40 const char* const RouteWindow::s_windowName = "Cortex"; 41 42 const BRect RouteWindow::s_initFrame(100,100,700,550); 43 44 const char* const g_aboutText = 45 "Cortex/Route 2.1.2\n\n" 46 "Copyright 1999-2000 Eric Moon\n" 47 "All rights reserved.\n\n" 48 "The Cortex Team:\n\n" 49 "Christopher Lenz: UI\n" 50 "Eric Moon: UI, back-end\n\n" 51 "Thanks to:\nJohn Ashmun\nJon Watte\nDoug Wright\n<your name here>\n\n" 52 "Certain icons used herein are the property of\n" 53 "Be, Inc. and are used by permission."; 54 55 // -------------------------------------------------------- // 56 // ctor/dtor 57 // -------------------------------------------------------- // 58 59 RouteWindow::~RouteWindow() {} 60 61 RouteWindow::RouteWindow(RouteAppNodeManager* manager) : 62 BWindow(s_initFrame, s_windowName, B_DOCUMENT_WINDOW, 0), 63 m_hScrollBar(0), 64 m_vScrollBar(0), 65 m_transportWindow(0), 66 m_dormantNodeWindow(0), 67 m_selectedGroupID(0), 68 m_zoomed(false), 69 m_zooming(false) { 70 71 BRect b = Bounds(); 72 73 // initialize the menu bar: add all menus that target this window 74 BMenuBar* pMenuBar = new BMenuBar(b, "menuBar"); 75 BMenu* pFileMenu = new BMenu("File"); 76 BMenuItem* item = new BMenuItem( 77 "Open" B_UTF8_ELLIPSIS, 78 new BMessage(RouteApp::M_SHOW_OPEN_PANEL), 79 'O'); 80 item->SetTarget(be_app); 81 pFileMenu->AddItem(item); 82 pFileMenu->AddItem(new BSeparatorItem()); 83 item = new BMenuItem( 84 "Save Nodes" B_UTF8_ELLIPSIS, 85 new BMessage(RouteApp::M_SHOW_SAVE_PANEL), 86 'S'); 87 item->SetTarget(be_app); 88 pFileMenu->AddItem(item); 89 pFileMenu->AddItem(new BSeparatorItem()); 90 pFileMenu->AddItem(new BMenuItem("About Cortex/Route" B_UTF8_ELLIPSIS, 91 new BMessage(B_ABOUT_REQUESTED))); 92 pFileMenu->AddItem(new BSeparatorItem()); 93 pFileMenu->AddItem(new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED))); 94 pMenuBar->AddItem(pFileMenu); 95 AddChild(pMenuBar); 96 97 // build the routing view 98 BRect rvBounds = b; 99 rvBounds.top = pMenuBar->Frame().bottom+1; 100 rvBounds.right -= B_V_SCROLL_BAR_WIDTH; 101 rvBounds.bottom -= B_H_SCROLL_BAR_HEIGHT; 102 m_routingView = new MediaRoutingView( 103 manager, 104 rvBounds, 105 "routingView"); 106 107 BRect hsBounds = rvBounds; 108 hsBounds.left = rvBounds.left + 199; 109 hsBounds.top = hsBounds.bottom + 1; 110 hsBounds.right++; 111 hsBounds.bottom = b.bottom + 1; 112 113 m_hScrollBar = new BScrollBar( 114 hsBounds, 115 "hScrollBar", 116 m_routingView, 117 0, 0, B_HORIZONTAL); 118 AddChild(m_hScrollBar); 119 120 BRect vsBounds = rvBounds; 121 vsBounds.left = vsBounds.right + 1; 122 vsBounds.top--; 123 vsBounds.right = b.right + 1; 124 vsBounds.bottom++; 125 126 m_vScrollBar = new BScrollBar( 127 vsBounds, 128 "vScrollBar", 129 m_routingView, 130 0, 0, B_VERTICAL); 131 AddChild(m_vScrollBar); 132 133 BRect svBounds = rvBounds; 134 svBounds.left -= 1; 135 svBounds.right = hsBounds.left - 1; 136 svBounds.top = svBounds.bottom + 1; 137 svBounds.bottom = b.bottom + 1; 138 139 m_statusView = new StatusView( 140 svBounds, 141 manager, 142 m_hScrollBar); 143 AddChild(m_statusView); 144 145 AddChild(m_routingView); 146 147 float minWidth, maxWidth, minHeight, maxHeight; 148 GetSizeLimits(&minWidth, &maxWidth, &minHeight, &maxHeight); 149 minWidth = m_statusView->Frame().Width() + 6 * B_V_SCROLL_BAR_WIDTH; 150 minHeight = 6 * B_H_SCROLL_BAR_HEIGHT; 151 SetSizeLimits(minWidth, maxWidth, minHeight, maxHeight); 152 153 // construct the Window menu 154 BMenu* windowMenu = new BMenu("Window"); 155 m_transportWindowItem = new BMenuItem( 156 "Show Transport", 157 new BMessage(M_TOGGLE_TRANSPORT_WINDOW)); 158 windowMenu->AddItem(m_transportWindowItem); 159 160 m_dormantNodeWindowItem = new BMenuItem( 161 "Show Add-Ons", 162 new BMessage(M_TOGGLE_DORMANT_NODE_WINDOW)); 163 windowMenu->AddItem(m_dormantNodeWindowItem); 164 165 windowMenu->AddItem(new BSeparatorItem()); 166 167 m_pullPalettesItem = new BMenuItem( 168 "Pull Palettes", 169 new BMessage(M_TOGGLE_PULLING_PALETTES)); 170 windowMenu->AddItem(m_pullPalettesItem); 171 172 pMenuBar->AddItem(windowMenu); 173 174 // create the dormant-nodes palette 175 _toggleDormantNodeWindow(); 176 177 // display group inspector 178 _toggleTransportWindow(); 179 } 180 181 // -------------------------------------------------------- // 182 // operations 183 // -------------------------------------------------------- // 184 185 // enable/disable palette position-locking (when the main 186 // window is moved, all palettes follow) 187 bool RouteWindow::isPullPalettes() const { 188 return m_pullPalettesItem->IsMarked(); 189 190 } 191 void RouteWindow::setPullPalettes( 192 bool enabled) { 193 m_pullPalettesItem->SetMarked(enabled); 194 } 195 196 // [e.moon 2dec99] force window & palettes on-screen 197 void RouteWindow::constrainToScreen() { 198 199 BScreen screen(this); 200 201 const BRect sr = screen.Frame(); 202 203 // [c.lenz 1mar2000] this should be handled by every window 204 // itself. will probably change soon ;-) 205 _constrainToScreen(); 206 /* // main window 207 BRect r = Frame(); 208 BPoint offset(0.0, 0.0); 209 if(r.left < 0.0) 210 offset.x = -r.left; 211 if(r.top < 0.0) 212 offset.y = -r.top; 213 if(r.left >= (sr.right - 20.0)) 214 offset.x -= (r.left - (sr.Width()/2)); 215 if(r.top >= (sr.bottom - 20.0)) 216 offset.y -= (r.top - (sr.Height()/2)); 217 if(offset.x != 0.0 || offset.y != 0.0) { 218 setPullPalettes(false); 219 MoveBy(offset.x, offset.y); 220 }*/ 221 222 // transport window 223 BPoint offset = BPoint(0.0, 0.0); 224 BRect r = (m_transportWindow) ? 225 m_transportWindow->Frame() : 226 m_transportWindowFrame; 227 if(r.left < 0.0) 228 offset.x = (sr.Width()*.75) - r.left; 229 if(r.top < 0.0) 230 offset.y = (sr.Height()*.25) - r.top; 231 if(r.left >= (sr.right - 20.0)) 232 offset.x -= (r.left - (sr.Width()/2)); 233 if(r.top >= (sr.bottom - 20.0)) 234 offset.y -= (r.top - (sr.Height()/2)); 235 236 if(offset.x != 0.0 || offset.y != 0.0) { 237 if(m_transportWindow) 238 m_transportWindow->MoveBy(offset.x, offset.y); 239 else 240 m_transportWindowFrame.OffsetBy(offset.x, offset.y); 241 } 242 243 // addon palette 244 offset = BPoint(0.0, 0.0); 245 r = (m_dormantNodeWindow) ? 246 m_dormantNodeWindow->Frame() : 247 m_dormantNodeWindowFrame; 248 if(r.left < 0.0) 249 offset.x = (sr.Width()*.25) - r.left; 250 if(r.top < 0.0) 251 offset.y = (sr.Height()*.125) - r.top; 252 if(r.left >= (sr.right - 20.0)) 253 offset.x -= (r.left - (sr.Width()/2)); 254 if(r.top >= (sr.bottom - 20.0)) 255 offset.y -= (r.top - (sr.Height()/2)); 256 257 if(offset.x != 0.0 || offset.y != 0.0) { 258 if(m_dormantNodeWindow) 259 m_dormantNodeWindow->MoveBy(offset.x, offset.y); 260 else 261 m_dormantNodeWindowFrame.OffsetBy(offset.x, offset.y); 262 } 263 264 } 265 266 // -------------------------------------------------------- // 267 // BWindow impl 268 // -------------------------------------------------------- // 269 270 // [e.moon 17nov99] 'palette-pulling' impl 271 void RouteWindow::FrameMoved( 272 BPoint point) { 273 274 // ignore notification if the window isn't yet visible 275 if(IsHidden()) 276 return; 277 278 BPoint delta = point - m_lastFramePosition; 279 m_lastFramePosition = point; 280 281 if(m_pullPalettesItem->IsMarked()) { 282 _movePalettesBy(delta.x, delta.y); 283 } 284 } 285 286 // [c.lenz 1mar2000] added for better Zoom() support 287 void RouteWindow::FrameResized( 288 float width, 289 float height) { 290 D_HOOK(("RouteWindow::FrameResized()\n")); 291 292 if (!m_zooming) { 293 m_zoomed = false; 294 } 295 else { 296 m_zooming = false; 297 } 298 } 299 300 bool RouteWindow::QuitRequested() { 301 302 be_app->PostMessage(B_QUIT_REQUESTED); 303 return false; // [e.moon 20oct99] app now quits window 304 } 305 306 // [c.lenz 1mar2000] resize to MediaRoutingView's preferred size 307 void RouteWindow::Zoom( 308 BPoint origin, 309 float width, 310 float height) { 311 D_HOOK(("RouteWindow::Zoom()\n")); 312 313 m_zooming = true; 314 315 BScreen screen(this); 316 if (!screen.Frame().Contains(Frame())) { 317 m_zoomed = false; 318 } 319 320 if (!m_zoomed) { 321 // resize to the ideal size 322 m_manualSize = Bounds(); 323 float width, height; 324 m_routingView->GetPreferredSize(&width, &height); 325 width += B_V_SCROLL_BAR_WIDTH; 326 height += B_H_SCROLL_BAR_HEIGHT; 327 if (KeyMenuBar()) { 328 height += KeyMenuBar()->Frame().Height(); 329 } 330 ResizeTo(width, height); 331 _constrainToScreen(); 332 m_zoomed = true; 333 } 334 else { 335 // resize to the most recent manual size 336 ResizeTo(m_manualSize.Width(), m_manualSize.Height()); 337 m_zoomed = false; 338 } 339 } 340 341 // -------------------------------------------------------- // 342 // BHandler impl 343 // -------------------------------------------------------- // 344 345 void RouteWindow::MessageReceived(BMessage* pMsg) { 346 // PRINT(( 347 // "RouteWindow::MessageReceived()\n")); 348 // pMsg->PrintToStream(); 349 // 350 switch(pMsg->what) { 351 case B_ABOUT_REQUESTED: 352 (new BAlert("About", g_aboutText, "Ok"))->Go(); 353 break; 354 355 case MediaRoutingView::M_GROUP_SELECTED: 356 _handleGroupSelected(pMsg); 357 break; 358 359 case MediaRoutingView::M_SHOW_ERROR_MESSAGE: 360 _handleShowErrorMessage(pMsg); 361 break; 362 363 case M_TOGGLE_TRANSPORT_WINDOW: 364 _toggleTransportWindow(); 365 break; 366 367 case M_REFRESH_TRANSPORT_SETTINGS: 368 _refreshTransportSettings(pMsg); 369 break; 370 371 case M_TOGGLE_PULLING_PALETTES: 372 _togglePullPalettes(); 373 break; 374 375 case M_TOGGLE_DORMANT_NODE_WINDOW: 376 _toggleDormantNodeWindow(); 377 break; 378 379 case M_TOGGLE_GROUP_ROLLING: 380 _toggleGroupRolling(); 381 break; 382 383 default: 384 _inherited::MessageReceived(pMsg); 385 break; 386 } 387 } 388 389 // -------------------------------------------------------- // 390 // *** IStateArchivable 391 // -------------------------------------------------------- // 392 393 status_t RouteWindow::importState( 394 const BMessage* archive) { 395 396 status_t err; 397 398 // frame rect 399 BRect r; 400 err = archive->FindRect("frame", &r); 401 if(err == B_OK) { 402 MoveTo(r.LeftTop()); 403 ResizeTo(r.Width(), r.Height()); 404 m_lastFramePosition = r.LeftTop(); 405 } 406 407 // status view width 408 int32 i; 409 err = archive->FindInt32("statusViewWidth", &i); 410 if (err == B_OK) { 411 float diff = i - m_statusView->Bounds().IntegerWidth(); 412 m_statusView->ResizeBy(diff, 0.0); 413 m_hScrollBar->ResizeBy(-diff, 0.0); 414 m_hScrollBar->MoveBy(diff, 0.0); 415 } 416 417 // settings 418 bool b; 419 err = archive->FindBool("pullPalettes", &b); 420 if(err == B_OK) 421 m_pullPalettesItem->SetMarked(b); 422 423 // const char* p; 424 // err = archive->FindString("saveDir", &p); 425 // if(err == B_OK) { 426 // m_openPanel.SetPanelDirectory(p); 427 // m_savePanel.SetPanelDirectory(p); 428 // } 429 // 430 // dormant-node window 431 err = archive->FindRect("addonPaletteFrame", &r); 432 if(err == B_OK) 433 m_dormantNodeWindowFrame = r; 434 err = archive->FindBool("addonPaletteVisible", &b); 435 if(err == B_OK && 436 (b != (m_dormantNodeWindow != 0))) { 437 _toggleDormantNodeWindow(); 438 if(!m_dormantNodeWindow) 439 m_dormantNodeWindowFrame = r; 440 } 441 442 if(m_dormantNodeWindow) { 443 m_dormantNodeWindow->MoveTo(m_dormantNodeWindowFrame.LeftTop()); 444 m_dormantNodeWindow->ResizeTo( 445 m_dormantNodeWindowFrame.Width(), 446 m_dormantNodeWindowFrame.Height()); 447 } 448 449 // transport window 450 err = archive->FindRect("transportFrame", &r); 451 if(err == B_OK) 452 m_transportWindowFrame = r; 453 err = archive->FindBool("transportVisible", &b); 454 if(err == B_OK && 455 (b != (m_transportWindow != 0))) { 456 _toggleTransportWindow(); 457 if(!m_transportWindow) 458 m_transportWindowFrame = r; 459 } 460 461 if(m_transportWindow) { 462 m_transportWindow->MoveTo(m_transportWindowFrame.LeftTop()); 463 m_transportWindow->ResizeTo( 464 m_transportWindowFrame.Width(), 465 m_transportWindowFrame.Height()); 466 } 467 468 return B_OK; 469 } 470 471 status_t RouteWindow::exportState( 472 BMessage* archive) const { 473 474 BRect r = Frame(); 475 archive->AddRect("frame", r); 476 archive->AddBool("pullPalettes", m_pullPalettesItem->IsMarked()); 477 478 bool b = (m_dormantNodeWindow != 0); 479 r = (b) ? 480 m_dormantNodeWindow->Frame() : 481 m_dormantNodeWindowFrame; 482 archive->AddRect("addonPaletteFrame", r); 483 archive->AddBool("addonPaletteVisible", b); 484 485 b = (m_transportWindow != 0); 486 r = (b) ? m_transportWindow->Frame() : 487 m_transportWindowFrame; 488 489 archive->AddRect("transportFrame", r); 490 archive->AddBool("transportVisible", b); 491 492 // [c.lenz 23may00] remember status view width 493 int i = m_statusView->Bounds().IntegerWidth(); 494 archive->AddInt32("statusViewWidth", i); 495 496 // entry_ref saveRef; 497 // m_savePanel.GetPanelDirectory(&saveRef); 498 // BEntry saveEntry(&saveRef); 499 // if(saveEntry.InitCheck() == B_OK) { 500 // BPath p; 501 // saveEntry.GetPath(&p); 502 // archive->AddString("saveDir", p.Path()); 503 // } 504 505 return B_OK; 506 } 507 508 // -------------------------------------------------------- // 509 // implementation 510 // -------------------------------------------------------- // 511 512 // [c.lenz 1mar2000] added for better Zoom() support 513 void RouteWindow::_constrainToScreen() { 514 D_INTERNAL(("RouteWindow::_constrainToScreen()\n")); 515 516 BScreen screen(this); 517 BRect screenRect = screen.Frame(); 518 BRect windowRect = Frame(); 519 520 // if the window is outside the screen rect 521 // move it to the default position 522 if (!screenRect.Intersects(windowRect)) { 523 windowRect.OffsetTo(screenRect.LeftTop()); 524 MoveTo(windowRect.LeftTop()); 525 windowRect = Frame(); 526 } 527 528 // if the window is larger than the screen rect 529 // resize it to fit at each side 530 if (!screenRect.Contains(windowRect)) { 531 if (windowRect.left < screenRect.left) { 532 windowRect.left = screenRect.left + 5.0; 533 MoveTo(windowRect.LeftTop()); 534 windowRect = Frame(); 535 } 536 if (windowRect.top < screenRect.top) { 537 windowRect.top = screenRect.top + 5.0; 538 MoveTo(windowRect.LeftTop()); 539 windowRect = Frame(); 540 } 541 if (windowRect.right > screenRect.right) { 542 windowRect.right = screenRect.right - 5.0; 543 } 544 if (windowRect.bottom > screenRect.bottom) { 545 windowRect.bottom = screenRect.bottom - 5.0; 546 } 547 ResizeTo(windowRect.Width(), windowRect.Height()); 548 } 549 } 550 551 void RouteWindow::_toggleTransportWindow() { 552 if(m_transportWindow) { 553 m_transportWindowFrame = m_transportWindow->Frame(); 554 m_transportWindow->Lock(); 555 m_transportWindow->Quit(); 556 m_transportWindow = 0; 557 m_transportWindowItem->SetMarked(false); 558 } 559 else { 560 m_transportWindow = new TransportWindow( 561 m_routingView->manager, 562 this, 563 "Transport"); 564 565 // ask for a selection update 566 BMessenger(m_routingView).SendMessage( 567 MediaRoutingView::M_BROADCAST_SELECTION); 568 569 // place & display the window 570 if(m_transportWindowFrame.IsValid()) { 571 m_transportWindow->MoveTo(m_transportWindowFrame.LeftTop()); 572 m_transportWindow->ResizeTo( 573 m_transportWindowFrame.Width(), 574 m_transportWindowFrame.Height()); 575 } 576 577 m_transportWindow->Show(); 578 m_transportWindowItem->SetMarked(true); 579 } 580 } 581 582 // [e.moon 17nov99] 583 void RouteWindow::_togglePullPalettes() { 584 585 m_pullPalettesItem->SetMarked(!m_pullPalettesItem->IsMarked()); 586 } 587 588 void RouteWindow::_toggleDormantNodeWindow() { 589 590 if(m_dormantNodeWindow) { 591 m_dormantNodeWindowFrame = m_dormantNodeWindow->Frame(); 592 m_dormantNodeWindow->Lock(); 593 m_dormantNodeWindow->Quit(); 594 m_dormantNodeWindow = 0; 595 m_dormantNodeWindowItem->SetMarked(false); 596 } 597 else { 598 m_dormantNodeWindow = new DormantNodeWindow(this); 599 if(m_dormantNodeWindowFrame.IsValid()) { 600 m_dormantNodeWindow->MoveTo(m_dormantNodeWindowFrame.LeftTop()); 601 m_dormantNodeWindow->ResizeTo( 602 m_dormantNodeWindowFrame.Width(), 603 m_dormantNodeWindowFrame.Height()); 604 } 605 m_dormantNodeWindow->Show(); 606 m_dormantNodeWindowItem->SetMarked(true); 607 } 608 } 609 610 void RouteWindow::_handleGroupSelected( 611 BMessage* message) { 612 status_t err; 613 uint32 groupID; 614 615 err = message->FindInt32("groupID", (int32*)&groupID); 616 if(err < B_OK) { 617 PRINT(( 618 "! RouteWindow::_handleGroupSelected(): no groupID in message!\n")); 619 return; 620 } 621 622 if(!m_transportWindow) 623 return; 624 625 BMessage m(TransportWindow::M_SELECT_GROUP); 626 m.AddInt32("groupID", groupID); 627 BMessenger(m_transportWindow).SendMessage(&m); 628 629 m_selectedGroupID = groupID; 630 } 631 632 void RouteWindow::_handleShowErrorMessage( 633 BMessage* message) { 634 status_t err; 635 BString text; 636 637 err = message->FindString("text", &text); 638 if(err < B_OK) { 639 PRINT(( 640 "! RouteWindow::_handleShowErrorMessage(): no text in message!\n")); 641 return; 642 } 643 644 m_statusView->setErrorMessage(text.String(), message->HasBool("error")); 645 } 646 647 // refresh the transport window for the given group, if any 648 void RouteWindow::_refreshTransportSettings( 649 BMessage* message) { 650 651 status_t err; 652 uint32 groupID; 653 654 err = message->FindInt32("groupID", (int32*)&groupID); 655 if(err < B_OK) { 656 PRINT(( 657 "! RouteWindow::_refreshTransportSettings(): no groupID in message!\n")); 658 return; 659 } 660 661 if(m_transportWindow) { 662 // relay the message 663 BMessenger(m_transportWindow).SendMessage(message); 664 } 665 } 666 667 668 void RouteWindow::_closePalettes() { 669 BAutolock _l(this); 670 671 if(m_transportWindow) { 672 m_transportWindow->Lock(); 673 m_transportWindow->Quit(); 674 m_transportWindow = 0; 675 } 676 } 677 678 // [e.moon 17nov99] move all palette windows by the 679 // specified amounts 680 void RouteWindow::_movePalettesBy( 681 float xDelta, 682 float yDelta) { 683 684 if(m_transportWindow) 685 m_transportWindow->MoveBy(xDelta, yDelta); 686 687 if(m_dormantNodeWindow) 688 m_dormantNodeWindow->MoveBy(xDelta, yDelta); 689 } 690 691 // [e.moon 1dec99] toggle group playback 692 void RouteWindow::_toggleGroupRolling() { 693 694 if(!m_selectedGroupID) 695 return; 696 697 NodeGroup* g; 698 status_t err = m_routingView->manager->findGroup( 699 m_selectedGroupID, 700 &g); 701 if(err < B_OK) 702 return; 703 704 Autolock _l(g); 705 uint32 startAction = 706 (g->runMode() == BMediaNode::B_OFFLINE) ? 707 NodeGroup::M_ROLL : 708 NodeGroup::M_START; 709 710 BMessenger m(g); 711 switch(g->transportState()) { 712 case NodeGroup::TRANSPORT_STOPPED: 713 m.SendMessage(startAction); 714 break; 715 716 case NodeGroup::TRANSPORT_RUNNING: 717 case NodeGroup::TRANSPORT_ROLLING: 718 m.SendMessage(NodeGroup::M_STOP); 719 break; 720 721 default: 722 break; 723 } 724 } 725 726 // END -- RouteWindow.cpp -- 727