/***************************************************************************** * extensions.cpp: Extensions manager for Qt: dialogs manager **************************************************************************** * Copyright (C) 2009-2010 VideoLAN and authors * $Id$ * * Authors: Jean-Philippe André < jpeg # videolan.org > * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ #include "extensions.hpp" #include "extensions_manager.hpp" // for isUnloading() #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util/customwidgets.hpp" ExtensionsDialogProvider *ExtensionsDialogProvider::instance = NULL; static void DialogCallback( extension_dialog_t *p_ext_dialog, void *p_data ); ExtensionsDialogProvider::ExtensionsDialogProvider( intf_thread_t *_p_intf, extensions_manager_t *p_mgr ) : QObject( NULL ), p_intf( _p_intf ), p_extensions_manager( p_mgr ) { vlc_dialog_provider_set_ext_callback( p_intf, DialogCallback, NULL ); CONNECT( this, SignalDialog( extension_dialog_t* ), this, UpdateExtDialog( extension_dialog_t* ) ); } ExtensionsDialogProvider::~ExtensionsDialogProvider() { msg_Dbg( p_intf, "ExtensionsDialogProvider is quitting..." ); vlc_dialog_provider_set_ext_callback( p_intf, NULL, NULL ); } /** Create a dialog * Note: Lock on p_dialog->lock must be held. */ ExtensionDialog* ExtensionsDialogProvider::CreateExtDialog( extension_dialog_t *p_dialog ) { ExtensionDialog *dialog = new ExtensionDialog( p_intf, p_extensions_manager, p_dialog ); p_dialog->p_sys_intf = (void*) dialog; CONNECT( dialog, destroyDialog( extension_dialog_t* ), this, DestroyExtDialog( extension_dialog_t* ) ); return dialog; } /** Destroy a dialog * Note: Lock on p_dialog->lock must be held. */ int ExtensionsDialogProvider::DestroyExtDialog( extension_dialog_t *p_dialog ) { assert( p_dialog ); ExtensionDialog *dialog = ( ExtensionDialog* ) p_dialog->p_sys_intf; if( !dialog ) return VLC_EGENERIC; delete dialog; p_dialog->p_sys_intf = NULL; vlc_cond_signal( &p_dialog->cond ); return VLC_SUCCESS; } /** * Update/Create/Destroy a dialog **/ ExtensionDialog* ExtensionsDialogProvider::UpdateExtDialog( extension_dialog_t *p_dialog ) { assert( p_dialog ); ExtensionDialog *dialog = ( ExtensionDialog* ) p_dialog->p_sys_intf; if( p_dialog->b_kill && !dialog ) { /* This extension could not be activated properly but tried to create a dialog. We must ignore it. */ return NULL; } vlc_mutex_lock( &p_dialog->lock ); if( !p_dialog->b_kill && !dialog ) { dialog = CreateExtDialog( p_dialog ); dialog->setVisible( !p_dialog->b_hide ); dialog->has_lock = false; } else if( !p_dialog->b_kill && dialog ) { dialog->has_lock = true; dialog->UpdateWidgets(); if( strcmp( qtu( dialog->windowTitle() ), p_dialog->psz_title ) != 0 ) dialog->setWindowTitle( qfu( p_dialog->psz_title ) ); dialog->has_lock = false; dialog->setVisible( !p_dialog->b_hide ); } else if( p_dialog->b_kill ) { DestroyExtDialog( p_dialog ); } vlc_cond_signal( &p_dialog->cond ); vlc_mutex_unlock( &p_dialog->lock ); return dialog; } /** * Ask the dialog manager to create/update/kill the dialog. Thread-safe. **/ void ExtensionsDialogProvider::ManageDialog( extension_dialog_t *p_dialog ) { assert( p_dialog ); ExtensionsManager *extMgr = ExtensionsManager::getInstance( p_intf ); assert( extMgr != NULL ); if( !extMgr->isUnloading() ) emit SignalDialog( p_dialog ); // Safe because we signal Qt thread else UpdateExtDialog( p_dialog ); // This is safe, we're already in Qt thread } /** * Ask the dialogs provider to create a new dialog **/ static void DialogCallback( extension_dialog_t *p_ext_dialog, void *p_data ) { (void) p_data; ExtensionsDialogProvider *p_edp = ExtensionsDialogProvider::getInstance(); if( p_edp ) p_edp->ManageDialog( p_ext_dialog ); } ExtensionDialog::ExtensionDialog( intf_thread_t *_p_intf, extensions_manager_t *p_mgr, extension_dialog_t *_p_dialog ) : QDialog( NULL ), p_intf( _p_intf ), p_extensions_manager( p_mgr ) , p_dialog( _p_dialog ), has_lock(true) { assert( p_dialog ); CONNECT( ExtensionsDialogProvider::getInstance(), destroyed(), this, parentDestroyed() ); msg_Dbg( p_intf, "Creating a new dialog: '%s'", p_dialog->psz_title ); this->setWindowFlags( Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint ); this->setWindowTitle( qfu( p_dialog->psz_title ) ); layout = new QGridLayout( this ); clickMapper = new QSignalMapper( this ); CONNECT( clickMapper, mapped( QObject* ), this, TriggerClick( QObject* ) ); inputMapper = new QSignalMapper( this ); CONNECT( inputMapper, mapped( QObject* ), this, SyncInput( QObject* ) ); selectMapper = new QSignalMapper( this ); CONNECT( selectMapper, mapped( QObject* ), this, SyncSelection(QObject*) ); UpdateWidgets(); } ExtensionDialog::~ExtensionDialog() { msg_Dbg( p_intf, "Deleting extension dialog '%s'", qtu(windowTitle()) ); } QWidget* ExtensionDialog::CreateWidget( extension_widget_t *p_widget ) { QLabel *label = NULL; QPushButton *button = NULL; QTextBrowser *textArea = NULL; QLineEdit *textInput = NULL; QCheckBox *checkBox = NULL; QComboBox *comboBox = NULL; QListWidget *list = NULL; SpinningIcon *spinIcon = NULL; struct extension_widget_t::extension_widget_value_t *p_value = NULL; assert( p_widget->p_sys_intf == NULL ); switch( p_widget->type ) { case EXTENSION_WIDGET_LABEL: label = new QLabel( qfu( p_widget->psz_text ), this ); p_widget->p_sys_intf = label; label->setTextFormat( Qt::RichText ); label->setOpenExternalLinks( true ); return label; case EXTENSION_WIDGET_BUTTON: button = new QPushButton( qfu( p_widget->psz_text ), this ); clickMapper->setMapping( button, new WidgetMapper( button, p_widget ) ); CONNECT( button, clicked(), clickMapper, map() ); p_widget->p_sys_intf = button; return button; case EXTENSION_WIDGET_IMAGE: label = new QLabel( this ); label->setPixmap( QPixmap( qfu( p_widget->psz_text ) ) ); if( p_widget->i_width > 0 ) label->setMaximumWidth( p_widget->i_width ); if( p_widget->i_height > 0 ) label->setMaximumHeight( p_widget->i_height ); label->setScaledContents( true ); p_widget->p_sys_intf = label; return label; case EXTENSION_WIDGET_HTML: textArea = new QTextBrowser( this ); textArea->setOpenExternalLinks( true ); textArea->setHtml( qfu( p_widget->psz_text ) ); p_widget->p_sys_intf = textArea; return textArea; case EXTENSION_WIDGET_TEXT_FIELD: textInput = new QLineEdit( this ); textInput->setText( qfu( p_widget->psz_text ) ); textInput->setReadOnly( false ); textInput->setEchoMode( QLineEdit::Normal ); inputMapper->setMapping( textInput, new WidgetMapper( textInput, p_widget ) ); /// @note: maybe it would be wiser to use textEdited here? CONNECT( textInput, textChanged(const QString &), inputMapper, map() ); p_widget->p_sys_intf = textInput; return textInput; case EXTENSION_WIDGET_PASSWORD: textInput = new QLineEdit( this ); textInput->setText( qfu( p_widget->psz_text ) ); textInput->setReadOnly( false ); textInput->setEchoMode( QLineEdit::Password ); inputMapper->setMapping( textInput, new WidgetMapper( textInput, p_widget ) ); /// @note: maybe it would be wiser to use textEdited here? CONNECT( textInput, textChanged(const QString &), inputMapper, map() ); p_widget->p_sys_intf = textInput; return textInput; case EXTENSION_WIDGET_CHECK_BOX: checkBox = new QCheckBox( this ); checkBox->setText( qfu( p_widget->psz_text ) ); checkBox->setChecked( p_widget->b_checked ); clickMapper->setMapping( checkBox, new WidgetMapper( checkBox, p_widget ) ); CONNECT( checkBox, stateChanged( int ), clickMapper, map() ); p_widget->p_sys_intf = checkBox; return checkBox; case EXTENSION_WIDGET_DROPDOWN: comboBox = new QComboBox( this ); comboBox->setEditable( false ); for( p_value = p_widget->p_values; p_value != NULL; p_value = p_value->p_next ) { comboBox->addItem( qfu( p_value->psz_text ), p_value->i_id ); } /* Set current item */ if( p_widget->psz_text ) { int idx = comboBox->findText( qfu( p_widget->psz_text ) ); if( idx >= 0 ) comboBox->setCurrentIndex( idx ); } selectMapper->setMapping( comboBox, new WidgetMapper( comboBox, p_widget ) ); CONNECT( comboBox, currentIndexChanged( const QString& ), selectMapper, map() ); return comboBox; case EXTENSION_WIDGET_LIST: list = new QListWidget( this ); list->setSelectionMode( QAbstractItemView::ExtendedSelection ); for( p_value = p_widget->p_values; p_value != NULL; p_value = p_value->p_next ) { QListWidgetItem *item = new QListWidgetItem( qfu( p_value->psz_text ) ); item->setData( Qt::UserRole, p_value->i_id ); list->addItem( item ); } selectMapper->setMapping( list, new WidgetMapper( list, p_widget ) ); CONNECT( list, itemSelectionChanged(), selectMapper, map() ); return list; case EXTENSION_WIDGET_SPIN_ICON: spinIcon = new SpinningIcon( this ); spinIcon->play( p_widget->i_spin_loops ); p_widget->p_sys_intf = spinIcon; return spinIcon; default: msg_Err( p_intf, "Widget type %d unknown", p_widget->type ); return NULL; } } /** * Forward click event to the extension * @param object A WidgetMapper, whose data() is the p_widget **/ int ExtensionDialog::TriggerClick( QObject *object ) { assert( object != NULL ); WidgetMapper *mapping = static_cast< WidgetMapper* >( object ); extension_widget_t *p_widget = mapping->getWidget(); QCheckBox *checkBox = NULL; int i_ret = VLC_EGENERIC; bool lockedHere = false; if( !has_lock ) { vlc_mutex_lock( &p_dialog->lock ); has_lock = true; lockedHere = true; } switch( p_widget->type ) { case EXTENSION_WIDGET_BUTTON: i_ret = extension_WidgetClicked( p_dialog, p_widget ); break; case EXTENSION_WIDGET_CHECK_BOX: checkBox = static_cast< QCheckBox* >( p_widget->p_sys_intf ); p_widget->b_checked = checkBox->isChecked(); i_ret = VLC_SUCCESS; break; default: msg_Dbg( p_intf, "A click event was triggered by a wrong widget" ); break; } if( lockedHere ) { vlc_mutex_unlock( &p_dialog->lock ); has_lock = false; } return i_ret; } /** * Synchronize psz_text with the widget's text() value on update * @param object A WidgetMapper **/ void ExtensionDialog::SyncInput( QObject *object ) { assert( object != NULL ); bool lockedHere = false; if( !has_lock ) { vlc_mutex_lock( &p_dialog->lock ); has_lock = true; lockedHere = true; } WidgetMapper *mapping = static_cast< WidgetMapper* >( object ); extension_widget_t *p_widget = mapping->getWidget(); assert( p_widget->type == EXTENSION_WIDGET_TEXT_FIELD || p_widget->type == EXTENSION_WIDGET_PASSWORD ); /* Synchronize psz_text with the new value */ QLineEdit *widget = static_cast< QLineEdit* >( p_widget->p_sys_intf ); char *psz_text = widget->text().isNull() ? NULL : strdup( qtu( widget->text() ) ); free( p_widget->psz_text ); p_widget->psz_text = psz_text; if( lockedHere ) { vlc_mutex_unlock( &p_dialog->lock ); has_lock = false; } } /** * Synchronize parameter b_selected in the values list * @param object A WidgetMapper **/ void ExtensionDialog::SyncSelection( QObject *object ) { assert( object != NULL ); struct extension_widget_t::extension_widget_value_t *p_value; bool lockedHere = false; if( !has_lock ) { vlc_mutex_lock( &p_dialog->lock ); has_lock = true; lockedHere = true; } WidgetMapper *mapping = static_cast< WidgetMapper* >( object ); extension_widget_t *p_widget = mapping->getWidget(); assert( p_widget->type == EXTENSION_WIDGET_DROPDOWN || p_widget->type == EXTENSION_WIDGET_LIST ); if( p_widget->type == EXTENSION_WIDGET_DROPDOWN ) { QComboBox *combo = static_cast< QComboBox* >( p_widget->p_sys_intf ); for( p_value = p_widget->p_values; p_value != NULL; p_value = p_value->p_next ) { // if( !qstrcmp( p_value->psz_text, qtu( combo->currentText() ) ) ) if( combo->itemData( combo->currentIndex(), Qt::UserRole ).toInt() == p_value->i_id ) { p_value->b_selected = true; } else { p_value->b_selected = false; } } free( p_widget->psz_text ); p_widget->psz_text = strdup( qtu( combo->currentText() ) ); } else if( p_widget->type == EXTENSION_WIDGET_LIST ) { QListWidget *list = static_cast( p_widget->p_sys_intf ); QList selection = list->selectedItems(); for( p_value = p_widget->p_values; p_value != NULL; p_value = p_value->p_next ) { bool b_selected = false; foreach( const QListWidgetItem *item, selection ) { // if( !qstrcmp( qtu( item->text() ), p_value->psz_text ) ) if( item->data( Qt::UserRole ).toInt() == p_value->i_id ) { b_selected = true; break; } } p_value->b_selected = b_selected; } } if( lockedHere ) { vlc_mutex_unlock( &p_dialog->lock ); has_lock = false; } } void ExtensionDialog::UpdateWidgets() { assert( p_dialog ); extension_widget_t *p_widget; FOREACH_ARRAY( p_widget, p_dialog->widgets ) { if( !p_widget ) continue; /* Some widgets may be NULL at this point */ QWidget *widget; int row = p_widget->i_row - 1; int col = p_widget->i_column - 1; if( row < 0 ) { row = layout->rowCount(); col = 0; } else if( col < 0 ) col = layout->columnCount(); int hsp = __MAX( 1, p_widget->i_horiz_span ); int vsp = __MAX( 1, p_widget->i_vert_span ); if( !p_widget->p_sys_intf && !p_widget->b_kill ) { widget = CreateWidget( p_widget ); if( !widget ) { msg_Warn( p_intf, "Could not create a widget for dialog %s", p_dialog->psz_title ); continue; } widget->setVisible( !p_widget->b_hide ); layout->addWidget( widget, row, col, vsp, hsp ); if( ( p_widget->i_width > 0 ) && ( p_widget->i_height > 0 ) ) widget->resize( p_widget->i_width, p_widget->i_height ); p_widget->p_sys_intf = widget; this->resize( sizeHint() ); /* If an update was required, cancel it as we just created the widget */ p_widget->b_update = false; } else if( p_widget->p_sys_intf && !p_widget->b_kill && p_widget->b_update ) { widget = UpdateWidget( p_widget ); if( !widget ) { msg_Warn( p_intf, "Could not update a widget for dialog %s", p_dialog->psz_title ); return; } widget->setVisible( !p_widget->b_hide ); layout->addWidget( widget, row, col, vsp, hsp ); if( ( p_widget->i_width > 0 ) && ( p_widget->i_height > 0 ) ) widget->resize( p_widget->i_width, p_widget->i_height ); p_widget->p_sys_intf = widget; this->resize( sizeHint() ); /* Do not update again */ p_widget->b_update = false; } else if( p_widget->p_sys_intf && p_widget->b_kill ) { DestroyWidget( p_widget ); p_widget->p_sys_intf = NULL; this->resize( sizeHint() ); } } FOREACH_END() } QWidget* ExtensionDialog::UpdateWidget( extension_widget_t *p_widget ) { QLabel *label = NULL; QPushButton *button = NULL; QTextBrowser *textArea = NULL; QLineEdit *textInput = NULL; QCheckBox *checkBox = NULL; QComboBox *comboBox = NULL; QListWidget *list = NULL; SpinningIcon *spinIcon = NULL; struct extension_widget_t::extension_widget_value_t *p_value = NULL; assert( p_widget->p_sys_intf != NULL ); switch( p_widget->type ) { case EXTENSION_WIDGET_LABEL: label = static_cast< QLabel* >( p_widget->p_sys_intf ); label->setText( qfu( p_widget->psz_text ) ); return label; case EXTENSION_WIDGET_BUTTON: // FIXME: looks like removeMappings does not work button = static_cast< QPushButton* >( p_widget->p_sys_intf ); button->setText( qfu( p_widget->psz_text ) ); clickMapper->removeMappings( button ); clickMapper->setMapping( button, new WidgetMapper( button, p_widget ) ); CONNECT( button, clicked(), clickMapper, map() ); return button; case EXTENSION_WIDGET_IMAGE: label = static_cast< QLabel* >( p_widget->p_sys_intf ); label->setPixmap( QPixmap( qfu( p_widget->psz_text ) ) ); return label; case EXTENSION_WIDGET_HTML: textArea = static_cast< QTextBrowser* >( p_widget->p_sys_intf ); textArea->setHtml( qfu( p_widget->psz_text ) ); return textArea; case EXTENSION_WIDGET_TEXT_FIELD: textInput = static_cast< QLineEdit* >( p_widget->p_sys_intf ); textInput->setText( qfu( p_widget->psz_text ) ); return textInput; case EXTENSION_WIDGET_PASSWORD: textInput = static_cast< QLineEdit* >( p_widget->p_sys_intf ); textInput->setText( qfu( p_widget->psz_text ) ); return textInput; case EXTENSION_WIDGET_CHECK_BOX: checkBox = static_cast< QCheckBox* >( p_widget->p_sys_intf ); checkBox->setText( qfu( p_widget->psz_text ) ); checkBox->setChecked( p_widget->b_checked ); return checkBox; case EXTENSION_WIDGET_DROPDOWN: comboBox = static_cast< QComboBox* >( p_widget->p_sys_intf ); // method widget:clear() if ( p_widget->p_values == NULL ) { comboBox->clear(); return comboBox; } // method widget:addvalue() for( p_value = p_widget->p_values; p_value != NULL; p_value = p_value->p_next ) { if ( comboBox->findText( qfu( p_value->psz_text ) ) < 0 ) comboBox->addItem( qfu( p_value->psz_text ), p_value->i_id ); } return comboBox; case EXTENSION_WIDGET_LIST: list = static_cast< QListWidget* >( p_widget->p_sys_intf ); list->clear(); for( p_value = p_widget->p_values; p_value != NULL; p_value = p_value->p_next ) { QListWidgetItem *item = new QListWidgetItem( qfu( p_value->psz_text ) ); item->setData( Qt::UserRole, p_value->i_id ); list->addItem( item ); } return list; case EXTENSION_WIDGET_SPIN_ICON: spinIcon = static_cast< SpinningIcon* >( p_widget->p_sys_intf ); if( !spinIcon->isPlaying() && p_widget->i_spin_loops != 0 ) spinIcon->play( p_widget->i_spin_loops ); else if( spinIcon->isPlaying() && p_widget->i_spin_loops == 0 ) spinIcon->stop(); p_widget->i_height = p_widget->i_width = 16; return spinIcon; default: msg_Err( p_intf, "Widget type %d unknown", p_widget->type ); return NULL; } } void ExtensionDialog::DestroyWidget( extension_widget_t *p_widget, bool b_cond ) { assert( p_widget && p_widget->b_kill ); QWidget *widget = static_cast< QWidget* >( p_widget->p_sys_intf ); delete widget; p_widget->p_sys_intf = NULL; if( b_cond ) vlc_cond_signal( &p_dialog->cond ); } /** Implement closeEvent() in order to intercept the event */ void ExtensionDialog::closeEvent( QCloseEvent * ) { assert( p_dialog != NULL ); msg_Dbg( p_intf, "Dialog '%s' received a closeEvent", p_dialog->psz_title ); extension_DialogClosed( p_dialog ); } /** Grab some keyboard input (ESC, ...) and handle actions manually */ void ExtensionDialog::keyPressEvent( QKeyEvent *event ) { assert( p_dialog != NULL ); switch( event->key() ) { case Qt::Key_Escape: close(); return; default: QDialog::keyPressEvent( event ); return; } } void ExtensionDialog::parentDestroyed() { msg_Dbg( p_intf, "About to destroy dialog '%s'", p_dialog->psz_title ); deleteLater(); // May not work at this point (event loop can be ended) p_dialog->p_sys_intf = NULL; vlc_cond_signal( &p_dialog->cond ); }