From b2d844b22aa7f37d2592ee6e98842c2264deb98f Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Mon, 25 Jun 2018 15:39:22 +0200 Subject: [PATCH] Allow changing the default editor for mime types Double-clicking or clicking on the selected item shows a combo box with all editor types that can handle that mime type. Modified handlers are shown in italic, and a new button resets all handlers to the default. Change-Id: I4083c31e3867eb2a2a47adc85e4bd20f3d57be9a Reviewed-by: Leena Miettinen Reviewed-by: David Schulz --- src/plugins/coreplugin/coreplugin.pro | 3 +- src/plugins/coreplugin/coreplugin.qbs | 2 +- .../dialogs/filepropertiesdialog.cpp | 2 +- .../editormanager/editormanager.cpp | 47 +++++- .../editormanager/ieditorfactory.cpp | 46 +++++- .../coreplugin/editormanager/ieditorfactory.h | 4 +- .../editormanager/ieditorfactory_p.h | 42 ++++++ src/plugins/coreplugin/mimetypesettings.cpp | 139 ++++++++++++++++-- .../coreplugin/mimetypesettingspage.ui | 48 +++--- 9 files changed, 287 insertions(+), 46 deletions(-) create mode 100644 src/plugins/coreplugin/editormanager/ieditorfactory_p.h diff --git a/src/plugins/coreplugin/coreplugin.pro b/src/plugins/coreplugin/coreplugin.pro index 93c52d3ddac..53b3943476e 100644 --- a/src/plugins/coreplugin/coreplugin.pro +++ b/src/plugins/coreplugin/coreplugin.pro @@ -224,7 +224,8 @@ HEADERS += corejsextensions.h \ coreicons.h \ editormanager/documentmodel_p.h \ diffservice.h \ - menubarfilter.h + menubarfilter.h \ + editormanager/ieditorfactory_p.h FORMS += dialogs/newdialog.ui \ dialogs/saveitemsdialog.ui \ diff --git a/src/plugins/coreplugin/coreplugin.qbs b/src/plugins/coreplugin/coreplugin.qbs index bab2b062593..c19b5d68458 100644 --- a/src/plugins/coreplugin/coreplugin.qbs +++ b/src/plugins/coreplugin/coreplugin.qbs @@ -229,7 +229,7 @@ Project { "editorview.cpp", "editorview.h", "editorwindow.cpp", "editorwindow.h", "ieditor.cpp", "ieditor.h", - "ieditorfactory.cpp", "ieditorfactory.h", + "ieditorfactory.cpp", "ieditorfactory.h", "ieditorfactory_p.h", "iexternaleditor.cpp", "iexternaleditor.h", "openeditorsview.cpp", "openeditorsview.h", "openeditorswindow.cpp", "openeditorswindow.h", diff --git a/src/plugins/coreplugin/dialogs/filepropertiesdialog.cpp b/src/plugins/coreplugin/dialogs/filepropertiesdialog.cpp index 73ef3ef1853..69f40ab06b6 100644 --- a/src/plugins/coreplugin/dialogs/filepropertiesdialog.cpp +++ b/src/plugins/coreplugin/dialogs/filepropertiesdialog.cpp @@ -72,7 +72,7 @@ void FilePropertiesDialog::refresh() m_ui->mimeType->setText(Utils::mimeTypeForFile(fileInfo).name()); - const Core::EditorFactoryList factories = Core::IEditorFactory::editorFactories(m_fileName); + const Core::EditorFactoryList factories = Core::IEditorFactory::preferredEditorFactories(m_fileName); m_ui->defaultEditor->setText(!factories.isEmpty() ? factories.at(0)->displayName() : tr("Undefined")); m_ui->owner->setText(fileInfo.owner()); diff --git a/src/plugins/coreplugin/editormanager/editormanager.cpp b/src/plugins/coreplugin/editormanager/editormanager.cpp index ae70e6a7be2..2abb16ccd48 100644 --- a/src/plugins/coreplugin/editormanager/editormanager.cpp +++ b/src/plugins/coreplugin/editormanager/editormanager.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -121,6 +122,7 @@ static const char autoSuspendMinDocumentCountKey[] = "EditorManager/AutoSuspendM static const char warnBeforeOpeningBigTextFilesKey[] = "EditorManager/WarnBeforeOpeningBigTextFiles"; static const char bigTextFileSizeLimitKey[] = "EditorManager/BigTextFileSizeLimitInMB"; static const char fileSystemCaseSensitivityKey[] = "Core/FileSystemCaseSensitivity"; +static const char preferredEditorFactoriesKey[] = "EditorManager/PreferredEditorFactories"; static const char scratchBufferKey[] = "_q_emScratchBuffer"; @@ -936,12 +938,11 @@ void EditorManagerPrivate::showPopupOrSelectDocument() Id EditorManagerPrivate::getOpenWithEditorId(const QString &fileName, bool *isExternalEditor) { // Collect editors that can open the file - const Utils::MimeType mt = Utils::mimeTypeForFile(fileName); QList allEditorIds; QStringList allEditorDisplayNames; QList externalEditorIds; // Built-in - const EditorFactoryList editors = IEditorFactory::editorFactories(mt); + const EditorFactoryList editors = IEditorFactory::preferredEditorFactories(fileName); const int size = editors.size(); allEditorDisplayNames.reserve(size); for (int i = 0; i < size; i++) { @@ -949,6 +950,7 @@ Id EditorManagerPrivate::getOpenWithEditorId(const QString &fileName, bool *isEx allEditorDisplayNames.push_back(editors.at(i)->displayName()); } // External editors + const Utils::MimeType mt = Utils::mimeTypeForFile(fileName); const ExternalEditorList exEditors = IExternalEditor::externalEditors(mt); const int esize = exEditors.size(); for (int i = 0; i < esize; i++) { @@ -971,6 +973,39 @@ Id EditorManagerPrivate::getOpenWithEditorId(const QString &fileName, bool *isEx return selectedId; } +static QMap toMap(const QHash &hash) +{ + QMap map; + auto it = hash.begin(); + const auto end = hash.end(); + while (it != end) { + map.insert(it.key().name(), it.value()->id().toSetting()); + ++it; + } + return map; +} + +static QHash fromMap(const QMap &map) +{ + const EditorFactoryList factories = IEditorFactory::allEditorFactories(); + QHash hash; + auto it = map.begin(); + const auto end = map.end(); + while (it != end) { + const Utils::MimeType mimeType = Utils::mimeTypeForName(it.key()); + if (mimeType.isValid()) { + const Id factoryId = Id::fromSetting(it.value()); + IEditorFactory *factory = Utils::findOrDefault(factories, + Utils::equal(&IEditorFactory::id, + factoryId)); + if (factory) + hash.insert(mimeType, factory); + } + ++it; + } + return hash; +} + void EditorManagerPrivate::saveSettings() { ICore::settingsDatabase()->setValue(documentStatesKey, d->m_editorStates); @@ -992,6 +1027,7 @@ void EditorManagerPrivate::saveSettings() qsettings->remove(fileSystemCaseSensitivityKey); else qsettings->setValue(fileSystemCaseSensitivityKey, sensitivity); + qsettings->setValue(preferredEditorFactoriesKey, toMap(userPreferredEditorFactories())); } void EditorManagerPrivate::readSettings() @@ -1023,6 +1059,9 @@ void EditorManagerPrivate::readSettings() else HostOsInfo::setOverrideFileNameCaseSensitivity(sensitivity); } + const QHash preferredEditorFactories = fromMap( + qs->value(preferredEditorFactoriesKey).toMap()); + setUserPreferredEditorFactories(preferredEditorFactories); SettingsDatabase *settings = ICore::settingsDatabase(); if (settings->contains(documentStatesKey)) { @@ -1127,7 +1166,7 @@ EditorFactoryList EditorManagerPrivate::findFactories(Id editorId, const QString EditorFactoryList factories; if (!editorId.isValid()) { - factories = IEditorFactory::editorFactories(fileName); + factories = IEditorFactory::preferredEditorFactories(fileName); } else { // Find by editor id IEditorFactory *factory = Utils::findOrDefault(IEditorFactory::allEditorFactories(), @@ -2467,8 +2506,8 @@ void EditorManager::populateOpenWithMenu(QMenu *menu, const QString &fileName) menu->clear(); + const EditorFactoryList factories = IEditorFactory::preferredEditorFactories(fileName); const Utils::MimeType mt = Utils::mimeTypeForFile(fileName); - const EditorFactoryList factories = IEditorFactory::editorFactories(mt); const ExternalEditorList extEditors = IExternalEditor::externalEditors(mt); const bool anyMatches = !factories.empty() || !extEditors.empty(); if (anyMatches) { diff --git a/src/plugins/coreplugin/editormanager/ieditorfactory.cpp b/src/plugins/coreplugin/editormanager/ieditorfactory.cpp index 7ffbfe4a9bc..fe21fb7a108 100644 --- a/src/plugins/coreplugin/editormanager/ieditorfactory.cpp +++ b/src/plugins/coreplugin/editormanager/ieditorfactory.cpp @@ -24,6 +24,7 @@ ****************************************************************************/ #include "ieditorfactory.h" +#include "ieditorfactory_p.h" #include "editormanager.h" #include "editormanager_p.h" @@ -35,6 +36,7 @@ namespace Core { static QList g_editorFactories; +static QHash g_userPreferredEditorFactories; IEditorFactory::IEditorFactory(QObject *parent) : QObject(parent) @@ -52,7 +54,11 @@ const EditorFactoryList IEditorFactory::allEditorFactories() return g_editorFactories; } -const EditorFactoryList IEditorFactory::editorFactories(const Utils::MimeType &mimeType) +/*! + Returns all available editors for this \a mimeType in the default order + (editors ordered by mime type hierarchy). +*/ +const EditorFactoryList IEditorFactory::defaultEditorFactories(const Utils::MimeType &mimeType) { EditorFactoryList rc; const EditorFactoryList allFactories = IEditorFactory::allEditorFactories(); @@ -60,19 +66,45 @@ const EditorFactoryList IEditorFactory::editorFactories(const Utils::MimeType &m return rc; } -const EditorFactoryList IEditorFactory::editorFactories(const QString &fileName) +/*! + Returns the available editors for \a fileName in order of preference. + That is the default order for the file's MIME type but with a user overridden default + editor first, and if the file is a too large text file, with the binary editor as the + very first. +*/ +const EditorFactoryList IEditorFactory::preferredEditorFactories(const QString &fileName) { const QFileInfo fileInfo(fileName); - // Find by mime type - Utils::MimeType mimeType = Utils::mimeTypeForFile(fileInfo); + // default factories by mime type + const Utils::MimeType mimeType = Utils::mimeTypeForFile(fileInfo); + EditorFactoryList factories = defaultEditorFactories(mimeType); + const auto factories_moveToFront = [&factories](IEditorFactory *f) { + factories.removeAll(f); + factories.prepend(f); + }; + // user preferred factory to front + IEditorFactory *userPreferred = Internal::userPreferredEditorFactories().value(mimeType); + if (userPreferred) + factories_moveToFront(userPreferred); // open text files > 48 MB in binary editor if (fileInfo.size() > EditorManager::maxTextFileSize() && mimeType.inherits("text/plain")) { - mimeType = Utils::mimeTypeForName("application/octet-stream"); + const Utils::MimeType binary = Utils::mimeTypeForName("application/octet-stream"); + const EditorFactoryList binaryEditors = defaultEditorFactories(binary); + if (!binaryEditors.isEmpty()) + factories_moveToFront(binaryEditors.first()); } - - return IEditorFactory::editorFactories(mimeType); + return factories; } +QHash Core::Internal::userPreferredEditorFactories() +{ + return g_userPreferredEditorFactories; +} + +void Internal::setUserPreferredEditorFactories(const QHash &factories) +{ + g_userPreferredEditorFactories = factories; +} } // Core diff --git a/src/plugins/coreplugin/editormanager/ieditorfactory.h b/src/plugins/coreplugin/editormanager/ieditorfactory.h index 779d032873f..a759854e720 100644 --- a/src/plugins/coreplugin/editormanager/ieditorfactory.h +++ b/src/plugins/coreplugin/editormanager/ieditorfactory.h @@ -49,8 +49,8 @@ public: ~IEditorFactory() override; static const EditorFactoryList allEditorFactories(); - static const EditorFactoryList editorFactories(const Utils::MimeType &mimeType); - static const EditorFactoryList editorFactories(const QString &fileName); + static const EditorFactoryList defaultEditorFactories(const Utils::MimeType &mimeType); + static const EditorFactoryList preferredEditorFactories(const QString &fileName); QString displayName() const { return m_displayName; } void setDisplayName(const QString &displayName) { m_displayName = displayName; } diff --git a/src/plugins/coreplugin/editormanager/ieditorfactory_p.h b/src/plugins/coreplugin/editormanager/ieditorfactory_p.h new file mode 100644 index 00000000000..74cd0ca029e --- /dev/null +++ b/src/plugins/coreplugin/editormanager/ieditorfactory_p.h @@ -0,0 +1,42 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include + +#include + +namespace Core { + +class IEditorFactory; + +namespace Internal { + +QHash userPreferredEditorFactories(); +void setUserPreferredEditorFactories(const QHash &factories); + +} // Internal +} // Core diff --git a/src/plugins/coreplugin/mimetypesettings.cpp b/src/plugins/coreplugin/mimetypesettings.cpp index 6e770330f62..eb5c6f695f5 100644 --- a/src/plugins/coreplugin/mimetypesettings.cpp +++ b/src/plugins/coreplugin/mimetypesettings.cpp @@ -30,6 +30,7 @@ #include "ui_mimetypesettingspage.h" #include #include +#include #include #include @@ -48,6 +49,7 @@ #include #include #include +#include #include static const char kModifiedMimeTypesFile[] = "/mimetypes/modifiedmimetypes.xml"; @@ -66,6 +68,18 @@ static const char matchMaskAttributeC[] = "mask"; namespace Core { namespace Internal { +class MimeEditorDelegate : public QStyledItemDelegate +{ +public: + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const final; + void setEditorData(QWidget *editor, const QModelIndex &index) const final; + void setModelData(QWidget *editor, + QAbstractItemModel *model, + const QModelIndex &index) const final; +}; + class UserMimeType { public: @@ -81,6 +95,10 @@ class MimeTypeSettingsModel : public QAbstractTableModel Q_OBJECT public: + enum class Role { + DefaultHandler = Qt::UserRole + }; + MimeTypeSettingsModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {} @@ -89,13 +107,18 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &modelIndex, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) final; + Qt::ItemFlags flags(const QModelIndex &index) const final; void load(); - QString handlerForMimeType(const Utils::MimeType &mimeType) const; + QList handlersForMimeType(const Utils::MimeType &mimeType) const; + IEditorFactory *defaultHandlerForMimeType(const Utils::MimeType &mimeType) const; + void resetUserDefaults(); QList m_mimeTypes; - mutable QHash m_handlersByMimeType; + mutable QHash> m_handlersByMimeType; + QHash m_userDefault; }; int MimeTypeSettingsModel::rowCount(const QModelIndex &) const @@ -127,18 +150,61 @@ QVariant MimeTypeSettingsModel::data(const QModelIndex &modelIndex, int role) co const int column = modelIndex.column(); if (role == Qt::DisplayRole) { const Utils::MimeType &type = m_mimeTypes.at(modelIndex.row()); - if (column == 0) + if (column == 0) { return type.name(); - else - return handlerForMimeType(type); + } else { + IEditorFactory *defaultHandler = defaultHandlerForMimeType(type); + return defaultHandler ? defaultHandler->displayName() : QString(); + } + } else if (role == Qt::EditRole) { + return qVariantFromValue(handlersForMimeType(m_mimeTypes.at(modelIndex.row()))); + } else if (role == int(Role::DefaultHandler)) { + return qVariantFromValue(defaultHandlerForMimeType(m_mimeTypes.at(modelIndex.row()))); + } else if (role == Qt::FontRole) { + if (column == 1) { + const Utils::MimeType &type = m_mimeTypes.at(modelIndex.row()); + if (m_userDefault.contains(type)) { + QFont font = QGuiApplication::font(); + font.setItalic(true); + return font; + } + } + return QVariant(); } return QVariant(); } +bool MimeTypeSettingsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role != int(Role::DefaultHandler) || index.column() != 1) + return false; + auto factory = value.value(); + QTC_ASSERT(factory, return false); + const int row = index.row(); + QTC_ASSERT(row >= 0 && row < m_mimeTypes.size(), return false); + const Utils::MimeType mimeType = m_mimeTypes.at(row); + const QList handlers = handlersForMimeType(mimeType); + QTC_ASSERT(handlers.contains(factory), return false); + if (handlers.first() == factory) // selection is the default anyhow + m_userDefault.remove(mimeType); + else + m_userDefault.insert(mimeType, factory); + emit dataChanged(index, index); + return true; +} + +Qt::ItemFlags MimeTypeSettingsModel::flags(const QModelIndex &index) const +{ + if (index.column() == 0 || handlersForMimeType(m_mimeTypes.at(index.row())).size() < 2) + return QAbstractTableModel::flags(index); + return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; +} + void MimeTypeSettingsModel::load() { beginResetModel(); m_mimeTypes = Utils::allMimeTypes(); + m_userDefault = Core::Internal::userPreferredEditorFactories(); Utils::sort(m_mimeTypes, [](const Utils::MimeType &a, const Utils::MimeType &b) { return a.name().compare(b.name(), Qt::CaseInsensitive) < 0; }); @@ -146,16 +212,29 @@ void MimeTypeSettingsModel::load() endResetModel(); } -QString MimeTypeSettingsModel::handlerForMimeType(const Utils::MimeType &mimeType) const +QList MimeTypeSettingsModel::handlersForMimeType( + const Utils::MimeType &mimeType) const { - if (!m_handlersByMimeType.contains(mimeType)) { - const QList factories = IEditorFactory::editorFactories(mimeType); - const QString value = factories.isEmpty() ? "" : factories.front()->displayName(); - m_handlersByMimeType.insert(mimeType, value); - } + if (!m_handlersByMimeType.contains(mimeType)) + m_handlersByMimeType.insert(mimeType, IEditorFactory::defaultEditorFactories(mimeType)); return m_handlersByMimeType.value(mimeType); } +IEditorFactory *MimeTypeSettingsModel::defaultHandlerForMimeType(const Utils::MimeType &mimeType) const +{ + if (m_userDefault.contains(mimeType)) + return m_userDefault.value(mimeType); + const QList handlers = handlersForMimeType(mimeType); + return handlers.isEmpty() ? nullptr : handlers.first(); +} + +void MimeTypeSettingsModel::resetUserDefaults() +{ + beginResetModel(); + m_userDefault.clear(); + endResetModel(); +} + // MimeTypeSettingsPrivate class MimeTypeSettingsPrivate : public QObject { @@ -197,6 +276,7 @@ public: QString m_filterPattern; Ui::MimeTypeSettingsPage m_ui; QPointer m_widget; + MimeEditorDelegate m_delegate; }; const QChar MimeTypeSettingsPrivate::kSemiColon(QLatin1Char(';')); @@ -225,6 +305,7 @@ void MimeTypeSettingsPrivate::configureUi(QWidget *w) connect(m_ui.filterLineEdit, &QLineEdit::textChanged, this, &MimeTypeSettingsPrivate::setFilterPattern); m_ui.mimeTypesTreeView->setModel(m_filterModel); + m_ui.mimeTypesTreeView->setItemDelegate(&m_delegate); new Utils::HeaderViewStretcher(m_ui.mimeTypesTreeView->header(), 1); @@ -243,6 +324,8 @@ void MimeTypeSettingsPrivate::configureUi(QWidget *w) this, &MimeTypeSettingsPrivate::editMagicHeader); connect(m_ui.resetButton, &QPushButton::clicked, this, &MimeTypeSettingsPrivate::resetMimeTypes); + connect(m_ui.resetHandlersButton, &QPushButton::clicked, + m_model, &MimeTypeSettingsModel::resetUserDefaults); connect(m_ui.magicHeadersTreeWidget, &QTreeWidget::itemSelectionChanged, this, &MimeTypeSettingsPrivate::updatePatternEditAndMagicButtons); @@ -576,6 +659,7 @@ QWidget *MimeTypeSettings::widget() void MimeTypeSettings::apply() { MimeTypeSettingsPrivate::applyUserModifiedMimeTypes(d->m_pendingModifiedMimeTypes); + Core::Internal::setUserPreferredEditorFactories(d->m_model->m_userDefault); d->m_pendingModifiedMimeTypes.clear(); d->m_model->load(); } @@ -593,6 +677,39 @@ void MimeTypeSettings::restoreSettings() MimeTypeSettingsPrivate::applyUserModifiedMimeTypes(mimetypes); } +QWidget *MimeEditorDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + return new QComboBox(parent); +} + +void MimeEditorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + auto box = static_cast(editor); + const auto factories = index.model()->data(index, Qt::EditRole).value>(); + for (IEditorFactory *factory : factories) + box->addItem(factory->displayName(), qVariantFromValue(factory)); + int currentIndex = factories.indexOf( + index.model() + ->data(index, int(MimeTypeSettingsModel::Role::DefaultHandler)) + .value()); + if (QTC_GUARD(currentIndex != -1)) + box->setCurrentIndex(currentIndex); +} + +void MimeEditorDelegate::setModelData(QWidget *editor, + QAbstractItemModel *model, + const QModelIndex &index) const +{ + auto box = static_cast(editor); + model->setData(index, + box->currentData(Qt::UserRole), + int(MimeTypeSettingsModel::Role::DefaultHandler)); +} + } // Internal } // Core diff --git a/src/plugins/coreplugin/mimetypesettingspage.ui b/src/plugins/coreplugin/mimetypesettingspage.ui index 8d1afd9bbe6..e3d5731b2f4 100644 --- a/src/plugins/coreplugin/mimetypesettingspage.ui +++ b/src/plugins/coreplugin/mimetypesettingspage.ui @@ -27,24 +27,7 @@ Registered MIME Types - - - - Filter - - - - - - - Reset all to default. - - - Reset All - - - - + Qt::Horizontal @@ -57,8 +40,18 @@ - + + + + Filter + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + false @@ -67,6 +60,23 @@ + + + + Reset all to default. + + + Reset MIME Types + + + + + + + Reset Handlers + + +