From 4870558827c02c21b7b1221d27ee9f2526cfa4ce Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 25 May 2018 23:31:18 +0300 Subject: [PATCH] Apply formatting from context menu or shortcuts. --- .travis/build.sh | 2 +- Telegram/Resources/langs/lang.strings | 6 + .../SourceFiles/ui/widgets/input_fields.cpp | 195 +++++++++++++++++- .../SourceFiles/ui/widgets/input_fields.h | 10 + Telegram/SourceFiles/ui/widgets/menu.cpp | 13 +- 5 files changed, 221 insertions(+), 5 deletions(-) diff --git a/.travis/build.sh b/.travis/build.sh index 31ff9a4e7..4a274a8e8 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -588,7 +588,7 @@ buildCustomQt() { sudo rm -rf "$EXTERNAL/qt${QT_VERSION}" fi cd $QT_PATH - rm -rf * + sudo rm -rf * cd "$EXTERNAL" git clone git://code.qt.io/qt/qt5.git qt${QT_VERSION} diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b81d06a28..c075bbf3d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1255,6 +1255,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_new_version_minor" = "— Bug fixes and other minor improvements"; "lng_menu_insert_unicode" = "Insert Unicode control character"; +"lng_menu_formatting" = "Formatting"; +"lng_menu_formatting_bold" = "Bold"; +"lng_menu_formatting_italic" = "Italic"; +"lng_menu_formatting_monospace" = "Monospace"; +//"lng_menu_formatting_link" = "Create link"; +"lng_menu_formatting_clear" = "Plain text"; "lng_full_name" = "{first_name} {last_name}"; diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index 2e6cda5ef..c2394679f 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -39,11 +39,36 @@ const auto kNewlineChars = QString("\r\n") + QChar(0xfdd1) // QTextEndOfFrame + QChar(QChar::ParagraphSeparator) + QChar(QChar::LineSeparator); +const auto kClearFormatSequence = QKeySequence("ctrl+shift+n"); +const auto kMonospaceSequence = QKeySequence("ctrl+shift+m"); bool IsNewline(QChar ch) { return (kNewlineChars.indexOf(ch) >= 0); } +QString GetFullSimpleTextTag(const TextWithTags &textWithTags) { + const auto &text = textWithTags.text; + const auto &tags = textWithTags.tags; + const auto tag = (tags.size() == 1) ? tags[0] : TextWithTags::Tag(); + auto from = 0; + auto till = int(text.size()); + for (; from != till; ++from) { + if (!IsNewline(text[from]) && !chIsSpace(text[from])) { + break; + } + } + while (till != from) { + if (!IsNewline(text[till - 1]) && !chIsSpace(text[till - 1])) { + break; + } + --till; + } + return ((tag.offset <= from) + && (tag.offset + tag.length >= till)) + ? (tag.id == kTagPre ? kTagCode : tag.id) + : QString(); +} + class TagAccumulator { public: TagAccumulator(TextWithTags::Tags &tags) : _tags(tags) { @@ -2227,6 +2252,8 @@ void InputField::keyPressEventInner(QKeyEvent *e) { } } else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) { e->ignore(); + } else if (handleMarkdownKey(e)) { + e->accept(); } else if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { e->ignore(); #ifdef Q_OS_MAC @@ -2273,13 +2300,36 @@ void InputField::keyPressEventInner(QKeyEvent *e) { } } +bool InputField::handleMarkdownKey(QKeyEvent *e) { + if (!_markdownEnabled) { + return false; + } + const auto matches = [&](const QKeySequence &sequence) { + const auto searchKey = (e->modifiers() | e->key()) + & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); + const auto events = QKeySequence(searchKey); + return sequence.matches(events) == QKeySequence::ExactMatch; + }; + if (e == QKeySequence::Bold) { + toggleSelectionMarkdown(kTagBold); + } else if (e == QKeySequence::Italic) { + toggleSelectionMarkdown(kTagItalic); + } else if (matches(kMonospaceSequence)) { + toggleSelectionMarkdown(kTagCode); + } else if (matches(kClearFormatSequence)) { + clearSelectionMarkdown(); + } else { + return false; + } + return true; +} + const InstantReplaces &InputField::instantReplaces() const { return _mutableInstantReplaces; } bool InputField::processMarkdownReplaces(const QString &appended) { - if (appended.size() != 1 - || !_markdownEnabled) { + if (appended.size() != 1 || !_markdownEnabled) { return false; } const auto ch = appended[0]; @@ -2374,6 +2424,10 @@ void InputField::commitInstantReplacement( } auto cursor = textCursor(); + const auto currentTag = cursor.charFormat().property(kTagProperty); + if (currentTag == kTagPre || currentTag == kTagCode) { + return; + } cursor.setPosition(from); cursor.setPosition(till, QTextCursor::KeepAnchor); @@ -2513,6 +2567,53 @@ bool InputField::commitMarkdownReplacement( return true; } +void InputField::toggleSelectionMarkdown(const QString &tag) { + _reverseMarkdownReplacement = false; + const auto cursor = textCursor(); + const auto anchor = cursor.anchor(); + const auto position = cursor.position(); + const auto from = std::min(anchor, position); + const auto till = std::max(anchor, position); + if (from == till) { + return; + } + if (tag.isEmpty() + || GetFullSimpleTextTag(getTextWithTagsPart(from, till)) == tag) { + RemoveDocumentTags(_st, document(), from, till); + return; + } + const auto commitTag = [&] { + if (tag != kTagCode) { + return tag; + } + const auto leftForBlock = [&] { + if (!from) { + return true; + } + const auto text = getTextWithTagsPart(from - 1, from + 1).text; + return text.isEmpty() + || IsNewline(text[0]) + || IsNewline(text[text.size() - 1]); + }(); + const auto rightForBlock = [&] { + const auto text = getTextWithTagsPart(till - 1, till + 1).text; + return text.isEmpty() + || IsNewline(text[0]) + || IsNewline(text[text.size() - 1]); + }(); + return (leftForBlock && rightForBlock) ? kTagPre : kTagCode; + }(); + commitMarkdownReplacement(from, till, commitTag); + auto restorePosition = textCursor(); + restorePosition.setPosition(anchor); + restorePosition.setPosition(position, QTextCursor::KeepAnchor); + setTextCursor(restorePosition); +} + +void InputField::clearSelectionMarkdown() { + toggleSelectionMarkdown(QString()); +} + bool InputField::revertFormatReplace() { const auto cursor = textCursor(); const auto position = cursor.position(); @@ -2604,10 +2705,98 @@ bool InputField::revertFormatReplace() { void InputField::contextMenuEventInner(QContextMenuEvent *e) { if (const auto menu = _inner->createStandardContextMenu()) { - (new Ui::PopupMenu(nullptr, menu))->popup(e->globalPos()); + addMarkdownActions(menu); + _contextMenu = base::make_unique_q(nullptr, menu); + _contextMenu->popup(e->globalPos()); } } +void InputField::addMarkdownActions(not_null menu) { + if (!_markdownEnabled) { + return; + } + const auto formatting = new QAction(lang(lng_menu_formatting), menu); + addMarkdownMenuAction(menu, formatting); + + const auto submenu = new QMenu(menu); + formatting->setMenu(submenu); + + const auto cursor = textCursor(); + const auto from = std::min(cursor.anchor(), cursor.position()); + const auto till = std::max(cursor.anchor(), cursor.position()); + const auto textWithTags = getTextWithTagsPart(from, till); + const auto &text = textWithTags.text; + const auto &tags = textWithTags.tags; + formatting->setDisabled(text.isEmpty()); + if (text.isEmpty()) { + return; + } + const auto hasTags = !textWithTags.tags.isEmpty(); + const auto fullTag = GetFullSimpleTextTag(textWithTags); + const auto add = [&]( + LangKey key, + QKeySequence sequence, + bool disabled, + auto callback) { + const auto add = sequence.isEmpty() + ? QString() + : QChar('\t') + sequence.toString(QKeySequence::NativeText); + const auto action = new QAction(lang(key) + add, submenu); + connect(action, &QAction::triggered, this, callback); + action->setDisabled(disabled); + submenu->addAction(action); + }; + const auto addtag = [&]( + LangKey key, + QKeySequence sequence, + const QString &tag) { + const auto disabled = (fullTag == tag) + || (fullTag == kTagPre && tag == kTagCode); + add(key, sequence, (fullTag == tag), [=] { + toggleSelectionMarkdown(tag); + }); + }; + //const auto addlink = [&] { + // add(lng_menu_formatting_link, QKeySequence("ctrl+k"), false, [=] { + // createMarkdownLink(); + // }); + //}; + const auto addclear = [&] { + add(lng_menu_formatting_clear, kClearFormatSequence, !hasTags, [=] { + clearSelectionMarkdown(); + }); + }; + addtag(lng_menu_formatting_bold, QKeySequence::Bold, kTagBold); + addtag(lng_menu_formatting_italic, QKeySequence::Italic, kTagItalic); + + addtag(lng_menu_formatting_monospace, kMonospaceSequence, kTagCode); + + //submenu->addSeparator(); + //addlink(); + + submenu->addSeparator(); + addclear(); +} + +void InputField::addMarkdownMenuAction( + not_null menu, + not_null action) { + const auto actions = menu->actions(); + const auto before = [&] { + auto seenAfter = false; + for (const auto action : actions) { + if (seenAfter) { + return action; + } else if (action->objectName() == qstr("edit-delete")) { + seenAfter = true; + } + } + return (QAction*)nullptr; + }(); + menu->insertSeparator(before); + menu->insertAction(before, action); +} + void InputField::dropEventInner(QDropEvent *e) { _inDrop = true; _inner->QTextEdit::dropEvent(e); diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index 930e6113b..cf7f1a009 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -14,6 +14,8 @@ class UserData; namespace Ui { +class PopupMenu; + void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji); struct InstantReplaces { @@ -199,6 +201,8 @@ public: int till, const QString &tag, const QString &edge = QString()); + void toggleSelectionMarkdown(const QString &tag); + void clearSelectionMarkdown(); const QString &getLastText() const { return _lastTextWithTags.text; @@ -340,6 +344,11 @@ private: bool processMarkdownReplaces(const QString &appended); bool processMarkdownReplace(const QString &tag); + void addMarkdownActions(not_null menu); + void addMarkdownMenuAction( + not_null menu, + not_null action); + bool handleMarkdownKey(QKeyEvent *e); // We don't want accidentally detach InstantReplaces map. // So we access it only by const reference from this method. @@ -411,6 +420,7 @@ private: bool _correcting = false; MimeDataHook _mimeDataHook; + base::unique_qptr _contextMenu; QTextCharFormat _defaultCharFormat; diff --git a/Telegram/SourceFiles/ui/widgets/menu.cpp b/Telegram/SourceFiles/ui/widgets/menu.cpp index f6fd0e4b1..d79fd2df4 100644 --- a/Telegram/SourceFiles/ui/widgets/menu.cpp +++ b/Telegram/SourceFiles/ui/widgets/menu.cpp @@ -216,7 +216,18 @@ void Menu::paintEvent(QPaintEvent *e) { p.setPen(selected ? _st.itemFgOver : (enabled ? _st.itemFg : _st.itemFgDisabled)); p.drawTextLeft(_st.itemPadding.left(), _st.itemPadding.top(), width(), data.text); if (data.hasSubmenu) { - _st.arrow.paint(p, width() - _st.itemPadding.right() - _st.arrow.width(), (_itemHeight - _st.arrow.height()) / 2, width()); + const auto left = width() - _st.itemPadding.right() - _st.arrow.width(); + const auto top = (_itemHeight - _st.arrow.height()) / 2; + if (enabled) { + _st.arrow.paint(p, left, top, width()); + } else { + _st.arrow.paint( + p, + left, + top, + width(), + _st.itemFgDisabled->c); + } } else if (!data.shortcut.isEmpty()) { p.setPen(selected ? _st.itemFgShortcutOver : (enabled ? _st.itemFgShortcut : _st.itemFgShortcutDisabled)); p.drawTextRight(_st.itemPadding.right(), _st.itemPadding.top(), width(), data.shortcut);