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(¤tFormatter == &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() == ¤tStyle) { 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"