From 8b75d973bb6a7f4457623f3681a4991ba5f9ab35 Mon Sep 17 00:00:00 2001 From: Lyndon Brown Date: Sun, 28 Apr 2019 20:55:00 +0100 Subject: [PATCH] qt: add expert preferences mode This gives a table listing all options, similar to and inspired by the `about:config` interface in Firefox. Unique benefits: - It highlights which options have been modified from default state. - It allows selective resetting of individual options to default values. Support is included to toggle booleans with a simple double-click. Double-clicking on other types opens the edit dialog. For colour selection items, the modify action directly opens the Qt colour selection dialog rather than present the colour control used in the advanced preferences view, since this is much cleaner. Note that the existing simple and advanced views are not linked; if you change an option in one view, that change is not reflected in the other, and saving changes only uses the state from the selected view. The same is currently true of the new expert mode, though I plan to later change this behaviour (for all three). Note also that hotkey items are deliberately excluded from this view. The dedicated hotkey editor is best suited to managing hotkeys. It does not work well to include the set of 224 unique hotkey options within this table, especially since we'd have to duplicate the code checking for duplicate assignments if we allow editing of them as with all other option types within this interface. It may seem odd for the 'expert' mode to be the only one without hotkey editing, however the hotkey editor does not really fit well into 'advanced' mode either, and I have plans to propose separating the hotkey editor entirely from within the set of three views in a small redesign. Fixes #18607. --- modules/gui/qt/Makefile.am | 6 + .../qt/dialogs/preferences/expert_model.cpp | 434 ++++++++++++++++++ .../qt/dialogs/preferences/expert_model.hpp | 133 ++++++ .../qt/dialogs/preferences/expert_view.cpp | 281 ++++++++++++ .../qt/dialogs/preferences/expert_view.hpp | 101 ++++ .../qt/dialogs/preferences/preferences.cpp | 104 ++++- .../qt/dialogs/preferences/preferences.hpp | 18 +- .../preferences/preferences_widgets.cpp | 19 + .../preferences/preferences_widgets.hpp | 5 + po/POTFILES.in | 4 + 10 files changed, 1100 insertions(+), 5 deletions(-) create mode 100644 modules/gui/qt/dialogs/preferences/expert_model.cpp create mode 100644 modules/gui/qt/dialogs/preferences/expert_model.hpp create mode 100644 modules/gui/qt/dialogs/preferences/expert_view.cpp create mode 100644 modules/gui/qt/dialogs/preferences/expert_view.hpp diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am index 3a0b6fa3c7..01b16daaa0 100644 --- a/modules/gui/qt/Makefile.am +++ b/modules/gui/qt/Makefile.am @@ -110,6 +110,10 @@ libqt_plugin_la_SOURCES = \ gui/qt/dialogs/podcast/podcast_configuration.hpp \ gui/qt/dialogs/preferences/complete_preferences.cpp \ gui/qt/dialogs/preferences/complete_preferences.hpp \ + gui/qt/dialogs/preferences/expert_model.cpp \ + gui/qt/dialogs/preferences/expert_model.hpp \ + gui/qt/dialogs/preferences/expert_view.cpp \ + gui/qt/dialogs/preferences/expert_view.hpp \ gui/qt/dialogs/preferences/preferences.cpp \ gui/qt/dialogs/preferences/preferences.hpp \ gui/qt/dialogs/preferences/preferences_widgets.cpp \ @@ -368,6 +372,8 @@ nodist_libqt_plugin_la_SOURCES = \ gui/qt/dialogs/plugins/plugins.moc.cpp \ gui/qt/dialogs/podcast/podcast_configuration.moc.cpp \ gui/qt/dialogs/preferences/complete_preferences.moc.cpp \ + gui/qt/dialogs/preferences/expert_model.moc.cpp \ + gui/qt/dialogs/preferences/expert_view.moc.cpp \ gui/qt/dialogs/preferences/preferences.moc.cpp \ gui/qt/dialogs/preferences/preferences_widgets.moc.cpp \ gui/qt/dialogs/preferences/simple_preferences.moc.cpp \ diff --git a/modules/gui/qt/dialogs/preferences/expert_model.cpp b/modules/gui/qt/dialogs/preferences/expert_model.cpp new file mode 100644 index 0000000000..9776189fd7 --- /dev/null +++ b/modules/gui/qt/dialogs/preferences/expert_model.cpp @@ -0,0 +1,434 @@ +/***************************************************************************** + * expert_model.cpp : Detailed preferences overview - model + ***************************************************************************** + * Copyright (C) 2019-2022 VLC authors and VideoLAN + * + * Authors: Lyndon Brown + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + *****************************************************************************/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "expert_model.hpp" +#include "preferences_widgets.hpp" + +#include +#include + +#define COLUMN_COUNT 4 + +/********************************************************************* + * Model Item + *********************************************************************/ +ExpertPrefsTableItem::ExpertPrefsTableItem( module_config_t *config, + const QString &mod_name, + const QString &mod_name_pretty, + bool is_core ) +{ + cfg_item = config; + + if( CONFIG_CLASS( getType() ) == CONFIG_ITEM_STRING && + config->value.psz != nullptr ) + { + config->value.psz = strdup( config->value.psz ); + } + + /* Create table entry name from dot-prefixing module name to option name. + Many plugins dash-prefix their name to their option names, which we must + skip over here to replace the dash with a dot. */ + QString option_name = config->psz_name; + int mod_name_len = mod_name.length(); + if( !is_core && option_name.length() > mod_name_len && + option_name.startsWith( mod_name ) && option_name[mod_name_len] == '-' ) + option_name.remove( 0, mod_name_len + 1 ); + name = QString( "%1.%2" ).arg( mod_name ).arg( option_name ); + /* Some options use underscores when they shouldn't. Let's not pointlessly + leak that imperfection into our interface. */ + name.replace( "_", "-" ); + + /* Create "title" text label for info panel. */ + title = QString( "%1 :: %2" ).arg( mod_name_pretty ).arg( config->psz_text ); + description = QString(); /* We'll use lazy creation */ + + updateMatchesDefault(); + updateValueDisplayString(); +} + +ExpertPrefsTableItem::~ExpertPrefsTableItem() +{ + if( CONFIG_CLASS( getType() ) == CONFIG_ITEM_STRING ) + { + free( cfg_item->value.psz ); + cfg_item->value.psz = nullptr; + } +} + +const QString &ExpertPrefsTableItem::getDescription() +{ + if( description.isNull() ) /* Lazy creation */ + { + if( cfg_item->psz_longtext ) + description = qfut( cfg_item->psz_longtext ); + else if( cfg_item->psz_text ) + description = qfut( cfg_item->psz_text ); + else + description = QStringLiteral( u"" ); + } + return description; +} + +void ExpertPrefsTableItem::updateMatchesDefault() +{ + switch ( CONFIG_CLASS( getType() ) ) + { + case CONFIG_ITEM_FLOAT: + matches_default = (cfg_item->value.f == cfg_item->orig.f); + break; + case CONFIG_ITEM_BOOL: + case CONFIG_ITEM_INTEGER: + matches_default = (cfg_item->value.i == cfg_item->orig.i); + break; + case CONFIG_ITEM_STRING: + { + bool orig_is_empty = (cfg_item->orig.psz == nullptr || cfg_item->orig.psz[0] == '\0'); + bool curr_is_empty = (cfg_item->value.psz == nullptr || cfg_item->value.psz[0] == '\0'); + if (orig_is_empty) + matches_default = curr_is_empty; + else if( curr_is_empty ) + matches_default = false; + else + matches_default = (strcmp( cfg_item->value.psz, cfg_item->orig.psz ) == 0); + break; + } + default: + vlc_assert_unreachable(); + break; + } +} + +void ExpertPrefsTableItem::updateValueDisplayString() +{ + switch ( CONFIG_CLASS( getType() ) ) + { + case CONFIG_ITEM_BOOL: + /* Do nothing - set at the model level to allow reusing cached translation lookup */ + break; + case CONFIG_ITEM_FLOAT: + displayed_value = QString( "%L1" ).arg( cfg_item->value.f, 0, 'g' ); + break; + case CONFIG_ITEM_INTEGER: + if( cfg_item->i_type == CONFIG_ITEM_RGB ) + { + QString hex_upper = QString( "%1" ).arg( cfg_item->value.i, 0, 16 ).toUpper(); + displayed_value = QString( "0x" ).append( hex_upper ); + } + else + displayed_value = QString( "%L1" ).arg( cfg_item->value.i ); + break; + case CONFIG_ITEM_STRING: + if( cfg_item->i_type != CONFIG_ITEM_PASSWORD ) + displayed_value = cfg_item->value.psz; + else if( cfg_item->value.psz && *cfg_item->value.psz != '\0' ) + displayed_value = QStringLiteral( u"•••••" ); + else + displayed_value = QStringLiteral( u"" ); + break; + default: + break; + } +} + +void ExpertPrefsTableItem::setToDefault() +{ + /* Note, this modifies our local copy of the item only */ + if( CONFIG_CLASS( getType() ) == CONFIG_ITEM_STRING ) + { + free( cfg_item->value.psz ); + cfg_item->value.psz = (cfg_item->orig.psz) ? strdup( cfg_item->orig.psz ) : nullptr; + } + else + cfg_item->value = cfg_item->orig; + + matches_default = true; + updateValueDisplayString(); +} + +void ExpertPrefsTableItem::toggleBoolean() +{ + assert( CONFIG_CLASS( getType() ) == CONFIG_ITEM_BOOL ); + /* Note, this modifies our local copy of the item only */ + cfg_item->value.i = !cfg_item->value.i; + updateMatchesDefault(); + /* Note, display text is updated by the model for efficiency */ +} + +/* search name and value columns */ +bool ExpertPrefsTableItem::contains( const QString &text, Qt::CaseSensitivity cs ) +{ + return ( name.contains( text, cs ) || displayed_value.contains( text, cs ) ); +} + +/********************************************************************* + * Model + *********************************************************************/ +ExpertPrefsTableModel::ExpertPrefsTableModel( module_t **mod_list, size_t mod_count, + QWidget *parent_ ) : + QAbstractListModel( parent_ ) +{ + /* Cache translations of common text */ + state_same_text = qtr( "default" ); + state_different_text = qtr( "modified" ); + true_text = qtr( "true" ); + false_text = qtr( "false" ); + type_boolean_text = qtr( "boolean" ); + type_float_text = qtr( "float" ); + type_integer_text = qtr( "integer" ); + type_color_text = qtr( "color" ); + type_string_text = qtr( "string" ); + type_password_text = qtr( "password" ); + type_module_text = qtr( "module" ); + type_module_list_text = qtr( "module-list" ); + type_file_text = qtr( "file" ); + type_directory_text = qtr( "directory" ); + type_font_text = qtr( "font" ); + type_unknown_text = qtr( "unknown" ); + + items.reserve( 1400 ); + for( size_t i = 0; i < mod_count; i++ ) + { + module_t *mod = mod_list[i]; + + unsigned confsize; + module_config_t *const config = module_config_get( mod, &confsize ); + if( confsize == 0 ) + continue; + config_sets.append( config ); + + bool is_core = module_is_main( mod ); + QString mod_name = module_get_object( mod ); + QString mod_name_pretty = is_core ? qtr( "Core" ) : module_GetShortName( mod ); + + enum vlc_config_subcat subcat = SUBCAT_UNKNOWN; + + for( size_t j = 0; j < confsize; j++ ) + { + module_config_t *cfg_item = config + j; + + if( cfg_item->i_type == CONFIG_SUBCATEGORY ) + { + subcat = (enum vlc_config_subcat) cfg_item->value.i; + continue; + } + if( subcat == SUBCAT_UNKNOWN || subcat == SUBCAT_HIDDEN ) + continue; + /* Exclude hotkey items in favour of them being edited exclusively + via the dedicated hotkey editor. */ + if( cfg_item->i_type == CONFIG_ITEM_KEY ) + continue; + + if( CONFIG_ITEM( cfg_item->i_type ) ) + { + ExpertPrefsTableItem *item = + new ExpertPrefsTableItem( cfg_item, mod_name, mod_name_pretty, is_core ); + + if( CONFIG_CLASS( cfg_item->i_type ) == CONFIG_ITEM_BOOL ) + { + /* Set the translated display text from cached lookup */ + item->displayed_value = ( cfg_item->value.i == 0 ) ? false_text : true_text; + } + + /* Sorted list insertion */ + int insert_index = 0; + while( insert_index < items.count() ) + { + if( item->name.compare( items[insert_index]->name ) < 0 ) + break; + insert_index++; + } + items.insert( insert_index, item ); + } + } + } +}; + +ExpertPrefsTableModel::~ExpertPrefsTableModel() +{ + items.clear(); /* We must destroy the items before releasing the config set */ + foreach ( module_config_t *config_set, config_sets ) + module_config_free( config_set ); +} + +QVariant ExpertPrefsTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if( orientation == Qt::Horizontal && role == Qt::DisplayRole ) + { + switch ( section ) + { + case NameField: return qtr( "Option" ); + case StateField: return qtr( "Status" ); + case TypeField: return qtr( "Type" ); + case ValueField: return qtr( "Value" ); + } + } + return QVariant(); +} + +int ExpertPrefsTableModel::rowCount( const QModelIndex &parent ) const +{ + return parent.isValid() ? 0 : items.count(); +} + +int ExpertPrefsTableModel::columnCount( const QModelIndex &parent ) const +{ + return parent.isValid() ? 0 : COLUMN_COUNT; +} + +QVariant ExpertPrefsTableModel::data( const QModelIndex &index, int role ) const +{ + ExpertPrefsTableItem *item = itemAt( index ); + switch ( role ) + { + case Qt::DisplayRole: + { + switch ( index.column() ) + { + case NameField: return item->name; + case ValueField: return item->displayed_value; + case StateField: return (item->matches_default) ? state_same_text + : state_different_text; + case TypeField: + { + switch ( item->cfg_item->i_type ) + { + case CONFIG_ITEM_BOOL: return type_boolean_text; break; + case CONFIG_ITEM_FLOAT: return type_float_text; break; + case CONFIG_ITEM_INTEGER: return type_integer_text; break; + case CONFIG_ITEM_RGB: return type_color_text; break; + case CONFIG_ITEM_STRING: return type_string_text; break; + case CONFIG_ITEM_PASSWORD: return type_password_text; break; + case CONFIG_ITEM_MODULE_CAT: + case CONFIG_ITEM_MODULE: return type_module_text; break; + case CONFIG_ITEM_MODULE_LIST_CAT: + case CONFIG_ITEM_MODULE_LIST: return type_module_list_text; break; + case CONFIG_ITEM_LOADFILE: + case CONFIG_ITEM_SAVEFILE: return type_file_text; break; + case CONFIG_ITEM_DIRECTORY: return type_directory_text; break; + case CONFIG_ITEM_FONT: return type_font_text; break; + default: return type_unknown_text; break; + } + } + } + break; + } + case Qt::FontRole: + { + QFont font = QFont(); + font.setBold( (item->matches_default) ? false : true ); + return font; + } + case TypeClassRole: + return CONFIG_CLASS( item->getType() ); + case CopyValueRole: + { + switch ( CONFIG_CLASS( item->getType() ) ) + { + case CONFIG_ITEM_BOOL: + // Note, no translation wanted here! + return (item->cfg_item->value.i == 0) ? QStringLiteral( u"false") + : QStringLiteral( u"true" ); + case CONFIG_ITEM_FLOAT: + return QString( "%1" ).arg( item->cfg_item->value.f ); + case CONFIG_ITEM_INTEGER: + // For RGB it is presumably more useful to give a hex form that can be + // copy-pasted into a colour selection dialog rather than the raw int. + if( item->getType() == CONFIG_ITEM_RGB ) + return QString( "#%1" ).arg( item->cfg_item->value.i, 0, 16 ); + return QString( "%1" ).arg( item->cfg_item->value.i ); + case CONFIG_ITEM_STRING: + return item->cfg_item->value.psz; + default: + break; + } + } + default: break; + } + return QVariant(); +} + +void ExpertPrefsTableModel::toggleBoolean( const QModelIndex &index ) +{ + ExpertPrefsTableItem *item = itemAt( index ); + item->toggleBoolean(); + /* Set the translated display text from cached lookup */ + item->displayed_value = ( item->cfg_item->value.i == 0 ) ? false_text : true_text; + notifyUpdatedRow( index.row() ); +} + +void ExpertPrefsTableModel::setItemToDefault( const QModelIndex &index ) +{ + ExpertPrefsTableItem *item = itemAt( index ); + item->setToDefault(); + if( CONFIG_CLASS( item->cfg_item->i_type ) == CONFIG_ITEM_BOOL ) + { + /* Set the translated display text from cached lookup */ + item->displayed_value = ( item->cfg_item->value.i == 0 ) ? false_text : true_text; + } + notifyUpdatedRow( index.row() ); +} + +void ExpertPrefsTableModel::notifyUpdatedRow( int row ) +{ + emit dataChanged( index( row, 0 ), index( row, COLUMN_COUNT - 1 ) ); +} + +bool ExpertPrefsTableModel::submit() +{ + for( int i= 0 ; i < items.count(); i++ ) + { + ExpertPrefsTableItem *item = items[i]; + + /* save from copy to actual */ + switch ( CONFIG_CLASS( item->getType() ) ) + { + case CONFIG_ITEM_BOOL: + case CONFIG_ITEM_INTEGER: + config_PutInt( item->cfg_item->psz_name, item->cfg_item->value.i ); + break; + case CONFIG_ITEM_FLOAT: + config_PutFloat( item->cfg_item->psz_name, item->cfg_item->value.f ); + break; + case CONFIG_ITEM_STRING: + config_PutPsz( item->cfg_item->psz_name, item->cfg_item->value.psz ); + break; + } + } + return true; +} diff --git a/modules/gui/qt/dialogs/preferences/expert_model.hpp b/modules/gui/qt/dialogs/preferences/expert_model.hpp new file mode 100644 index 0000000000..2d6195fa6f --- /dev/null +++ b/modules/gui/qt/dialogs/preferences/expert_model.hpp @@ -0,0 +1,133 @@ +/***************************************************************************** + * expert_model.hpp : Detailed preferences overview - model + ***************************************************************************** + * Copyright (C) 2019-2022 VLC authors and VideoLAN + * + * Authors: Lyndon Brown + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + *****************************************************************************/ + +#ifndef VLC_QT_EXPERT_PREFERENCES_MODEL_HPP_ +#define VLC_QT_EXPERT_PREFERENCES_MODEL_HPP_ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include + +#include + +#include "qt.hpp" + +class ConfigControl; +class ExpertPrefsTableModel; + +class ExpertPrefsTableItem +{ + friend ExpertPrefsTableModel; + +public: + ExpertPrefsTableItem( module_config_t *, const QString &, const QString &, bool ); + ~ExpertPrefsTableItem(); + int getType() const { return cfg_item->i_type; } + const QString &getTitle() const { return title; } + const QString &getDescription(); + module_config_t *getConfig() const { return cfg_item; } + bool matchesDefault() const { return matches_default; } + void updateMatchesDefault(); + void updateValueDisplayString(); + /** Search filter helper */ + bool contains( const QString &text, Qt::CaseSensitivity cs ); + +private: + void setToDefault(); + void toggleBoolean(); + /** Name shown in table entry, combining module name and option name */ + QString name; + /** The displayed value text */ + QString displayed_value; + /** "Pretty" name for info panel */ + QString title; + /** Description for info panel */ + QString description; + /** The local copy of the corresponding option */ + module_config_t *cfg_item; + /** Is item state different to the default value? */ + bool matches_default; +}; + +class ExpertPrefsTableModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ExpertPrefsTableModel( module_t **, size_t, QWidget * = nullptr ); + ~ExpertPrefsTableModel(); + enum ItemField + { + NameField, + StateField, + TypeField, + ValueField, + }; + enum DataRoles + { + TypeClassRole = Qt::UserRole, + CopyValueRole + }; + ExpertPrefsTableItem *itemAt( int row ) const + { + assert( row < items.count() ); + return items[ row ]; + } + ExpertPrefsTableItem *itemAt( const QModelIndex &index ) const + { + return itemAt( index.row() ); + } + void toggleBoolean( const QModelIndex & ); + void setItemToDefault( const QModelIndex & ); + void notifyUpdatedRow( int ); + bool submit(); + /* Standard model interface */ + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + +private: + QList config_sets; + QList items; + /* Cached translations of common text */ + QString state_same_text; + QString state_different_text; + QString true_text; + QString false_text; + QString type_boolean_text; + QString type_float_text; + QString type_integer_text; + QString type_color_text; + QString type_string_text; + QString type_password_text; + QString type_module_text; + QString type_module_list_text; + QString type_file_text; + QString type_directory_text; + QString type_font_text; + QString type_unknown_text; +}; + +#endif diff --git a/modules/gui/qt/dialogs/preferences/expert_view.cpp b/modules/gui/qt/dialogs/preferences/expert_view.cpp new file mode 100644 index 0000000000..6480ab8077 --- /dev/null +++ b/modules/gui/qt/dialogs/preferences/expert_view.cpp @@ -0,0 +1,281 @@ +/***************************************************************************** + * expert_view.cpp : Detailed preferences overview - view + ***************************************************************************** + * Copyright (C) 2019-2022 VLC authors and VideoLAN + * + * Authors: Lyndon Brown + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + *****************************************************************************/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "expert_view.hpp" +#include "preferences_widgets.hpp" + +#include +#include + +#define COLUMN_COUNT 4 +/********************************************************************* + * The Table + *********************************************************************/ +ExpertPrefsTable::ExpertPrefsTable( QWidget *parent ) : + QTreeView( parent ) +{ + setSelectionBehavior( QAbstractItemView::SelectRows ); + setSelectionMode( QAbstractItemView::SingleSelection ); + setAlternatingRowColors( true ); + + setStyleSheet( "QTreeView::item { padding: 9px 0; }" ); + + /* edit sub-dialog (reusable) */ + expert_edit = new ExpertPrefsEditDialog( this ); + + connect( this, &QAbstractItemView::doubleClicked, this, &ExpertPrefsTable::doubleClicked ); + + /* context menu actions */ + reset_action = new QAction( qtr( "&Reset" ), this ); + toggle_action = new QAction( qtr( "&Toggle" ), this ); + modify_action = new QAction( qtr( "&Modify" ), this ); + copy_name_action = new QAction( qtr( "Copy &name" ), this ); + copy_value_action = new QAction( qtr( "Copy &value" ), this ); + + connect( reset_action, &QAction::triggered, this, &ExpertPrefsTable::resetItem ); + connect( toggle_action, &QAction::triggered, this, QOverload<>::of(&ExpertPrefsTable::toggleItem) ); + connect( modify_action, &QAction::triggered, this, QOverload<>::of(&ExpertPrefsTable::modifyItem) ); + connect( copy_name_action, &QAction::triggered, this, &ExpertPrefsTable::copyItemName ); + connect( copy_value_action, &QAction::triggered, this, &ExpertPrefsTable::copyItemValue ); +} + +void ExpertPrefsTable::applyAll() +{ + model()->submit(); +} + +/* apply filter on tree */ +void ExpertPrefsTable::filter( const QString &text, bool modified_only ) +{ + bool text_nonempty = !text.isEmpty(); + + ExpertPrefsTableModel *model = myModel(); + for( int i = 0 ; i < model->rowCount(); i++ ) + { + ExpertPrefsTableItem *item = model->itemAt( i ); + bool hide = ( ( modified_only && item->matchesDefault() ) || + ( text_nonempty && !item->contains( text, Qt::CaseInsensitive ) ) ); + setRowHidden( i, QModelIndex(), hide ); + } +} + +#ifndef QT_NO_CONTEXTMENU +void ExpertPrefsTable::contextMenuEvent( QContextMenuEvent *event ) +{ + QModelIndex index = currentIndex(); + if( !index.isValid() || isRowHidden( index.row(), QModelIndex() ) ) + return; + /* Avoid menu from right-click on empty space after last item */ + if( event->reason() == QContextMenuEvent::Mouse && + !indexAt( viewport()->mapFromGlobal( event->globalPos() ) ).isValid() ) + return; + + ExpertPrefsTableItem *item = myModel()->itemAt( index ); + + QMenu *menu = new QMenu(); + menu->setAttribute(Qt::WA_DeleteOnClose); + + if( CONFIG_CLASS( item->getType() ) == CONFIG_ITEM_BOOL ) + menu->addAction( toggle_action ); + else + menu->addAction( modify_action ); + menu->addSeparator(); + menu->addAction( copy_name_action ); + menu->addAction( copy_value_action ); + copy_value_action->setEnabled( item->getType() != CONFIG_ITEM_PASSWORD ); + menu->addSeparator(); + menu->addAction( reset_action ); + reset_action->setEnabled( !item->matchesDefault() ); + + menu->popup( event->globalPos() ); +} +#endif // QT_NO_CONTEXTMENU + +void ExpertPrefsTable::resetItem() +{ + QModelIndex index = currentIndex(); + if( !index.isValid() ) + return; + myModel()->setItemToDefault( index ); +} + +void ExpertPrefsTable::toggleItem() +{ + toggleItem( currentIndex() ); +} + +/* this obviously only applies to boolean options! */ +void ExpertPrefsTable::toggleItem( const QModelIndex &index ) +{ + if( !index.isValid() ) + return; + myModel()->toggleBoolean( index ); +} + +void ExpertPrefsTable::modifyItem() +{ + QModelIndex index = currentIndex(); + if( !index.isValid() ) + return; + modifyItem( index ); +} + +void ExpertPrefsTable::modifyItem( const QModelIndex &index ) +{ + ExpertPrefsTableItem *item = myModel()->itemAt( index ); + module_config_t *cfg_item = item->getConfig(); + /* For colour items it's much cleaner here to directly show a `QColorDialog` + than provide indirect access to one via an `ExpertPrefsEditDialog` with a + `ColorConfigControl`. */ + if( item->getType() == CONFIG_ITEM_RGB ) + { + QColor color = QColorDialog::getColor( QColor( cfg_item->value.i ) ); + if( color.isValid() ) + { + cfg_item->value.i = (color.red() << 16) + (color.green() << 8) + color.blue(); + item->updateMatchesDefault(); + item->updateValueDisplayString(); + myModel()->notifyUpdatedRow( index.row() ); + } + } + else + { + ConfigControl *control = ConfigControl::createControl( cfg_item, nullptr ); + expert_edit->setControl( control, item ); + expert_edit->exec(); + } +} + +void ExpertPrefsTable::copyItemName() +{ + QModelIndex index = currentIndex(); + if( !index.isValid() ) + return; + + QClipboard *clipboard = QGuiApplication::clipboard(); + + QModelIndex name_index = myModel()->index( index.row(), ExpertPrefsTableModel::NameField ); + clipboard->setText( name_index.data( Qt::DisplayRole ).toString() ); +} + +void ExpertPrefsTable::copyItemValue() +{ + QModelIndex index = currentIndex(); + if( !index.isValid() ) + return; + + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText( index.data( ExpertPrefsTableModel::CopyValueRole ).toString() ); +} + +void ExpertPrefsTable::doubleClicked( const QModelIndex &index ) +{ + if( index.data( ExpertPrefsTableModel::TypeClassRole ).toInt() == CONFIG_ITEM_BOOL ) + { + toggleItem( index ); + myModel()->notifyUpdatedRow( index.row() ); + } + else + modifyItem( index ); +} + +/********************************************************************* + * The Edit Dialog + *********************************************************************/ +ExpertPrefsEditDialog::ExpertPrefsEditDialog( ExpertPrefsTable *_table ) : + QDialog( _table ), table( _table ) +{ + table_item = nullptr; + control = nullptr; + control_widget = nullptr; + + setWindowTitle( qtr( "Set option value" ) ); + setWindowRole( "vlc-preferences" ); + setWindowModality( Qt::WindowModal ); + + setMinimumSize( 380, 110 ); + + layout = new QVBoxLayout( this ); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(); + QPushButton *ok = new QPushButton( qtr( "&Ok" ) ); + QPushButton *cancel = new QPushButton( qtr( "&Cancel" ) ); + buttonBox->addButton( ok, QDialogButtonBox::AcceptRole ); + buttonBox->addButton( cancel, QDialogButtonBox::RejectRole ); + layout->addWidget( buttonBox ); + + connect( buttonBox, &QDialogButtonBox::accepted, this, &ExpertPrefsEditDialog::accept ); + connect( buttonBox, &QDialogButtonBox::rejected, this, &ExpertPrefsEditDialog::reject ); + + setLayout( layout ); +} + +void ExpertPrefsEditDialog::setControl( ConfigControl *control_, ExpertPrefsTableItem *table_item_ ) +{ + table_item = table_item_; + control = control_; + control_widget = new QWidget( this ); + control_widget->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed ); + QVBoxLayout *control_layout = new QVBoxLayout( control_widget ); + control->insertInto( control_layout ); + layout->insertWidget( 0, control_widget ); +} + +void ExpertPrefsEditDialog::clearControl() +{ + delete control; + delete control_widget; + control = nullptr; + control_widget = nullptr; + table_item = nullptr; +} + +void ExpertPrefsEditDialog::accept() +{ + control->storeValue(); + table_item->updateMatchesDefault(); + table_item->updateValueDisplayString(); + clearControl(); + QDialog::accept(); +} + +void ExpertPrefsEditDialog::reject() +{ + clearControl(); + QDialog::reject(); +} diff --git a/modules/gui/qt/dialogs/preferences/expert_view.hpp b/modules/gui/qt/dialogs/preferences/expert_view.hpp new file mode 100644 index 0000000000..0657eb4325 --- /dev/null +++ b/modules/gui/qt/dialogs/preferences/expert_view.hpp @@ -0,0 +1,101 @@ +/***************************************************************************** + * expert_view.hpp : Detailed preferences overview - view + ***************************************************************************** + * Copyright (C) 2019-2022 VLC authors and VideoLAN + * + * Authors: Lyndon Brown + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + *****************************************************************************/ + +#ifndef VLC_QT_EXPERT_PREFERENCES_VIEW_HPP_ +#define VLC_QT_EXPERT_PREFERENCES_VIEW_HPP_ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "expert_model.hpp" + +#include +#include + +#include "qt.hpp" + +class QVBoxLayout; +class QAction; +class QContextMenuEvent; + +class ConfigControl; +class ExpertPrefsEditDialog; + +class ExpertPrefsTable : public QTreeView +{ + Q_OBJECT + +public: + ExpertPrefsTable( QWidget *parent = nullptr ); + ExpertPrefsTableModel *myModel() + { + return static_cast( model() ); + } + void applyAll(); + void filter( const QString &text, bool ); + +protected: +#ifndef QT_NO_CONTEXTMENU + void contextMenuEvent(QContextMenuEvent *event) Q_DECL_OVERRIDE; +#endif // QT_NO_CONTEXTMENU + +private: + void modifyItem( const QModelIndex & ); + void toggleItem( const QModelIndex & ); + QAction *reset_action; + QAction *toggle_action; + QAction *modify_action; + QAction *copy_name_action; + QAction *copy_value_action; + ExpertPrefsEditDialog *expert_edit; + +private slots: + void resetItem(); + void toggleItem(); + void modifyItem(); + void copyItemName(); + void copyItemValue(); + void doubleClicked( const QModelIndex & ); +}; + +class ExpertPrefsEditDialog : public QDialog +{ + Q_OBJECT +public: + ExpertPrefsEditDialog( ExpertPrefsTable * ); + void setControl( ConfigControl *, ExpertPrefsTableItem * ); + +private: + void clearControl(); + ExpertPrefsTable *table; + ExpertPrefsTableItem *table_item; + QVBoxLayout *layout; + QWidget *control_widget; + ConfigControl *control; + +private slots: + void accept(); + void reject(); +}; + +#endif diff --git a/modules/gui/qt/dialogs/preferences/preferences.cpp b/modules/gui/qt/dialogs/preferences/preferences.cpp index 62906f7971..0a837e5261 100644 --- a/modules/gui/qt/dialogs/preferences/preferences.cpp +++ b/modules/gui/qt/dialogs/preferences/preferences.cpp @@ -29,6 +29,7 @@ #include "widgets/native/qvlcframe.hpp" #include "dialogs/errors/errors.hpp" +#include "expert_view.hpp" #include "dialogs/preferences/complete_preferences.hpp" #include "dialogs/preferences/simple_preferences.hpp" #include "widgets/native/searchlineedit.hpp" @@ -73,8 +74,11 @@ PrefsDialog::PrefsDialog( QWindow *parent, qt_intf_t *_p_intf ) simple->setToolTip( qtr( "Switch to simple preferences view" ) ); all = new QRadioButton( qtr("All"), types ); all->setToolTip( qtr( "Switch to full preferences view" ) ); + expert = new QRadioButton( qtr("Expert"), types ); + expert->setToolTip( qtr( "Switch to expert preferences view" ) ); types_l->addWidget( simple ); types_l->addWidget( all ); + types_l->addWidget( expert ); types->setLayout( types_l ); simple->setChecked( true ); @@ -131,6 +135,30 @@ PrefsDialog::PrefsDialog( QWindow *parent, qt_intf_t *_p_intf ) stack->insertWidget( ADVANCED, advanced_split_widget ); + /* Expert view (panel) */ + expert_widget = new QWidget; + expert_widget_layout = new QGridLayout; + expert_widget->setLayout( expert_widget_layout ); + + expert_table_filter = nullptr; + expert_table = nullptr; + + expert_text = new QLabel; + expert_longtext = new QLabel; + + expert_text->setWordWrap(true); + expert_longtext->setWordWrap(true); + + QFont textFont = QApplication::font(); + textFont.setPointSize( textFont.pointSize() + 2 ); + textFont.setUnderline( true ); + expert_text->setFont( textFont ); + + expert_widget_layout->addWidget( expert_text, 2, 0, 1, 2 ); + expert_widget_layout->addWidget( expert_longtext, 3, 0, 1, 2 ); + + stack->insertWidget( EXPERT, expert_widget ); + /* Layout */ main_layout->addWidget( stack, 0, 0, 3, 3 ); main_layout->addWidget( types, 3, 0, 2, 1 ); @@ -149,6 +177,7 @@ PrefsDialog::PrefsDialog( QWindow *parent, qt_intf_t *_p_intf ) BUTTONACT( simple, &PrefsDialog::setSimple ); BUTTONACT( all, &PrefsDialog::setAdvanced ); + BUTTONACT( expert, &PrefsDialog::setExpert ); QVLCTools::restoreWidgetPosition( p_intf, "Preferences", this, QSize( 850, 700 ) ); } @@ -158,12 +187,55 @@ PrefsDialog::~PrefsDialog() module_list_free( p_list ); } +void PrefsDialog::setExpert() +{ + /* Lazy creation */ + if( !expert_table ) + { + if ( !p_list ) + p_list = module_list_get( &count ); + + expert_table_filter = new SearchLineEdit( expert_widget ); + expert_table_filter->setMinimumHeight( 26 ); + expert_table_filter_modified = new QCheckBox( qtr( "Modified only" ), expert_widget ); + + QShortcut *search = new QShortcut( QKeySequence( QKeySequence::Find ), expert_table_filter ); + + ExpertPrefsTableModel *table_model = new ExpertPrefsTableModel( p_list, count, this ); + expert_table = new ExpertPrefsTable( expert_widget ); + expert_table->setModel( table_model ); + expert_table->resizeColumnToContents( ExpertPrefsTableModel::NameField ); + + expert_widget_layout->addWidget( expert_table_filter, 0, 0 ); + expert_widget_layout->addWidget( expert_table_filter_modified, 0, 1 ); + expert_widget_layout->addWidget( expert_table, 1, 0, 1, 2 ); + + connect( expert_table->selectionModel(), &QItemSelectionModel::currentChanged, + this, &PrefsDialog::changeExpertDesc ); + connect( expert_table_filter, &SearchLineEdit::textChanged, + this, &PrefsDialog::expertTableFilterChanged ); + connect( expert_table_filter_modified, &QCheckBox::toggled, + this, &PrefsDialog::expertTableFilterModifiedToggled ); + connect( search, &QShortcut::activated, + expert_table_filter, QOverload<>::of(&SearchLineEdit::setFocus) ); + + /* Set initial selection */ + expert_table->setCurrentIndex( + expert_table->model()->index( 0, 0, QModelIndex() ) ); + } + + expert->setChecked( true ); + stack->setCurrentIndex( EXPERT ); + setWindowTitle( qtr( "Expert Preferences" ) ); +} + void PrefsDialog::setAdvanced() { /* Lazy creation */ if( !advanced_tree ) { - p_list = module_list_get( &count ); + if ( !p_list ) + p_list = module_list_get( &count ); advanced_tree = new PrefsTree( p_intf, advanced_tree_panel, p_list, count ); @@ -237,12 +309,23 @@ void PrefsDialog::changeAdvPanel( QTreeWidgetItem *item ) if( !node->panel ) { node->panel = new AdvPrefsPanel( p_intf, advanced_panels_stack, node ); - advanced_panels_stack->insertWidget( advanced_panels_stack->count(), - node->panel ); + advanced_panels_stack->addWidget( node->panel ); } advanced_panels_stack->setCurrentWidget( node->panel ); } +/* Changing from one Expert item description to another */ +void PrefsDialog::changeExpertDesc( const QModelIndex ¤t, const QModelIndex &previous ) +{ + Q_UNUSED( previous ); + if( !current.isValid() ) + return; + ExpertPrefsTableItem *item = expert_table->myModel()->itemAt( current ); + + expert_text->setText( item->getTitle() ); + expert_longtext->setText( item->getDescription() ); +} + /* Actual apply and save for the preferences */ void PrefsDialog::save() { @@ -259,6 +342,11 @@ void PrefsDialog::save() msg_Dbg( p_intf, "Saving the advanced preferences" ); advanced_tree->applyAll(); } + else if( expert->isChecked() && expert_table->isVisible() ) + { + msg_Dbg( p_intf, "Saving the expert preferences" ); + expert_table->applyAll(); + } /* Save to file */ if( config_SaveConfigFile( p_intf ) != 0 ) @@ -309,6 +397,16 @@ void PrefsDialog::reset() } } +void PrefsDialog::expertTableFilterModifiedToggled( bool checked ) +{ + expert_table->filter( expert_table_filter->text(), checked ); +} + +void PrefsDialog::expertTableFilterChanged( const QString & text ) +{ + expert_table->filter( text, expert_table_filter_modified->isChecked() ); +} + void PrefsDialog::advancedTreeFilterChanged( const QString & text ) { advanced_tree->filter( text ); diff --git a/modules/gui/qt/dialogs/preferences/preferences.hpp b/modules/gui/qt/dialogs/preferences/preferences.hpp index a8ca819f2a..cfc6b4266c 100644 --- a/modules/gui/qt/dialogs/preferences/preferences.hpp +++ b/modules/gui/qt/dialogs/preferences/preferences.hpp @@ -26,6 +26,7 @@ #include "widgets/native/qvlcframe.hpp" #include "dialogs/preferences/simple_preferences.hpp" +class ExpertPrefsTable; class PrefsTree; class SPrefsCatList; class SPrefsPanel; @@ -53,9 +54,9 @@ private: QStackedWidget *stack; /* View selection */ - enum { SIMPLE, ADVANCED }; + enum { SIMPLE, ADVANCED, EXPERT }; QGroupBox *types; - QRadioButton *simple, *all; + QRadioButton *simple, *all, *expert; /* Simple view components */ QWidget *simple_split_widget; @@ -72,13 +73,26 @@ private: PrefsTree *advanced_tree; QStackedWidget *advanced_panels_stack; + /* Expert view components */ + QWidget *expert_widget; + QGridLayout *expert_widget_layout; + SearchLineEdit *expert_table_filter; + QCheckBox *expert_table_filter_modified; + ExpertPrefsTable *expert_table; + QLabel *expert_text; + QLabel *expert_longtext; + private slots: + void setExpert(); void setAdvanced(); void setSimple(); + void changeExpertDesc( const QModelIndex &, const QModelIndex & ); void changeAdvPanel( QTreeWidgetItem * ); void changeSimplePanel( int ); void advancedTreeFilterChanged( const QString & ); + void expertTableFilterChanged( const QString & ); + void expertTableFilterModifiedToggled( bool ); void onlyLoadedToggled(); void save(); diff --git a/modules/gui/qt/dialogs/preferences/preferences_widgets.cpp b/modules/gui/qt/dialogs/preferences/preferences_widgets.cpp index d24be38de8..78e4149d33 100644 --- a/modules/gui/qt/dialogs/preferences/preferences_widgets.cpp +++ b/modules/gui/qt/dialogs/preferences/preferences_widgets.cpp @@ -214,6 +214,13 @@ VStringConfigControl::doApply() config_PutPsz( getName(), qtu( getValue() ) ); } +void VStringConfigControl::storeValue() +{ + /* Note, this modifies our local copy of the item only */ + free( p_item->value.psz ); + p_item->value.psz = strdup( qtu( getValue() ) ); +} + /*********** String **************/ StringConfigControl::StringConfigControl( module_config_t *_p_item, QWidget *_parent ) : @@ -851,6 +858,12 @@ VIntConfigControl::doApply() config_PutInt( getName(), getValue() ); } +void VIntConfigControl::storeValue() +{ + /* Note, this modifies our local copy of the item only */ + p_item->value.i = getValue(); +} + /*********** Integer **************/ IntegerConfigControl::IntegerConfigControl( module_config_t *_p_item, QWidget *p ) : @@ -1179,6 +1192,12 @@ VFloatConfigControl::doApply() config_PutFloat( getName(), getValue() ); } +void VFloatConfigControl::storeValue() +{ + /* Note, this modifies our local copy of the item only */ + p_item->value.f = getValue(); +} + /*********** Float **************/ FloatConfigControl::FloatConfigControl( module_config_t *_p_item, QWidget *p ) : diff --git a/modules/gui/qt/dialogs/preferences/preferences_widgets.hpp b/modules/gui/qt/dialogs/preferences/preferences_widgets.hpp index a2db4823da..22c67404f3 100644 --- a/modules/gui/qt/dialogs/preferences/preferences_widgets.hpp +++ b/modules/gui/qt/dialogs/preferences/preferences_widgets.hpp @@ -95,6 +95,7 @@ public: /** Inserts control into an existing box layout */ virtual void insertInto( QBoxLayout*, int index = 0 ) { Q_UNUSED( index ); } virtual void doApply() = 0; + virtual void storeValue() = 0; protected: ConfigControl( module_config_t *_p_conf ) : p_item( _p_conf ) {} virtual void changeVisibility( bool ) { } @@ -112,6 +113,7 @@ class VIntConfigControl : public ConfigControl public: virtual int getValue() const = 0; virtual void doApply() Q_DECL_OVERRIDE; + virtual void storeValue() Q_DECL_OVERRIDE; protected: VIntConfigControl( module_config_t *i ) : ConfigControl(i) {} }; @@ -222,6 +224,7 @@ class VFloatConfigControl : public ConfigControl public: virtual float getValue() const = 0; void doApply() Q_DECL_OVERRIDE; + void storeValue() Q_DECL_OVERRIDE; protected: VFloatConfigControl( module_config_t *i ) : ConfigControl(i) {} }; @@ -264,6 +267,7 @@ class VStringConfigControl : public ConfigControl public: virtual QString getValue() const = 0; void doApply() Q_DECL_OVERRIDE; + void storeValue() Q_DECL_OVERRIDE; protected: VStringConfigControl( module_config_t *i ) : ConfigControl(i) {} }; @@ -418,6 +422,7 @@ public: KeySelectorControl( QWidget * ); void insertInto( QGridLayout*, int row = 0 ) Q_DECL_OVERRIDE; void doApply() Q_DECL_OVERRIDE; + void storeValue() {}; enum ColumnIndex { ACTION_COL = 0, diff --git a/po/POTFILES.in b/po/POTFILES.in index 4a89e3a7df..a6a9899700 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -681,6 +681,10 @@ modules/gui/qt/dialogs/fingerprint/chromaprint.cpp modules/gui/qt/dialogs/fingerprint/chromaprint.hpp modules/gui/qt/dialogs/preferences/complete_preferences.cpp modules/gui/qt/dialogs/preferences/complete_preferences.hpp +modules/gui/qt/dialogs/preferences/expert_model.cpp +modules/gui/qt/dialogs/preferences/expert_model.hpp +modules/gui/qt/dialogs/preferences/expert_view.cpp +modules/gui/qt/dialogs/preferences/expert_view.hpp modules/gui/qt/menus/custom_menus.cpp modules/gui/qt/menus/custom_menus.hpp modules/gui/qt/menus/qml_menu_wrapper.cpp