1 //////////////////////////////////////////////////////////////////////
2 //
3 // BeeBEEP Copyright (C) 2010-2021 Marco Mastroddi
4 //
5 // BeeBEEP is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published
7 // by the Free Software Foundation, either version 3 of the License,
8 // or (at your option) any later version.
9 //
10 // BeeBEEP is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with BeeBEEP. If not, see <http://www.gnu.org/licenses/>.
17 //
18 // Author: Marco Mastroddi <marco.mastroddi(AT)gmail.com>
19 //
20 // $Id: GuiFloatingChat.cpp 1471 2021-01-06 12:10:07Z mastroddi $
21 //
22 //////////////////////////////////////////////////////////////////////
23 
24 #include "BeeUtils.h"
25 #include "ChatManager.h"
26 #include "Core.h"
27 #include "GuiFloatingChat.h"
28 #include "GuiEmoticons.h"
29 #include "GuiPresetMessageList.h"
30 #ifdef BEEBEEP_USE_VOICE_CHAT
31   #include "GuiRecordVoiceMessage.h"
32   #include "VoicePlayer.h"
33 #endif
34 #include "GuiUserList.h"
35 #include "IconManager.h"
36 #include "Settings.h"
37 #include "ShortcutManager.h"
38 #include "UserManager.h"
39 #ifdef Q_OS_WIN
40   #include <Windows.h>
41 #endif
42 
43 
GuiFloatingChat(QWidget * parent)44 GuiFloatingChat::GuiFloatingChat( QWidget *parent )
45  : QMainWindow( parent )
46 {
47   setObjectName( "GuiFloatingChat" );
48   m_mainWindowIcon = IconManager::instance().icon( "chat.png" );
49   setMainIcon( false );
50   mp_chat = new GuiChat( this );
51 
52   mp_actGroupMenu = new QAction( IconManager::instance().icon( "group-edit.png" ), tr( "Edit group chat" ), this );
53   connect( mp_actGroupMenu, SIGNAL( triggered() ), this, SLOT( showGroupMenu() ) );
54 
55   mp_barMembers = new QToolBar( tr( "Show the bar of members" ), this );
56   addToolBar( Qt::RightToolBarArea, mp_barMembers );
57   mp_barMembers->setObjectName( "GuiFloatingChatMemberToolBar" );
58   mp_barMembers->setIconSize( Settings::instance().avatarIconSize() );
59   mp_barMembers->setAllowedAreas( Qt::AllToolBarAreas );
60   mp_barMembers->setFloatable( false );
61   mp_barMembers->toggleViewAction()->setVisible( false );
62 
63   mp_barChat = new QToolBar( tr( "Show chat toolbar" ), this );
64   addToolBar( Qt::BottomToolBarArea, mp_barChat );
65   mp_barChat->setObjectName( "GuiFloatingChatToolBar" );
66   mp_barChat->setIconSize( Settings::instance().mainBarIconSize() );
67   mp_barChat->setAllowedAreas( Qt::AllToolBarAreas );
68   mp_chat->setupToolBar( mp_barChat );
69   mp_barChat->setVisible( Settings::instance().showChatToolbar() );
70   mp_barChat->insertSeparator( mp_barChat->actions().first() );
71   mp_barChat->setFloatable( false );
72 
73   mp_dockPresetMessageList = new QDockWidget( tr( "Preset messages" ), this );
74   mp_dockPresetMessageList->setObjectName( "GuiDockPresetMessageList" );
75   mp_presetMessageListWidget = new GuiPresetMessageList( this );
76   mp_presetMessageListWidget->loadFromSettings();
77   connect( mp_presetMessageListWidget, SIGNAL( presetMessageSelected( const QString& ) ), mp_chat, SLOT( addText( const QString& ) ) );
78   mp_dockPresetMessageList->setWidget( mp_presetMessageListWidget );
79   mp_dockPresetMessageList->setAllowedAreas( Qt::AllDockWidgetAreas );
80   addDockWidget( Qt::LeftDockWidgetArea, mp_dockPresetMessageList );
81   QAction* actViewPresetMessageList = mp_dockPresetMessageList->toggleViewAction();
82   actViewPresetMessageList->setIcon( IconManager::instance().icon( "preset-message.png" ) );
83   actViewPresetMessageList->setToolTip( tr( "Show the preset messages panel" ) );
84   mp_barChat->insertAction( mp_barChat->actions().first(), actViewPresetMessageList );
85   mp_actSaveWindowGeometry = mp_barChat->addAction( IconManager::instance().icon( "save-window.png" ), tr( "Save window's geometry" ), this, SLOT( saveGeometryAndState() ) );
86 
87   mp_dockEmoticons = new QDockWidget( tr( "Emoticons" ), this );
88   mp_dockEmoticons->setObjectName( "GuiDockEmoticons" );
89   mp_emoticonsWidget = new GuiEmoticons( this );
90   updateEmoticons();
91   connect( mp_emoticonsWidget, SIGNAL( emoticonSelected( const Emoticon& ) ), mp_chat, SLOT( addEmoticon( const Emoticon& ) ) );
92   mp_dockEmoticons->setWidget( mp_emoticonsWidget );
93   mp_dockEmoticons->setAllowedAreas( Qt::AllDockWidgetAreas );
94   addDockWidget( Qt::LeftDockWidgetArea, mp_dockEmoticons );
95   QAction* mp_actViewEmoticons = mp_dockEmoticons->toggleViewAction();
96   mp_actViewEmoticons->setIcon( IconManager::instance().icon( "emoticon.png" ) );
97   mp_actViewEmoticons->setText( tr( "Show the emoticon panel" ) );
98   mp_actViewEmoticons->setVisible( !Settings::instance().useOnlyTextEmoticons() );
99   mp_barChat->insertAction( mp_barChat->actions().first(), mp_actViewEmoticons );
100 
101   setCentralWidget( mp_chat );
102   statusBar();
103   m_chatIsVisible = false;
104   m_prevActivatedState = false;
105 
106   connect( mp_chat, SIGNAL( toggleVisibilityEmoticonsPanelRequest() ), this, SLOT( toggleVisibilityEmoticonPanel() ) );
107   connect( mp_chat, SIGNAL( toggleVisibilityPresetMessagesPanelRequest() ), this, SLOT( toggleVisibilityPresetMessagesPanel() ) );
108   connect( mp_chat, SIGNAL( hideRequest() ), this, SLOT( showMinimized() ) );
109   connect( mp_chat, SIGNAL( closeRequest() ), this, SLOT( close() ) );
110   connect( mp_chat, SIGNAL( updateChatFontRequest() ), this, SIGNAL( updateChatFontRequest() ) );
111   connect( mp_chat, SIGNAL( updateChatColorsRequest() ), this, SIGNAL( updateChatColorsRequest() ) );
112   connect( mp_chat, SIGNAL( showStatusMessageRequest( const QString&, int ) ), this, SLOT( showStatusMessage(const QString&, int ) ) );
113 #ifdef BEEBEEP_USE_VOICE_CHAT
114   connect( mp_chat, SIGNAL( showVoiceMessageDialogRequest() ), this, SLOT( showRecordMessageDialog() ) );
115 #endif
116   connect( qApp, SIGNAL( focusChanged( QWidget*, QWidget* ) ), this, SLOT( onApplicationFocusChanged( QWidget*, QWidget* ) ) );
117 }
118 
updateChatTitle(const Chat & c)119 void GuiFloatingChat::updateChatTitle( const Chat& c )
120 {
121   QString chat_title = "";
122 
123   if( c.isPrivate() )
124   {
125     VNumber user_id = c.privateUserId();
126     User u = UserManager::instance().findUser( user_id );
127     if( u.isValid() )
128     {
129       QString user_status = u.status() != User::Online ? Bee::userStatusToString( u.status() ) : "";
130       QString user_status_description = u.status() != User::Offline ? u.statusDescription() : "";
131       QString user_name = Bee::userNameToShow( u, false );
132 
133       if( !user_status.isEmpty() && !user_status_description.isEmpty() )
134         chat_title = QString( "%1 [%2 - %3]" ).arg( user_name, Bee::userStatusToString( u.status() ), user_status_description );
135       else if( !user_status.isEmpty() )
136         chat_title = QString( "%1 (%2)" ).arg( user_name, user_status );
137       else if( !user_status_description.isEmpty() )
138         chat_title = QString( "%1 [%2]" ).arg( user_name, user_status_description );
139       else
140         chat_title = user_name;
141 
142       m_mainWindowIcon = Bee::avatarForUser( u, QSize( 256, 256 ), true );
143     }
144     else
145     {
146       qWarning() << "Invalid user" << user_id << "found for private chat" << c.name();
147       m_mainWindowIcon = IconManager::instance().icon( "chat.png" );
148       chat_title = c.name();
149     }
150   }
151   else if( c.isDefault() )
152   {
153     chat_title = tr( "All users" ).toUpper();
154     m_mainWindowIcon = IconManager::instance().icon( "default-chat-online.png" );
155   }
156   else if( c.isGroup() )
157   {
158     chat_title = c.name();
159     m_mainWindowIcon = IconManager::instance().icon( "group.png" );
160   }
161   else
162   {
163     chat_title = c.name();
164     m_mainWindowIcon = IconManager::instance().icon( "chat.png" );
165   }
166 
167   setMainIcon( c.unreadMessages() > 0 );
168   setWindowTitle( QString( "%1 - %2 %3" ).arg( chat_title, Settings::instance().programName(), Settings::instance().version( false, false, false ) ) );
169 }
170 
updateChatMember(const Chat & c,const User & u)171 void GuiFloatingChat::updateChatMember( const Chat& c, const User& u )
172 {
173   if( !mp_barMembers->isEnabled() )
174     return;
175 
176   if( u.isLocal() )
177     return;
178 
179   QAction* act_user = Q_NULLPTR;
180   QList<QAction*> member_actions = mp_barMembers->actions();
181   foreach( QAction* act, member_actions )
182   {
183     if( u.id() == Bee::qVariantToVNumber( act->data() ) )
184     {
185       act_user = act;
186       break;
187     }
188   }
189 
190   QString user_name = Bee::userNameToShow( u, false );
191   if( !act_user )
192   {
193     act_user = mp_barMembers->addAction( user_name, this, SLOT( onGroupMemberActionTriggered() ) );
194     act_user->setData( u.id() );
195     act_user->setCheckable( false );
196   }
197   else
198     act_user->setText( user_name );
199 
200   int avatar_size = mp_barMembers->iconSize().width();
201   QString user_tooltip = Bee::toolTipForUser( u, true );
202   if( !u.isLocal() && u.protocolVersion() >= 63 && !c.userHasReadMessages( u.id() ) )
203   {
204     user_tooltip += QString( "\n%1" ).arg( tr( "%1 has not read last messages" ).arg( u.name() ) );
205     act_user->setIcon( Bee::avatarForUser( u, QSize( avatar_size, avatar_size ), Settings::instance().showUserPhoto(), User::Away ) );
206   }
207   else
208     act_user->setIcon( Bee::avatarForUser( u, QSize( avatar_size, avatar_size ), Settings::instance().showUserPhoto() ) );
209   act_user->setToolTip( user_tooltip.trimmed() );
210 }
211 
updateChatMembers(const Chat & c)212 void GuiFloatingChat::updateChatMembers( const Chat& c )
213 {
214   mp_barMembers->setVisible( !c.isDefault() );
215   mp_barMembers->setEnabled( !c.isDefault() );
216 
217   if( mp_barMembers->isEnabled() )
218   {
219     mp_barMembers->clear();
220     mp_barMembers->setIconSize( Settings::instance().avatarIconSize() );
221     mp_barMembers->setVisible( true );
222 
223     UserList ul = UserManager::instance().userList().fromUsersId( c.usersId() );
224     foreach( User u, ul.toList() )
225       updateChatMember( c, u );
226 
227     if( c.isGroup() )
228     {
229       mp_barMembers->addSeparator();
230       mp_barMembers->addAction( mp_actGroupMenu );
231     }
232     else
233       mp_actGroupMenu->setDisabled( true );
234   }
235 }
236 
updateChat(const Chat & c)237 void GuiFloatingChat::updateChat( const Chat& c )
238 {
239   if( mp_chat->updateChat( c ) )
240   {
241     setMainIcon( c.unreadMessages() > 0 );
242     updateChatTitle( c );
243     updateChatMembers( c );
244     mp_chat->updateShortcuts();
245     mp_chat->updateActions( c, beeCore->isConnected(), beeCore->connectedUsers(), beeCore->isFileTransferActive() );
246   }
247 }
248 
setChat(const Chat & c)249 bool GuiFloatingChat::setChat( const Chat& c )
250 {
251   if( mp_chat->setChat( c ) )
252   {
253     setMainIcon( c.unreadMessages() > 0 );
254     updateChatTitle( c );
255     updateChatMembers( c );
256     mp_chat->updateShortcuts();
257     mp_chat->updateActions( c, beeCore->isConnected(), beeCore->connectedUsers(), beeCore->isFileTransferActive() );
258     return true;
259   }
260   else
261     return false;
262 }
263 
updateActions(bool is_connected,int connected_users,bool file_transfer_is_active)264 void GuiFloatingChat::updateActions( bool is_connected, int connected_users, bool file_transfer_is_active )
265 {
266   Chat c = ChatManager::instance().chat( mp_chat->chatId() );
267   if( !c.isValid() )
268     return;
269   mp_chat->updateActions( c, is_connected, connected_users, file_transfer_is_active );
270 }
271 
updateUser(const User & u)272 void GuiFloatingChat::updateUser( const User& u )
273 {
274   Chat c = ChatManager::instance().chat( mp_chat->chatId() );
275   if( !c.hasUser( u.id() ) )
276     return;
277 
278   if( c.isPrivateForUser( u.id() ) )
279     updateChatTitle( c );
280 
281   updateChatMember( c, u );
282 }
283 
closeEvent(QCloseEvent * e)284 void GuiFloatingChat::closeEvent( QCloseEvent* e )
285 {
286 #ifdef BEEBEEP_USE_VOICE_CHAT
287   if( beeCore->voicePlayer()->chatId() == mp_chat->chatId() && beeCore->voicePlayer()->isPlaying() )
288     beeCore->voicePlayer()->stop();
289 #endif
290   if( Settings::instance().floatingChatState().isEmpty() )
291     Settings::instance().setShowEmoticonMenu( mp_dockEmoticons->isVisible() );
292   QMainWindow::closeEvent( e );
293   emit chatIsAboutToClose( mp_chat->chatId() );
294   e->accept();
295 }
296 
setWindowFlagsAndGeometry()297 void GuiFloatingChat::setWindowFlagsAndGeometry()
298 {
299   setAttribute( Qt::WA_ShowWithoutActivating );
300   Bee::setWindowStaysOnTop( this, Settings::instance().stayOnTop() );
301 
302   if( Settings::instance().floatingChatGeometry().isEmpty() )
303   {
304     resize( 600, 400 );
305     if( !QApplication::activeWindow() )
306       move( QApplication::desktop()->availableGeometry().width() - frameGeometry().width() - 30, 40 );
307   }
308   else
309     restoreGeometry( Settings::instance().floatingChatGeometry() );
310 
311   if( Settings::instance().floatingChatState().isEmpty() )
312   {
313     mp_dockEmoticons->setVisible( Settings::instance().showEmoticonMenu() );
314     mp_dockPresetMessageList->hide();
315   }
316   else
317     restoreState( Settings::instance().floatingChatState() );
318 
319   QSplitter* chat_splitter = mp_chat->chatSplitter();
320   if( Settings::instance().floatingChatSplitterState().isEmpty() )
321   {
322     int central_widget_height = centralWidget()->size().height();
323     QList<int> splitter_size_list;
324     splitter_size_list.append( central_widget_height - 80 );
325     splitter_size_list.append( 80 );
326     chat_splitter->setSizes( splitter_size_list );
327   }
328   else
329     chat_splitter->restoreState( Settings::instance().floatingChatSplitterState() );
330 }
331 
showUp()332 void GuiFloatingChat::showUp()
333 {
334   Bee::showUp( this );
335   mp_chat->ensureLastMessageVisible();
336 }
337 
raiseOnTop()338 void GuiFloatingChat::raiseOnTop()
339 {
340   Bee::raiseOnTop( this );
341 }
342 
setFocusInChat()343 void GuiFloatingChat::setFocusInChat()
344 {
345   QWidget* w = QApplication::activeWindow();
346   if( !w )
347   {
348 #ifdef BEEBEEP_DEBUG
349     qWarning() << "Unable to set focus in chat: application has not the focus";
350 #endif
351     return;
352   }
353 
354   QApplication::setActiveWindow( this );
355   mp_chat->ensureFocusInChat();
356 }
357 
onApplicationFocusChanged(QWidget * old,QWidget * now)358 void GuiFloatingChat::onApplicationFocusChanged( QWidget* old, QWidget* now )
359 {
360   if( old == Q_NULLPTR && isAncestorOf( now )  )
361   {
362 #ifdef BEEBEEP_DEBUG
363     qDebug() << "Floating chat" << mp_chat->chatId() << "has grab focus";
364 #endif
365     m_chatIsVisible = true;
366     m_prevActivatedState = true;
367     setWindowOpacity( Settings::instance().chatActiveWindowOpacityLevel() / 100.0 );
368     mp_chat->updateActionsOnFocusChanged();
369     emit readAllMessages( mp_chat->chatId() );
370     mp_chat->ensureFocusInChat();
371     return;
372   }
373 
374   if( isAncestorOf( old ) && now == Q_NULLPTR )
375   {
376 #ifdef BEEBEEP_DEBUG
377     qDebug() << "Floating chat" << mp_chat->chatId() << "has lost focus";
378 #endif
379     m_chatIsVisible = false;
380     m_prevActivatedState = false;
381     setWindowOpacity( Settings::instance().chatInactiveWindowOpacityLevel() / 100.0 );
382     return;
383   }
384 
385   bool current_state = isActiveWindow();
386   if( current_state != m_prevActivatedState )
387   {
388     m_prevActivatedState = current_state;
389     if( current_state )
390     {
391 #ifdef BEEBEEP_DEBUG
392       qDebug() << "Floating chat" << mp_chat->chatId() << "has grab focus (active)";
393 #endif
394       m_chatIsVisible = true;
395       setWindowOpacity( Settings::instance().chatActiveWindowOpacityLevel() / 100.0 );
396       mp_chat->updateActionsOnFocusChanged();
397       emit readAllMessages( mp_chat->chatId() );
398       mp_chat->ensureFocusInChat();
399     }
400     else
401     {
402 #ifdef BEEBEEP_DEBUG
403       qDebug() << "Floating chat" << mp_chat->chatId() << "has lost focus (inactive)";
404 #endif
405       m_chatIsVisible = false;
406       setWindowOpacity( Settings::instance().chatInactiveWindowOpacityLevel() / 100.0 );
407     }
408   }
409 }
410 
saveGeometryAndState()411 void GuiFloatingChat::saveGeometryAndState()
412 {
413   if( isVisible() )
414   {
415     QByteArray ba_state = saveState();
416 #if QT_VERSION == 0x050906
417     int default_button = mp_dockEmoticons->isVisible() || mp_dockPresetMessageList->isVisible() ? 0 : 1;
418     int ret_code = QMessageBox::warning( this, Settings::instance().programName(),
419                                          tr( "Qt libraries have a bug on saving the window's state." ) + QString( " " ) +
420                                          tr( "If you have layout problem please save only geometry." ),
421                                          tr( "Save all" ), tr( "Save only geometry" ), tr( "Cancel" ), default_button, 2 );
422     switch( ret_code )
423     {
424     case 0:
425       break;
426     case 1:
427       ba_state = QByteArray();
428       break;
429     default:
430       return;
431     }
432 #endif
433     Settings::instance().setFloatingChatGeometry( saveGeometry() );
434     Settings::instance().setFloatingChatState( ba_state );
435     QSplitter* chat_splitter = mp_chat->chatSplitter();
436     Settings::instance().setFloatingChatSplitterState( chat_splitter->saveState() );
437     Settings::instance().setShowEmoticonMenu( mp_dockEmoticons->isVisible() );
438     Settings::instance().save();
439     if( ba_state.isEmpty() )
440       statusBar()->showMessage( tr( "Window geometry saved" ), 5000 );
441     else
442       statusBar()->showMessage( tr( "Window geometry and state saved" ), 5000 );
443   }
444   else
445     qWarning() << "Unable to save floating chat geometry and state (window is not visible)";
446 }
447 
keyPressEvent(QKeyEvent * e)448 void GuiFloatingChat::keyPressEvent( QKeyEvent* e )
449 {
450   if( e->key() == Qt::Key_Escape )
451   {
452     if( Settings::instance().keyEscapeMinimizeInTray() )
453       QMetaObject::invokeMethod( this, "close", Qt::QueuedConnection );
454     else
455       QMetaObject::invokeMethod( this, "showMinimized", Qt::QueuedConnection );
456     e->accept();
457     return;
458   }
459 
460   QMainWindow::keyPressEvent( e );
461 }
462 
setMainIcon(bool with_message)463 void GuiFloatingChat::setMainIcon( bool with_message )
464 {
465   if( with_message )
466     setWindowIcon( IconManager::instance().icon( "beebeep-message.png" ) );
467   else
468     setWindowIcon( m_mainWindowIcon );
469 }
470 
updateEmoticons()471 void GuiFloatingChat::updateEmoticons()
472 {
473   QMetaObject::invokeMethod( mp_emoticonsWidget, "updateEmoticons", Qt::QueuedConnection );
474 }
475 
toggleVisibilityEmoticonPanel()476 void GuiFloatingChat::toggleVisibilityEmoticonPanel()
477 {
478   if( mp_dockEmoticons->isVisible() )
479     mp_dockEmoticons->hide();
480   else
481     mp_dockEmoticons->show();
482 }
483 
toggleVisibilityPresetMessagesPanel()484 void GuiFloatingChat::toggleVisibilityPresetMessagesPanel()
485 {
486   if( mp_dockPresetMessageList->isVisible() )
487     mp_dockPresetMessageList->hide();
488   else
489     mp_dockPresetMessageList->show();
490 }
491 
onGroupMemberActionTriggered()492 void GuiFloatingChat::onGroupMemberActionTriggered()
493 {
494   QAction *act = qobject_cast<QAction*>( sender() );
495   if( act )
496   {
497     VNumber user_id = Bee::qVariantToVNumber( act->data() );
498     emit showVCardRequest( user_id );
499   }
500 }
501 
setChatReadByUser(const Chat & c,const User & u)502 void GuiFloatingChat::setChatReadByUser( const Chat& c, const User& u )
503 {
504   if( c.id() == mp_chat->chatId() )
505     updateChatMember( c, u );
506 }
507 
showChatMessage(const Chat & c,const ChatMessage & cm)508 void GuiFloatingChat::showChatMessage( const Chat& c, const ChatMessage& cm )
509 {
510   if( mp_chat->appendChatMessage( c, cm ) )
511   {
512     if( cm.isFromLocalUser() )
513     {
514       updateChatMembers( c );
515     }
516     else
517     {
518       if( !cm.isFromSystem()  )
519         statusBar()->showMessage( "" ); // reset writing message
520     }
521   }
522 }
523 
showGroupMenu()524 void GuiFloatingChat::showGroupMenu()
525 {
526   mp_chat->editChatMembers();
527 }
528 
onTickEvent(int ticks)529 void GuiFloatingChat::onTickEvent( int ticks )
530 {
531   mp_chat->onTickEvent( ticks );
532 }
533 
showStatusMessage(const QString & msg,int timeout)534 void GuiFloatingChat::showStatusMessage( const QString& msg, int timeout )
535 {
536   statusBar()->showMessage( msg, timeout );
537   QApplication::processEvents();
538 }
539 
loadSavedMessages()540 void GuiFloatingChat::loadSavedMessages()
541 {
542   QTimer::singleShot( 0, mp_chat, SLOT( loadSavedMessages() ) );
543 }
544 
545 #ifdef BEEBEEP_USE_VOICE_CHAT
showRecordMessageDialog()546 void GuiFloatingChat::showRecordMessageDialog()
547 {
548   if( Settings::instance().disableVoiceMessages() )
549     return;
550   GuiRecordVoiceMessage* grvm = new GuiRecordVoiceMessage( this );
551   grvm->setModal( true );
552   grvm->setRecipient( ChatManager::instance().chatName( mp_chat->chatId() ) );
553   grvm->show();
554   if( grvm->exec() == QDialog::Accepted )
555   {
556     emit sendVoiceMessageRequest( mp_chat->chatId(), grvm->filePath(), grvm->duration() );
557   }
558   // deleted in Close Event to bypass crash if you close a modal dialog with QUIT
559 }
560 #endif
561