File indexing completed on 2024-04-28 04:37:26

0001 /*
0002     SPDX-FileCopyrightText: 2008 Cédric Pasteur <cedric.pasteur@free.fr>
0003     SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
0004     SPDX-FileCopyrightText: 2021 Igor Kushnir <igorkuo@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "sourceformatterselectionedit.h"
0010 #include "ui_sourceformatterselectionedit.h"
0011 
0012 #include "sourceformatterconfig.h"
0013 #include "sourceformattercontroller.h"
0014 #include "settings/editstyledialog.h"
0015 #include "debug.h"
0016 #include "core.h"
0017 
0018 #include <util/scopeddialog.h>
0019 
0020 #include <KMessageBox>
0021 #include <KTextEditor/Editor>
0022 #include <KTextEditor/ConfigInterface>
0023 #include <KTextEditor/View>
0024 #include <KTextEditor/Document>
0025 #include <KLocalizedString>
0026 #include <KConfig>
0027 
0028 #include <QMetaType>
0029 #include <QMimeDatabase>
0030 #include <QMimeType>
0031 #include <QSignalBlocker>
0032 #include <QString>
0033 #include <QStringView>
0034 #include <QVariant>
0035 #include <QWhatsThis>
0036 
0037 #include <algorithm>
0038 #include <array>
0039 #include <iterator>
0040 #include <memory>
0041 #include <utility>
0042 #include <vector>
0043 
0044 using namespace KDevelop;
0045 
0046 namespace {
0047 constexpr int styleItemDataRole = Qt::UserRole + 1;
0048 constexpr QLatin1String userStyleNamePrefix("User", 4);
0049 
0050 void updateLabel(QLabel& label, const QString& text)
0051 {
0052     if (text.isEmpty()) {
0053         label.hide(); // save UI space
0054     } else {
0055         label.setText(text);
0056         label.show();
0057     }
0058 }
0059 
0060 // std::map is chosen for iterator and reference stability relied upon by code in this file.
0061 // Besides, std::map's interface is convenient for searching by name and iterating in order.
0062 // std::set would be more convenient but cannot be used here, because its elements are
0063 // immutable, which prevents style modifications, even if they don't affect the
0064 // comparison, i.e. don't modify the style's name.
0065 using StyleMap = SourceFormatterController::StyleMap;
0066 
0067 enum class StyleCategory { UserDefined, Predefined };
0068 
0069 /**
0070  * This class encapsulates an ISourceFormatter and its styles.
0071  *
0072  * The class is non-copyable and non-movable to ensure that references to it are never invalidated.
0073  */
0074 class FormatterData
0075 {
0076     Q_DISABLE_COPY_MOVE(FormatterData)
0077 public:
0078     explicit FormatterData(const ISourceFormatter& formatter, StyleMap&& styles)
0079         : m_formatter{formatter}
0080         , m_name{m_formatter.name()}
0081         , m_styles{std::move(styles)}
0082     {
0083     }
0084 
0085     const QString& name() const
0086     {
0087         return m_name;
0088     }
0089     const ISourceFormatter& formatter() const
0090     {
0091         return m_formatter;
0092     }
0093 
0094     SourceFormatterStyle* findStyle(QStringView styleName)
0095     {
0096         const auto it = m_styles.find(styleName);
0097         return it == m_styles.end() ? nullptr : &it->second;
0098     }
0099 
0100     template<typename StyleUser>
0101     void forEachStyle(StyleUser callback)
0102     {
0103         for (auto it = m_styles.begin(), end = m_styles.end(); it != end; ++it) {
0104             callback(it->second);
0105         }
0106     }
0107 
0108     template<typename ConstStyleUser>
0109     void forEachUserDefinedStyle(ConstStyleUser callback) const
0110     {
0111         for (auto it = m_userStyleRange.first(m_styles); it != m_userStyleRange.last(); ++it) {
0112             Q_ASSERT(it->first.startsWith(userStyleNamePrefix));
0113             callback(it->second);
0114         }
0115     }
0116 
0117     template<typename StyleAndCategoryUser>
0118     void forEachSupportingStyleInUiOrder(const QString& supportedLanguageName, StyleAndCategoryUser callback);
0119 
0120     void assertExistingStyle(const SourceFormatterStyle& style)
0121     {
0122         Q_ASSERT(findStyle(style.name()) == &style);
0123     }
0124 
0125     void assertNullOrExistingStyle(const SourceFormatterStyle* style)
0126     {
0127         Q_ASSERT(!style || findStyle(style->name()) == style);
0128     }
0129 
0130     void removeStyle(const SourceFormatterStyle& style)
0131     {
0132         assertExistingStyle(style);
0133         Q_ASSERT_X(style.name().startsWith(userStyleNamePrefix), Q_FUNC_INFO, "Cannot remove a predefined style.");
0134         const auto removedCount = m_styles.erase(style.name());
0135         Q_ASSERT(removedCount == 1);
0136     }
0137 
0138     SourceFormatterStyle& addNewStyle()
0139     {
0140         int maxUserStyleIndex = 0;
0141         forEachUserDefinedStyle([&maxUserStyleIndex](const SourceFormatterStyle& userStyle) {
0142             const int index = QStringView{userStyle.name()}.mid(userStyleNamePrefix.size()).toInt();
0143             // index == 0 if conversion to int fails. Ignore such invalid user-defined style names.
0144             maxUserStyleIndex = std::max(maxUserStyleIndex, index);
0145         });
0146 
0147         // Use the next available user-defined style index in the new style's name.
0148         const QString newStyleName = userStyleNamePrefix + QString::number(maxUserStyleIndex + 1);
0149 
0150         const auto oldStyleCount = m_styles.size();
0151         const auto newStyleIt =
0152             m_styles.try_emplace(std::as_const(m_userStyleRange).last(), newStyleName, newStyleName);
0153         Q_ASSERT(newStyleIt->second.name() == newStyleIt->first);
0154         Q_ASSERT(m_styles.size() == oldStyleCount + 1);
0155 
0156         return newStyleIt->second;
0157     }
0158 
0159 private:
0160     class UserStyleRange
0161     {
0162     public:
0163         using iterator = StyleMap::iterator;
0164         using const_iterator = StyleMap::const_iterator;
0165 
0166         explicit UserStyleRange(StyleMap& styles)
0167         {
0168             const auto firstUserStyle = styles.lower_bound(userStyleNamePrefix);
0169 
0170             // m_lastBeforeUserStyles == styles.end() means that EITHER the first style is user-defined
0171             // OR there are no user-defined styles and the first predefined style name would compare
0172             // greater than any user-defined style name.
0173             m_lastBeforeUserStyles = firstUserStyle == styles.begin() ? styles.end() : std::prev(firstUserStyle);
0174 
0175             m_firstAfterUserStyles = std::find_if_not(firstUserStyle, styles.end(), [](const auto& pair) {
0176                 return pair.first.startsWith(userStyleNamePrefix);
0177             });
0178         }
0179 
0180         iterator first(StyleMap& styles)
0181         {
0182             return m_lastBeforeUserStyles == styles.end() ? styles.begin() : std::next(m_lastBeforeUserStyles);
0183         }
0184         const_iterator first(const StyleMap& styles) const
0185         {
0186             return m_lastBeforeUserStyles == styles.cend() ? styles.cbegin() : std::next(m_lastBeforeUserStyles);
0187         }
0188 
0189         iterator last()
0190         {
0191             return m_firstAfterUserStyles;
0192         }
0193         const_iterator last() const
0194         {
0195             return m_firstAfterUserStyles;
0196         }
0197 
0198     private:
0199         // The stored iterators point to predefined styles or equal styles.end(). They stay valid and
0200         // correct because only user-defined styles are inserted and erased. Furthermore, user-defined
0201         // styles are always positioned between these two iterators OR from the beginning until
0202         // m_firstAfterUserStyles if m_lastBeforeUserStyles == styles.end().
0203         iterator m_lastBeforeUserStyles;
0204         iterator m_firstAfterUserStyles;
0205     };
0206 
0207     const ISourceFormatter& m_formatter;
0208     const QString m_name; ///< cached m_formatter.name()
0209     StyleMap m_styles;
0210     UserStyleRange m_userStyleRange{m_styles};
0211 };
0212 
0213 template<typename StyleAndCategoryUser>
0214 void FormatterData::forEachSupportingStyleInUiOrder(const QString& supportedLanguageName, StyleAndCategoryUser callback)
0215 {
0216     std::vector<SourceFormatterStyle*> filteredStyles;
0217     // Few if any styles are filtered out => reserve the maximum possible size.
0218     filteredStyles.reserve(m_styles.size());
0219 
0220     const auto filterStyles = [&supportedLanguageName, &filteredStyles](StyleMap::iterator first,
0221                                                                         StyleMap::iterator last) {
0222         for (; first != last; ++first) {
0223             auto& style = first->second;
0224             // Filter out styles that do not support the selected language.
0225             if (style.supportsLanguage(supportedLanguageName)) {
0226                 filteredStyles.push_back(&style);
0227             }
0228         }
0229     };
0230 
0231     const auto sortAndUseFilteredStyles = [&callback, &filteredStyles](StyleCategory category) {
0232         const auto compareForUi = [](const SourceFormatterStyle* a, const SourceFormatterStyle* b) {
0233             const auto& left = a->caption();
0234             const auto& right = b->caption();
0235             const int ciResult = QString::compare(left, right, Qt::CaseInsensitive);
0236             if (ciResult != 0) {
0237                 return ciResult < 0;
0238             }
0239             return left < right; // compare case-sensitively as a fallback
0240         };
0241         // Stable sort ensures that styles with equal captions are ordered predictably (by style name).
0242         std::stable_sort(filteredStyles.begin(), filteredStyles.end(), compareForUi);
0243 
0244         for (auto* style : filteredStyles) {
0245             callback(*style, category);
0246         }
0247     };
0248 
0249     const auto firstUserStyle = m_userStyleRange.first(m_styles);
0250 
0251     // User-defined styles are more likely to be selected => show them on top of the list.
0252     filterStyles(firstUserStyle, m_userStyleRange.last());
0253     sortAndUseFilteredStyles(StyleCategory::UserDefined);
0254 
0255     filteredStyles.clear();
0256     filterStyles(m_styles.begin(), firstUserStyle);
0257     filterStyles(m_userStyleRange.last(), m_styles.end());
0258     sortAndUseFilteredStyles(StyleCategory::Predefined);
0259 }
0260 
0261 class LanguageSettings
0262 {
0263 public:
0264     explicit LanguageSettings(const QString& name, FormatterData& supportingFormatter)
0265         : m_name{name}
0266         , m_supportingFormatters{&supportingFormatter}
0267         , m_selectedFormatter{&supportingFormatter}
0268     {
0269     }
0270 
0271     const QString& name() const
0272     {
0273         return m_name;
0274     }
0275     FormatterData& selectedFormatter() const
0276     {
0277         return *m_selectedFormatter;
0278     }
0279     SourceFormatterStyle* selectedStyle() const
0280     {
0281         return m_selectedStyle;
0282     }
0283 
0284     bool isFormatterSupporting(const FormatterData& formatter) const
0285     {
0286         return findSupportingFormatter(formatter) != m_supportingFormatters.cend();
0287     }
0288 
0289     const auto& supportingFormatters() const
0290     {
0291         return m_supportingFormatters;
0292     }
0293 
0294     const QMimeType& defaultMimeType() const
0295     {
0296         Q_ASSERT_X(!m_mimeTypes.empty(), Q_FUNC_INFO,
0297                    "A valid MIME type must be added right after constructing a language. "
0298                    "MIME types are never removed.");
0299         return m_mimeTypes.front();
0300     }
0301 
0302     void setSelectedFormatter(FormatterData& selectedFormatter)
0303     {
0304         Q_ASSERT_X(m_selectedFormatter != &selectedFormatter, Q_FUNC_INFO,
0305                    "Reselecting an already selected formatter is currently not supported. "
0306                    "If this is needed, an early return here would probably be correct.");
0307         Q_ASSERT(isFormatterSupporting(selectedFormatter));
0308         m_selectedFormatter = &selectedFormatter;
0309         m_selectedStyle = nullptr;
0310     }
0311 
0312     void unselectStyle(const SourceFormatterStyle& selectedStyle)
0313     {
0314         Q_ASSERT(m_selectedStyle == &selectedStyle);
0315         m_selectedStyle = nullptr;
0316     }
0317 
0318     void setSelectedStyle(SourceFormatterStyle* style)
0319     {
0320         m_selectedFormatter->assertNullOrExistingStyle(style);
0321         m_selectedStyle = style;
0322     }
0323 
0324     void addMimeType(QMimeType&& mimeType)
0325     {
0326         Q_ASSERT(mimeType.isValid());
0327         if (std::find(m_mimeTypes.cbegin(), m_mimeTypes.cend(), mimeType) == m_mimeTypes.cend()) {
0328             m_mimeTypes.push_back(std::move(mimeType));
0329         }
0330     }
0331 
0332     void addSupportingFormatter(FormatterData& formatter)
0333     {
0334         // A linear search by pointer is much faster than a binary search by name => fast path:
0335         if (isFormatterSupporting(formatter)) {
0336             return; // already supporting => nothing to do
0337         }
0338         const auto insertionPosition = std::lower_bound(m_supportingFormatters.cbegin(), m_supportingFormatters.cend(),
0339                                                         &formatter, [](const FormatterData* a, const FormatterData* b) {
0340                                                             return a->name() < b->name();
0341                                                         });
0342         Q_ASSERT(insertionPosition == m_supportingFormatters.cend() || formatter.name() < (*insertionPosition)->name());
0343         m_supportingFormatters.insert(insertionPosition, &formatter);
0344     }
0345 
0346     /**
0347      * Removes @p formatter from the set of formatters that support this language
0348      * unless it is the single supporting formatter.
0349      *
0350      * @return @c true if @p formatter was not in the set or was removed from the set;
0351      *         @c false if @p formatter was the single supporting formatter and was not removed.
0352      */
0353     bool removeSupportingFormatter(const FormatterData& formatter)
0354     {
0355         const auto it = findSupportingFormatter(formatter);
0356         if (it == m_supportingFormatters.cend()) {
0357             return true; // formatter is not supporting => nothing to do
0358         }
0359         if (m_supportingFormatters.size() == 1) {
0360             return false; // removing the last supporting formatter would break an invariant => fail
0361         }
0362 
0363         m_supportingFormatters.erase(it);
0364         if (m_selectedFormatter == &formatter) {
0365             selectFirstFormatterAndUnselectStyle();
0366         }
0367         return true;
0368     }
0369 
0370     void readSelectedFormatterAndStyle(const KConfigGroup& config)
0371     {
0372         selectFirstFormatterAndUnselectStyle(); // ensure predictable selection in case of error
0373         for (const auto& mimeType : m_mimeTypes) {
0374             SourceFormatter::ConfigForMimeType parser(config, mimeType);
0375             if (parser.isValid()) {
0376                 setSelectedFormatterAndStyle(std::move(parser), mimeType);
0377                 // A valid entry for a MIME type has been processed. We are done here. Keep the first formatter
0378                 // selected and style unselected in case of unknown or unsupporting formatter or style name.
0379                 break;
0380             }
0381         }
0382     }
0383 
0384     void saveSettings(KConfigGroup& config) const
0385     {
0386         for (const auto& mimeType : m_mimeTypes) {
0387             SourceFormatter::ConfigForMimeType::writeEntry(config, mimeType, m_selectedFormatter->name(),
0388                                                            m_selectedStyle);
0389         }
0390     }
0391 
0392 private:
0393     std::vector<FormatterData*>::const_iterator findSupportingFormatter(const FormatterData& formatter) const
0394     {
0395         return std::find(m_supportingFormatters.cbegin(), m_supportingFormatters.cend(), &formatter);
0396     }
0397 
0398     void selectFirstFormatterAndUnselectStyle()
0399     {
0400         m_selectedFormatter = m_supportingFormatters.front();
0401         m_selectedStyle = nullptr;
0402     }
0403 
0404     void setSelectedFormatterAndStyle(const SourceFormatter::ConfigForMimeType& parser, const QMimeType& mimeType)
0405     {
0406         const QStringView formatterName = parser.formatterName();
0407         const auto formatterIt = std::find_if(m_supportingFormatters.cbegin(), m_supportingFormatters.cend(),
0408                                               [formatterName](const FormatterData* f) {
0409                                                   return f->name() == formatterName;
0410                                               });
0411         if (formatterIt == m_supportingFormatters.cend()) {
0412             qCWarning(SHELL) << "Unknown or unsupporting formatter" << formatterName << "is selected for MIME type"
0413                              << mimeType.name();
0414             return;
0415         }
0416         m_selectedFormatter = *formatterIt;
0417 
0418         m_selectedStyle = m_selectedFormatter->findStyle(parser.styleName());
0419         if (!m_selectedStyle) {
0420             qCWarning(SHELL) << "The style" << parser.styleName() << "selected for MIME type" << mimeType.name()
0421                              << "does not belong to the selected formatter" << formatterName;
0422         } else if (!m_selectedStyle->supportsLanguage(m_name)) {
0423             qCWarning(SHELL) << *m_selectedStyle << "selected for MIME type" << mimeType.name()
0424                              << "does not support the language" << m_name;
0425             m_selectedStyle = nullptr;
0426         }
0427     }
0428 
0429     QString m_name; ///< the name of this language, logically const
0430     /// unique MIME types that belong to this language; a sequence container to keep MIME type priority order
0431     std::vector<QMimeType> m_mimeTypes;
0432 
0433     // m_supportingFormatters is a sequence container rather than a map or a set for the following reasons:
0434     // 1) with only two formatter plugins linear search is faster than binary search
0435     //    because QString's equality comparison is faster than ordering comparison;
0436     // 2) map or set m_supportingFormatters is error-prone - makes it easy to accidentally search by name when
0437     //    more precise and efficient search by pointer is intended: m_supportingFormatters.find(formatter)
0438     /**
0439      * Unique formatters that support this language. The pointers are non-owning.
0440      * Ordered by FormatterData::name() to ensure UI item order stability.
0441      * Invariants: 1) the container is never empty; 2) no nullptr values.
0442      */
0443     std::vector<FormatterData*> m_supportingFormatters;
0444     /// invariant: @a m_supportingFormatters contains @a m_selectedFormatter => never nullptr
0445     FormatterData* m_selectedFormatter;
0446     /// pointer to one of @a m_selectedFormatter's styles or nullptr if unselected
0447     SourceFormatterStyle* m_selectedStyle = nullptr;
0448 };
0449 
0450 } // unnamed namespace
0451 
0452 Q_DECLARE_METATYPE(FormatterData*)
0453 
0454 enum class NewItemPosition { Bottom, Top };
0455 
0456 class KDevelop::SourceFormatterSelectionEditPrivate
0457 {
0458     Q_DISABLE_COPY_MOVE(SourceFormatterSelectionEditPrivate)
0459 public:
0460     SourceFormatterSelectionEditPrivate() = default;
0461 
0462     Ui::SourceFormatterSelectionEdit ui;
0463     // formatters is a sequence container for the same reasons as LanguageSettings::m_supportingFormatters
0464     // (see the comment above that data member).
0465     /// All known (added) formatters; unordered; no nullptr values.
0466     std::vector<std::unique_ptr<FormatterData>> formatters;
0467 
0468     // languages is a sequence container rather than a map or a set for the following reasons:
0469     // 1) the number of languages is small (normally less than 10), so the linear complexity of
0470     //    insertion and removal is not a problem;
0471     // 2) iterator and reference stability does not matter, because the current language is reset
0472     //    to the first language after insertion or removal.
0473     // 3) a map is less convenient because its element is a pair;
0474     // 4) std::set cannot be used, because its elements are immutable;
0475     // 5) boost::container::flat_set adds a dependency on boost and doesn't substantially simplify the code.
0476     /**
0477      * Unique programming languages displayed in the UI to support per-language formatting configuration.
0478      * Ordered by LanguageSettings::name() to support UI item order stability.
0479      */
0480     std::vector<LanguageSettings> languages;
0481     LanguageSettings* currentLanguagePtr = nullptr; ///< cached languageSelectedInUi()
0482     KTextEditor::Document* document;
0483     KTextEditor::View* view;
0484 
0485     // Most member functions below have preconditions. They may only be called when the values,
0486     // specified in their documentations, are certain to be the same in the model and in the UI.
0487     // If that is not the case, obtain the needed values in some other way.
0488 
0489     /// @pre model-UI matches: language
0490     LanguageSettings& currentLanguage()
0491     {
0492         Q_ASSERT(currentLanguagePtr);
0493         Q_ASSERT(currentLanguagePtr == &languageSelectedInUi());
0494         return *currentLanguagePtr;
0495     }
0496 
0497     /// @pre model-UI matches: language, formatter
0498     FormatterData& currentFormatter()
0499     {
0500         auto& currentFormatter = currentLanguage().selectedFormatter();
0501         Q_ASSERT(&currentFormatter == &formatterSelectedInUi());
0502         return currentFormatter;
0503     }
0504 
0505     /// @pre model-UI matches: language, formatter, style
0506     /// @pre current style is valid
0507     SourceFormatterStyle& validCurrentStyle()
0508     {
0509         auto* const style = currentStyle();
0510         Q_ASSERT(style);
0511         return *style;
0512     }
0513 
0514     /// @pre model-UI matches: language, formatter, style
0515     void assertValidSelectedStyleItem(const QListWidgetItem* item)
0516     {
0517         Q_ASSERT(item);
0518         Q_ASSERT(&styleFromVariant(item->data(styleItemDataRole)) == &validCurrentStyle());
0519     }
0520 
0521     /// @pre !languages.empty()
0522     LanguageSettings& languageSelectedInUi()
0523     {
0524         Q_ASSERT(!languages.empty());
0525         const auto languageName = ui.cbLanguages->currentText();
0526         Q_ASSERT(!languageName.isEmpty());
0527 
0528         const auto it = languageLowerBound(languageName);
0529         Q_ASSERT(it != languages.end());
0530         Q_ASSERT(it->name() == languageName);
0531         return *it;
0532     }
0533 
0534     /// @pre model-UI matches: language
0535     FormatterData& formatterSelectedInUi()
0536     {
0537         const auto currentData = ui.cbFormatters->currentData();
0538         Q_ASSERT(currentData.canConvert<FormatterData*>());
0539         auto* const formatter = currentData.value<FormatterData*>();
0540         Q_ASSERT(formatter);
0541         Q_ASSERT(currentLanguage().isFormatterSupporting(*formatter));
0542         return *formatter;
0543     }
0544 
0545     /// @pre model-UI matches: language, formatter
0546     SourceFormatterStyle* styleSelectedInUi()
0547     {
0548         const auto selectedIndexes = ui.styleList->selectionModel()->selectedIndexes();
0549         if (selectedIndexes.empty()) {
0550             return nullptr;
0551         }
0552         Q_ASSERT_X(selectedIndexes.size() == 1, Q_FUNC_INFO, "SingleSelection is assumed.");
0553 
0554         auto& style = styleFromVariant(selectedIndexes.constFirst().data(styleItemDataRole));
0555         currentFormatter().assertExistingStyle(style);
0556         return &style;
0557     }
0558 
0559     /// @pre model-UI matches: language, formatter
0560     void updateUiForCurrentFormatter();
0561 
0562     /// @pre languages.empty() OR model-UI matches: language, formatter, style
0563     void updateUiForCurrentStyle()
0564     {
0565         updateStyleButtons();
0566         updatePreview();
0567     }
0568 
0569     /// @pre languages.empty() OR model-UI matches: language, formatter, style
0570     void updateStyleButtons();
0571     /// @pre languages.empty() OR model-UI matches: language, formatter, style
0572     void updatePreview();
0573 
0574     QListWidgetItem& addStyleItem(SourceFormatterStyle& style, StyleCategory category,
0575                                   NewItemPosition position = NewItemPosition::Bottom);
0576 
0577     /**
0578      * Add the names of @a languages to @a ui.cbLanguages.
0579      */
0580     void fillLanguageCombobox();
0581 
0582     void addMimeTypes(const SourceFormatterStyle::MimeList& mimeTypes, FormatterData& formatter);
0583 
0584 private:
0585     static bool isUserDefinedStyle(const SourceFormatterStyle& style)
0586     {
0587         return style.name().startsWith(userStyleNamePrefix);
0588     }
0589 
0590     static SourceFormatterStyle& styleFromVariant(const QVariant& variant)
0591     {
0592         Q_ASSERT(variant.canConvert<SourceFormatterStyle*>());
0593         auto* const style = variant.value<SourceFormatterStyle*>();
0594         Q_ASSERT(style);
0595         return *style;
0596     }
0597 
0598     std::vector<LanguageSettings>::iterator languageLowerBound(QStringView languageName)
0599     {
0600         return std::lower_bound(languages.begin(), languages.end(), languageName,
0601                                 [](const LanguageSettings& lang, QStringView languageName) {
0602                                     return lang.name() < languageName;
0603                                 });
0604     }
0605 
0606     /// @pre model-UI matches: language, formatter, style
0607     SourceFormatterStyle* currentStyle()
0608     {
0609         auto* const style = currentLanguage().selectedStyle();
0610         Q_ASSERT(style == styleSelectedInUi());
0611         return style;
0612     }
0613 
0614     LanguageSettings& addSupportingFormatterToLanguage(const QString& languageName, FormatterData& formatter);
0615 };
0616 
0617 void SourceFormatterSelectionEditPrivate::updateUiForCurrentFormatter()
0618 {
0619     ui.formatterDescriptionButton->setWhatsThis(currentFormatter().formatter().description());
0620     updateLabel(*ui.usageHintLabel, currentFormatter().formatter().usageHint());
0621 
0622     {
0623         const QSignalBlocker blocker(ui.styleList);
0624         ui.styleList->clear();
0625 
0626         currentFormatter().forEachSupportingStyleInUiOrder(currentLanguage().name(),
0627                                                            [this](SourceFormatterStyle& style, StyleCategory category) {
0628                                                                auto& item = addStyleItem(style, category);
0629                                                                if (&style == currentLanguage().selectedStyle()) {
0630                                                                    ui.styleList->setCurrentItem(&item);
0631                                                                }
0632                                                            });
0633     }
0634     Q_ASSERT_X(currentLanguage().selectedStyle() == styleSelectedInUi(), Q_FUNC_INFO,
0635                "The selected style is not among the supporting styles!");
0636 
0637     updateUiForCurrentStyle();
0638 }
0639 
0640 void SourceFormatterSelectionEditPrivate::updateStyleButtons()
0641 {
0642     if (languages.empty() || !currentStyle()) {
0643         ui.btnDelStyle->setEnabled(false);
0644         ui.btnEditStyle->setEnabled(false);
0645         // Forbid creating a new style not based on an existing (selected) style,
0646         // because it would be useless with no MIME types and no way to add them.
0647         ui.btnNewStyle->setEnabled(false);
0648         return;
0649     }
0650 
0651     const bool userDefined = isUserDefinedStyle(validCurrentStyle());
0652     const bool hasEditWidget = currentFormatter().formatter().hasEditStyleWidget();
0653 
0654     ui.btnDelStyle->setEnabled(userDefined);
0655     ui.btnEditStyle->setEnabled(userDefined && hasEditWidget);
0656     ui.btnNewStyle->setEnabled(hasEditWidget);
0657 }
0658 
0659 void SourceFormatterSelectionEditPrivate::updatePreview()
0660 {
0661     if (languages.empty() || !currentStyle()) {
0662         ui.descriptionLabel->hide();
0663         ui.previewArea->hide();
0664         return;
0665     }
0666 
0667     const auto& currentStyle = validCurrentStyle();
0668 
0669     updateLabel(*ui.descriptionLabel, currentStyle.description());
0670 
0671     if (!currentStyle.usePreview()) {
0672         ui.previewArea->hide();
0673         return;
0674     }
0675 
0676     document->setReadWrite(true);
0677 
0678     const auto& mimeType = currentLanguage().defaultMimeType();
0679     document->setHighlightingMode(currentStyle.modeForMimetype(mimeType));
0680 
0681     //NOTE: this is ugly, but otherwise kate might remove tabs again :-/
0682     // see also: https://bugs.kde.org/show_bug.cgi?id=291074
0683     auto* const iface = qobject_cast<KTextEditor::ConfigInterface*>(document);
0684     const QString replaceTabsConfigKey = QStringLiteral("replace-tabs");
0685     QVariant oldReplaceTabsConfigValue;
0686     if (iface) {
0687         oldReplaceTabsConfigValue = iface->configValue(replaceTabsConfigKey);
0688         iface->setConfigValue(replaceTabsConfigKey, false);
0689     }
0690 
0691     const auto& formatter = currentFormatter().formatter();
0692     document->setText(
0693         formatter.formatSourceWithStyle(currentStyle, formatter.previewText(currentStyle, mimeType), QUrl(), mimeType));
0694 
0695     if (iface) {
0696         iface->setConfigValue(replaceTabsConfigKey, oldReplaceTabsConfigValue);
0697     }
0698 
0699     ui.previewArea->show();
0700     view->setCursorPosition(KTextEditor::Cursor(0, 0));
0701 
0702     document->setReadWrite(false);
0703 }
0704 
0705 QListWidgetItem& SourceFormatterSelectionEditPrivate::addStyleItem(SourceFormatterStyle& style, StyleCategory category,
0706                                                                    NewItemPosition position)
0707 {
0708     Q_ASSERT_X((category == StyleCategory::UserDefined) == isUserDefinedStyle(style), Q_FUNC_INFO,
0709                "Wrong style category!");
0710 
0711     auto* const item = new QListWidgetItem(style.caption());
0712     item->setData(styleItemDataRole, QVariant::fromValue(&style));
0713     if (category == StyleCategory::UserDefined) {
0714         item->setFlags(item->flags() | Qt::ItemIsEditable);
0715     }
0716 
0717     switch (position) {
0718     case NewItemPosition::Bottom:
0719         ui.styleList->addItem(item);
0720         break;
0721     case NewItemPosition::Top:
0722         ui.styleList->insertItem(0, item);
0723         break;
0724     }
0725 
0726     return *item;
0727 }
0728 
0729 void SourceFormatterSelectionEditPrivate::fillLanguageCombobox()
0730 {
0731     // Move the languages not supported by KDevelop to the bottom of the combobox.
0732     // Use std::array to avoid extra memory allocations.
0733 
0734     constexpr std::array unsupportedLanguages{
0735         QLatin1String("C#", 2),
0736         QLatin1String("Java", 4),
0737     };
0738     Q_ASSERT(std::is_sorted(unsupportedLanguages.cbegin(), unsupportedLanguages.cend()));
0739     std::array<QString, unsupportedLanguages.size()> skippedLanguages{};
0740 
0741     for (const auto& lang : languages) {
0742         const QString& name = lang.name();
0743         const auto unsupportedIt = std::find(unsupportedLanguages.cbegin(), unsupportedLanguages.cend(), name);
0744         if (unsupportedIt == unsupportedLanguages.cend()) {
0745             ui.cbLanguages->addItem(name);
0746         } else {
0747             skippedLanguages[unsupportedIt - unsupportedLanguages.cbegin()] = name;
0748         }
0749     }
0750 
0751     for (const auto& name : skippedLanguages) {
0752         if (!name.isEmpty()) {
0753             ui.cbLanguages->addItem(name);
0754         }
0755     }
0756 }
0757 
0758 void SourceFormatterSelectionEditPrivate::addMimeTypes(const SourceFormatterStyle::MimeList& mimeTypes,
0759                                                        FormatterData& formatter)
0760 {
0761     for (const auto& item : mimeTypes) {
0762         QMimeType mime = QMimeDatabase().mimeTypeForName(item.mimeType);
0763         if (!mime.isValid()) {
0764             qCWarning(SHELL) << "formatter plugin" << formatter.name() << "supports unknown MIME type entry"
0765                              << item.mimeType;
0766             continue;
0767         }
0768         auto& lang = addSupportingFormatterToLanguage(item.highlightMode, formatter);
0769         lang.addMimeType(std::move(mime));
0770     }
0771 }
0772 
0773 LanguageSettings& SourceFormatterSelectionEditPrivate::addSupportingFormatterToLanguage(const QString& languageName,
0774                                                                                         FormatterData& formatter)
0775 {
0776     Q_ASSERT_X(!languageName.isEmpty(), Q_FUNC_INFO,
0777                "Empty language name should not be displayed in the UI and should be skipped earlier.");
0778     const auto it = languageLowerBound(languageName);
0779     if (it == languages.end() || it->name() != languageName) {
0780         return *languages.emplace(it, languageName, formatter);
0781     }
0782     it->addSupportingFormatter(formatter);
0783     return *it;
0784 }
0785 
0786 SourceFormatterSelectionEdit::SourceFormatterSelectionEdit(QWidget* parent)
0787     : QWidget(parent)
0788     , d_ptr(new SourceFormatterSelectionEditPrivate)
0789 {
0790     Q_D(SourceFormatterSelectionEdit);
0791 
0792     d->ui.setupUi(this);
0793     // Aligning to the left prevents the widgets on the left side from moving right/left whenever
0794     // style description and preview on the right side become hidden/shown (when a different style
0795     // is selected or the current style is unselected).
0796     d->ui.mainLayout->setAlignment(Qt::AlignLeft);
0797 
0798     connect(d->ui.cbLanguages, QOverload<int>::of(&KComboBox::currentIndexChanged),
0799             this, &SourceFormatterSelectionEdit::selectLanguage);
0800     connect(d->ui.cbFormatters, QOverload<int>::of(&KComboBox::currentIndexChanged),
0801             this, &SourceFormatterSelectionEdit::selectFormatter);
0802     connect(d->ui.styleList, &QListWidget::itemSelectionChanged, this,
0803             &SourceFormatterSelectionEdit::styleSelectionChanged);
0804     connect(d->ui.btnDelStyle, &QPushButton::clicked, this, &SourceFormatterSelectionEdit::deleteStyle);
0805     connect(d->ui.btnNewStyle, &QPushButton::clicked, this, &SourceFormatterSelectionEdit::newStyle);
0806     connect(d->ui.btnEditStyle, &QPushButton::clicked, this, &SourceFormatterSelectionEdit::editStyle);
0807     connect(d->ui.styleList, &QListWidget::itemChanged, this, &SourceFormatterSelectionEdit::styleNameChanged);
0808 
0809     const auto showWhatsThisOnClick = [](QAbstractButton* button) {
0810         connect(button, &QAbstractButton::clicked, button, [button] {
0811             QWhatsThis::showText(button->mapToGlobal(QPoint{0, 0}), button->whatsThis(), button);
0812         });
0813     };
0814     showWhatsThisOnClick(d->ui.usageHelpButton);
0815     showWhatsThisOnClick(d->ui.formatterDescriptionButton);
0816 
0817     d->document = KTextEditor::Editor::instance()->createDocument(this);
0818     d->document->setReadWrite(false);
0819 
0820     d->view = d->document->createView(d->ui.textEditor);
0821     d->view->setStatusBarEnabled(false);
0822 
0823     auto *layout2 = new QVBoxLayout(d->ui.textEditor);
0824     layout2->setContentsMargins(0, 0, 0, 0);
0825     layout2->addWidget(d->view);
0826     d->ui.textEditor->setLayout(layout2);
0827     d->view->show();
0828 
0829     KTextEditor::ConfigInterface *iface =
0830     qobject_cast<KTextEditor::ConfigInterface*>(d->view);
0831     if (iface) {
0832         iface->setConfigValue(QStringLiteral("dynamic-word-wrap"), false);
0833         iface->setConfigValue(QStringLiteral("icon-bar"), false);
0834         iface->setConfigValue(QStringLiteral("scrollbar-minimap"), false);
0835     }
0836 
0837     SourceFormatterController* controller = Core::self()->sourceFormatterControllerInternal();
0838     connect(controller, &SourceFormatterController::formatterLoaded,
0839             this, &SourceFormatterSelectionEdit::addSourceFormatter);
0840     connect(controller, &SourceFormatterController::formatterUnloading,
0841             this, &SourceFormatterSelectionEdit::removeSourceFormatter);
0842     const auto& formatters = controller->formatters();
0843     for (auto* formatter : formatters) {
0844         addSourceFormatterNoUi(formatter); // loadSettings() calls resetUi() once later
0845     }
0846 }
0847 
0848 SourceFormatterSelectionEdit::~SourceFormatterSelectionEdit() = default;
0849 
0850 void SourceFormatterSelectionEdit::addSourceFormatterNoUi(ISourceFormatter* ifmt)
0851 {
0852     Q_D(SourceFormatterSelectionEdit);
0853 
0854     const QString formatterName = ifmt->name();
0855     qCDebug(SHELL) << "Adding source formatter:" << formatterName;
0856 
0857     if (std::any_of(d->formatters.cbegin(), d->formatters.cend(), [&formatterName](const auto& f) {
0858             return formatterName == f->name();
0859         })) {
0860         qCWarning(SHELL) << "formatter plugin" << formatterName
0861                          << "loading which was already seen before by SourceFormatterSelectionEdit";
0862         return;
0863     }
0864 
0865     d->formatters.push_back(std::make_unique<FormatterData>(
0866         *ifmt, Core::self()->sourceFormatterControllerInternal()->stylesForFormatter(*ifmt)));
0867     auto& formatter = *d->formatters.back();
0868 
0869     // The loop below can invalidate currentLanguagePtr; resetUi() selects the first language anyway.
0870     d->currentLanguagePtr = nullptr;
0871 
0872     // Built-in styles share the same MIME list object. User-defined styles usually have MIME lists equal to the
0873     // shared built-in list. addedMimeLists allows to quickly skip duplicate lists as an optimization.
0874     // Note that a single addedMimeLists object cannot be shared by consecutive calls to this function, because
0875     // formatter is different in each call, and formatter is added to language settings in the loop below.
0876     std::vector<SourceFormatterStyle::MimeList> addedMimeLists;
0877     formatter.forEachStyle([d, &formatter, &addedMimeLists](const SourceFormatterStyle& style) {
0878         auto mimeTypes = style.mimeTypes();
0879         if (std::find(addedMimeLists.cbegin(), addedMimeLists.cend(), mimeTypes) != addedMimeLists.cend()) {
0880             return; // this is a duplicate list
0881         }
0882         addedMimeLists.push_back(std::move(mimeTypes));
0883         d->addMimeTypes(addedMimeLists.back(), formatter);
0884     });
0885 }
0886 
0887 void SourceFormatterSelectionEdit::addSourceFormatter(ISourceFormatter* ifmt)
0888 {
0889     addSourceFormatterNoUi(ifmt);
0890     resetUi();
0891 }
0892 
0893 void SourceFormatterSelectionEdit::removeSourceFormatter(ISourceFormatter* ifmt)
0894 {
0895     Q_D(SourceFormatterSelectionEdit);
0896 
0897     qCDebug(SHELL) << "Removing source formatter:" << ifmt->name();
0898 
0899     const auto formatterIt = std::find_if(d->formatters.cbegin(), d->formatters.cend(), [ifmt](const auto& f) {
0900         return ifmt == &f->formatter();
0901     });
0902     if (formatterIt == d->formatters.cend()) {
0903         qCWarning(SHELL) << "formatter plugin" << ifmt->name() << "unloading which was not seen before by SourceFormatterSelectionEdit";
0904         return;
0905     }
0906 
0907     // The loop below can invalidate currentLanguagePtr; resetUi() selects the first language anyway.
0908     d->currentLanguagePtr = nullptr;
0909 
0910     for (auto languageIt = d->languages.begin(); languageIt != d->languages.end();) {
0911         if (languageIt->removeSupportingFormatter(**formatterIt)) {
0912             ++languageIt;
0913         } else {
0914             // Remove the language, for which no supporting formatters remain.
0915             languageIt = d->languages.erase(languageIt);
0916         }
0917     }
0918 
0919     d->formatters.erase(formatterIt);
0920 
0921     resetUi();
0922 }
0923 
0924 void SourceFormatterSelectionEdit::loadSettings(const KConfigGroup& config)
0925 {
0926     Q_D(SourceFormatterSelectionEdit);
0927 
0928     for (auto& lang : d->languages) {
0929         lang.readSelectedFormatterAndStyle(config);
0930     }
0931     resetUi();
0932 }
0933 
0934 void SourceFormatterSelectionEdit::resetUi()
0935 {
0936     Q_D(SourceFormatterSelectionEdit);
0937 
0938     qCDebug(SHELL) << "Resetting UI";
0939 
0940     d->currentLanguagePtr = nullptr;
0941 
0942     if (d->languages.empty()) {
0943         {
0944             const QSignalBlocker blocker(d->ui.cbLanguages);
0945             d->ui.cbLanguages->clear();
0946         }
0947         {
0948             const QSignalBlocker blocker(d->ui.cbFormatters);
0949             d->ui.cbFormatters->clear();
0950         }
0951         d->ui.formatterDescriptionButton->setWhatsThis(QString{});
0952         d->ui.usageHintLabel->hide();
0953         {
0954             const QSignalBlocker blocker(d->ui.styleList);
0955             d->ui.styleList->clear();
0956         }
0957 
0958         d->updateUiForCurrentStyle();
0959     } else {
0960         {
0961             const QSignalBlocker blocker(d->ui.cbLanguages);
0962             d->ui.cbLanguages->clear();
0963             d->fillLanguageCombobox();
0964         }
0965         Q_ASSERT(d->ui.cbLanguages->count() == static_cast<int>(d->languages.size()));
0966         selectLanguage(d->ui.cbLanguages->currentIndex());
0967     }
0968 }
0969 
0970 void SourceFormatterSelectionEdit::saveSettings(KConfigGroup& config) const
0971 {
0972     Q_D(const SourceFormatterSelectionEdit);
0973 
0974     // Store possibly modified user-defined styles. Store globally to allow reusing styles across sessions.
0975     KConfigGroup globalConfig = Core::self()->sourceFormatterControllerInternal()->globalConfig();
0976     for (const auto& formatter : d->formatters) {
0977         KConfigGroup fmtgrp = globalConfig.group(formatter->name());
0978 
0979         // Delete all user-defined styles so we don't leave behind styles deleted in the UI.
0980         const auto oldStyleGroups = fmtgrp.groupList();
0981         for (const QString& subgrp : oldStyleGroups) {
0982             if (subgrp.startsWith(userStyleNamePrefix)) {
0983                 fmtgrp.deleteGroup( subgrp );
0984             }
0985         }
0986 
0987         formatter->forEachUserDefinedStyle([&fmtgrp](const SourceFormatterStyle& style) {
0988             KConfigGroup styleGroup = fmtgrp.group(style.name());
0989             styleGroup.writeEntry(SourceFormatterController::styleCaptionKey(), style.caption());
0990             styleGroup.writeEntry(SourceFormatterController::styleShowPreviewKey(), style.usePreview());
0991             styleGroup.writeEntry(SourceFormatterController::styleContentKey(), style.content());
0992             styleGroup.writeEntry(SourceFormatterController::styleMimeTypesKey(), style.mimeTypesVariant());
0993             styleGroup.writeEntry(SourceFormatterController::styleSampleKey(), style.overrideSample());
0994         });
0995     }
0996     globalConfig.sync();
0997 
0998     // Store formatter and style selection for each language.
0999     for (const auto& lang : d->languages) {
1000         lang.saveSettings(config);
1001     }
1002 }
1003 
1004 void SourceFormatterSelectionEdit::selectLanguage(int index)
1005 {
1006     Q_D(SourceFormatterSelectionEdit);
1007 
1008     Q_ASSERT(index >= 0);
1009     Q_ASSERT(d->ui.cbLanguages->currentIndex() == index);
1010 
1011     Q_ASSERT(d->currentLanguagePtr != &d->languageSelectedInUi());
1012     d->currentLanguagePtr = &d->languageSelectedInUi();
1013 
1014     {
1015         const QSignalBlocker blocker(d->ui.cbFormatters);
1016         d->ui.cbFormatters->clear();
1017 
1018         for (auto* formatter : d->currentLanguage().supportingFormatters()) {
1019             d->ui.cbFormatters->addItem(formatter->formatter().caption(), QVariant::fromValue(formatter));
1020             if (formatter == &d->currentLanguage().selectedFormatter()) {
1021                 d->ui.cbFormatters->setCurrentIndex(d->ui.cbFormatters->count() - 1);
1022             }
1023         }
1024         Q_ASSERT_X(&d->currentLanguage().selectedFormatter() == &d->formatterSelectedInUi(), Q_FUNC_INFO,
1025                    "The selected formatter is not among the supporting formatters!");
1026     }
1027 
1028     d->updateUiForCurrentFormatter();
1029     // Selecting a language does not change configuration => don't emit changed().
1030 }
1031 
1032 void SourceFormatterSelectionEdit::selectFormatter(int index)
1033 {
1034     Q_D(SourceFormatterSelectionEdit);
1035 
1036     Q_ASSERT(index >= 0);
1037     Q_ASSERT(d->ui.cbFormatters->currentIndex() == index);
1038 
1039     const bool styleWasSelected = d->currentLanguage().selectedStyle();
1040 
1041     Q_ASSERT(&d->currentLanguage().selectedFormatter() != &d->formatterSelectedInUi());
1042     d->currentLanguage().setSelectedFormatter(d->formatterSelectedInUi());
1043 
1044     d->updateUiForCurrentFormatter();
1045 
1046     // Switching between formatters does not affect configuration if style remains
1047     // unselected => don't emit changed() then.
1048     Q_ASSERT(!d->currentLanguage().selectedStyle());
1049     if (styleWasSelected) {
1050         emit changed();
1051     }
1052 }
1053 
1054 void SourceFormatterSelectionEdit::styleSelectionChanged()
1055 {
1056     Q_D(SourceFormatterSelectionEdit);
1057 
1058     Q_ASSERT(d->currentLanguage().selectedStyle() != d->styleSelectedInUi());
1059     d->currentLanguage().setSelectedStyle(d->styleSelectedInUi());
1060 
1061     d->updateUiForCurrentStyle();
1062     emit changed();
1063 }
1064 
1065 void SourceFormatterSelectionEdit::deleteStyle()
1066 {
1067     Q_D(SourceFormatterSelectionEdit);
1068 
1069     const auto& currentStyle = d->validCurrentStyle();
1070 
1071     QStringList otherLanguageNames;
1072     std::vector<LanguageSettings*> otherLanguages;
1073     for (auto& lang : d->languages) {
1074         if (&lang != &d->currentLanguage() && lang.selectedStyle() == &currentStyle) {
1075             otherLanguageNames.push_back(lang.name());
1076             otherLanguages.push_back(&lang);
1077         }
1078     }
1079     // The deleted style can be used in other sessions or projects. But we show the warning dialog only if it
1080     // is selected for another language in the current session or project model (!otherLanguageNames.empty()).
1081     // Checking style selections in all other sessions is complicated, in all other projects - impossible.
1082     // Showing the warning dialog every time a style is deleted, even if the user just created it, can be
1083     // annoying. So the current behavior makes sense.
1084     if (!otherLanguageNames.empty()
1085         && KMessageBox::warningContinueCancel(
1086                this,
1087                i18n("The style %1 is also used for the following languages:\n%2.\nAre you sure you want to delete it?",
1088                     currentStyle.caption(), otherLanguageNames.join(QLatin1Char('\n'))),
1089                i18nc("@title:window", "Deleting Style"))
1090             != KMessageBox::Continue) {
1091         return;
1092     }
1093 
1094     const auto* const currentStyleItem = d->ui.styleList->currentItem();
1095     d->assertValidSelectedStyleItem(currentStyleItem);
1096     {
1097         const QSignalBlocker blocker(d->ui.styleList);
1098         delete currentStyleItem;
1099         // QListWidget selects the next item in the list when the currently selected item is destroyed.
1100         // Clear the selection for consistency with other languages where the deleted style was selected.
1101         d->ui.styleList->clearSelection();
1102     }
1103 
1104     d->currentLanguage().unselectStyle(currentStyle);
1105     for (auto* lang : otherLanguages) {
1106         lang->unselectStyle(currentStyle);
1107     }
1108     d->currentFormatter().removeStyle(currentStyle);
1109 
1110     d->updateUiForCurrentStyle();
1111     emit changed();
1112 }
1113 
1114 void SourceFormatterSelectionEdit::editStyle()
1115 {
1116     Q_D(SourceFormatterSelectionEdit);
1117 
1118     auto& currentStyle = d->validCurrentStyle();
1119 
1120     Q_ASSERT_X(d->currentFormatter().formatter().hasEditStyleWidget(), Q_FUNC_INFO,
1121                "The Edit... button must be disabled if the current style is not editable.");
1122     KDevelop::ScopedDialog<EditStyleDialog> dlg(d->currentFormatter().formatter(),
1123                                                 d->currentLanguage().defaultMimeType(), currentStyle, this);
1124     if (dlg->exec() == QDialog::Rejected) {
1125         return; // nothing changed
1126     }
1127 
1128     QString updatedContent = dlg->content();
1129     const bool updatedUsePreview = dlg->usePreview();
1130     if (updatedUsePreview == currentStyle.usePreview() && updatedContent == currentStyle.content()) {
1131         return; // nothing changed
1132     }
1133 
1134     currentStyle.setContent(std::move(updatedContent));
1135     currentStyle.setUsePreview(updatedUsePreview);
1136 
1137     // Don't call updateStyleButtons(), because editing a style doesn't affect the buttons.
1138     d->updatePreview();
1139     emit changed();
1140 }
1141 
1142 void SourceFormatterSelectionEdit::newStyle()
1143 {
1144     Q_D(SourceFormatterSelectionEdit);
1145 
1146     const auto& currentStyle = d->validCurrentStyle();
1147     auto& newStyle = d->currentFormatter().addNewStyle();
1148     newStyle.copyDataFrom(currentStyle);
1149     newStyle.setCaption(i18n("New %1", currentStyle.caption()));
1150 
1151     d->currentLanguage().setSelectedStyle(&newStyle);
1152 
1153     // Don't insert the new item into the correct ordered position, because the user will probably enter a
1154     // different caption (which affects ordering) right away. Note that when the user renames a style, it is
1155     // not immediately moved into its new ordered position to avoid the "jumping" of the renamed style.
1156     // Always keeping style items in their correct UI order positions is not trivial to implement, which is
1157     // another reason not to do it.
1158     // User-defined styles are displayed on top of predefined styles, so the eventual ordered position of
1159     // the new item is most likely closer to the top than to the bottom => place it at the top of the list.
1160     auto& newStyleItem = d->addStyleItem(newStyle, StyleCategory::UserDefined, NewItemPosition::Top);
1161     {
1162         const QSignalBlocker blocker(d->ui.styleList);
1163         // An edited item is not automatically selected, which results in weird UI behavior:
1164         // the source style (currentStyle) remains selected after the editing finishes.
1165         // Select the new style here to prevent this confusion.
1166         d->ui.styleList->setCurrentItem(&newStyleItem);
1167     }
1168     d->ui.styleList->editItem(&newStyleItem);
1169 
1170     d->updateUiForCurrentStyle();
1171     emit changed();
1172 }
1173 
1174 void SourceFormatterSelectionEdit::styleNameChanged(QListWidgetItem* item)
1175 {
1176     Q_D(SourceFormatterSelectionEdit);
1177 
1178     d->assertValidSelectedStyleItem(item);
1179     d->validCurrentStyle().setCaption(item->text());
1180 
1181     // Don't call updateUiForCurrentStyle(), because neither style buttons nor preview depend on style captions.
1182     emit changed();
1183 }
1184 
1185 #include "moc_sourceformatterselectionedit.cpp"