diff --git a/Telegram/Resources/icons/menu/rich/blockquote.svg b/Telegram/Resources/icons/menu/rich/blockquote.svg
new file mode 100644
index 0000000000..7b99a19145
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/blockquote.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/bullet_list.svg b/Telegram/Resources/icons/menu/rich/bullet_list.svg
new file mode 100644
index 0000000000..36fb9d099c
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/bullet_list.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/details.svg b/Telegram/Resources/icons/menu/rich/details.svg
new file mode 100644
index 0000000000..2b5bc0c80c
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/details.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/divider.svg b/Telegram/Resources/icons/menu/rich/divider.svg
new file mode 100644
index 0000000000..7e392c330c
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/divider.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/heading.svg b/Telegram/Resources/icons/menu/rich/heading.svg
new file mode 100644
index 0000000000..a8f848b4f5
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/heading.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/math.svg b/Telegram/Resources/icons/menu/rich/math.svg
new file mode 100644
index 0000000000..064f32eed3
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/math.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/ordered_list.svg b/Telegram/Resources/icons/menu/rich/ordered_list.svg
new file mode 100644
index 0000000000..5f9d9aa880
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/ordered_list.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/pullquote.svg b/Telegram/Resources/icons/menu/rich/pullquote.svg
new file mode 100644
index 0000000000..e6ee26c9cc
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/pullquote.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/table.svg b/Telegram/Resources/icons/menu/rich/table.svg
new file mode 100644
index 0000000000..ec34938fd8
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/table.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/icons/menu/rich/task_list.svg b/Telegram/Resources/icons/menu/rich/task_list.svg
new file mode 100644
index 0000000000..776fef8606
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/task_list.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index c4e2b4fb51..828578f798 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -7166,8 +7166,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_stop_sure" = "Stop";
"lng_article_menu_item" = "Article";
"lng_article_editor_title" = "Article";
+"lng_article_insert_heading" = "Heading";
"lng_article_insert_heading1" = "Heading 1";
+"lng_article_insert_heading2" = "Heading 2";
+"lng_article_insert_heading3" = "Heading 3";
+"lng_article_insert_heading4" = "Heading 4";
+"lng_article_insert_heading5" = "Heading 5";
+"lng_article_insert_heading6" = "Heading 6";
"lng_article_insert_blockquote" = "Blockquote";
+"lng_article_insert_math" = "Math";
+"lng_article_insert_divider" = "Divider";
+"lng_article_insert_ordered_list" = "Ordered List";
+"lng_article_insert_bullet_list" = "Bullet List";
+"lng_article_insert_task_list" = "Task List";
+"lng_article_insert_pullquote" = "Pullquote";
+"lng_article_insert_media" = "Attach Media";
+"lng_article_insert_details" = "Details";
+"lng_article_insert_table" = "Table";
+"lng_article_insert_map" = "Map";
"lng_polls_menu_item" = "Poll";
"lng_polls_create" = "Create poll";
"lng_polls_create_title" = "New poll";
diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style
index fe88bac174..69c48d8152 100644
--- a/Telegram/SourceFiles/iv/iv.style
+++ b/Telegram/SourceFiles/iv/iv.style
@@ -79,11 +79,30 @@ ivWrongLayoutLink: LinkButton(defaultLinkButton) {
overColor: windowSubTextFg;
padding: margins(5px, 5px, 5px, 5px);
}
-ivEditorBodyPadding: margins(0px, 0px, 0px, 0px);
-ivEditorToolbarPadding: margins(12px, 24px, 12px, 24px);
-ivEditorToolbarButtonsSkip: 8px;
-ivEditorToolbarButtonMinWidth: 132px;
-ivEditorToolbarButtonHeight: 42px;
+ivEditorBodyPadding: margins(0px, 8px, 0px, 8px);
+ivEditorToolbarPadding: margins(8px, defaultVerticalListSkip, 8px, defaultVerticalListSkip);
+ivEditorToolbarButtonSize: 36px;
+ivEditorToolbarButton: IconButton(defaultIconButton) {
+ width: ivEditorToolbarButtonSize;
+ height: ivEditorToolbarButtonSize;
+ iconPosition: point(-1px, -1px);
+ rippleAreaPosition: point(0px, 0px);
+ rippleAreaSize: ivEditorToolbarButtonSize;
+ ripple: defaultRippleAnimationBgOver;
+}
+ivEditorToolbarButtonSkip: 6px;
+ivEditorToolbarRowSkip: 0px;
+ivEditorToolbarHeadingIcon: icon {{ "menu/rich/heading-24x24", menuIconColor }};
+ivEditorToolbarBlockquoteIcon: icon {{ "menu/rich/blockquote-24x24", menuIconColor }};
+ivEditorToolbarMathIcon: icon {{ "menu/rich/math-24x24", menuIconColor }};
+ivEditorToolbarDividerIcon: icon {{ "menu/rich/divider-24x24", menuIconColor }};
+ivEditorToolbarOrderedListIcon: icon {{ "menu/rich/ordered_list-24x24", menuIconColor }};
+ivEditorToolbarBulletListIcon: icon {{ "menu/rich/bullet_list-24x24", menuIconColor }};
+ivEditorToolbarTaskListIcon: icon {{ "menu/rich/task_list-24x24", menuIconColor }};
+ivEditorToolbarPullquoteIcon: icon {{ "menu/rich/pullquote-24x24", menuIconColor }};
+ivEditorToolbarAttachIcon: icon {{ "chat/input_attach", menuIconColor }};
+ivEditorToolbarDetailsIcon: icon {{ "menu/rich/details-24x24", menuIconColor }};
+ivEditorToolbarTableIcon: icon {{ "menu/rich/table-24x24", menuIconColor }};
ivEditorInlineFieldMargins: margins(4px, 0px, 6px, 0px);
ivEditorMinHeight: boxWidth;
ivEditorInputField: InputField(defaultInputField) {
diff --git a/Telegram/SourceFiles/iv/iv_editor_box.cpp b/Telegram/SourceFiles/iv/iv_editor_box.cpp
index 46496527da..d221c524e8 100644
--- a/Telegram/SourceFiles/iv/iv_editor_box.cpp
+++ b/Telegram/SourceFiles/iv/iv_editor_box.cpp
@@ -7,32 +7,412 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/iv_editor_box.h"
-#include "data/data_msg_id.h"
+#include
+#include
+#include
+#include
+#include
+
+#include "base/flat_map.h"
+#include "base/const_string.h"
+#include "base/unique_qptr.h"
+#include "base/weak_ptr.h"
+#include "core/file_utilities.h"
+#include "core/mime_type.h"
+#include "core/shortcuts.h"
#include "ui/image/image_location.h"
+#include "data/data_location.h"
#include "data/data_types.h"
#include "iv/iv_editor_state.h"
#include "iv/iv_editor_widget.h"
#include "lang/lang_keys.h"
+#include "main/main_app_config.h"
+#include "main/main_session.h"
+#include "settings.h"
+#include "ui/emoji_config.h"
+#include "storage/storage_account.h"
+#include "ui/controls/location_picker.h"
#include "ui/layers/generic_box.h"
+#include "ui/rect_part.h"
+#include "ui/ui_utility.h"
#include "ui/widgets/buttons.h"
-#include "ui/wrap/vertical_layout.h"
-#include "styles/style_layers.h"
-#include "styles/style_settings.h"
-
-#include
+#include "ui/widgets/popup_menu.h"
+#include "ui/widgets/tooltip.h"
#include "window/window_session_controller.h"
+#include "mainwindow.h"
+#include "mainwidget.h"
+#include
#include
+#include
+#include
+
+#include "styles/style_iv.h"
+#include "styles/style_layers.h"
+#include "styles/style_menu_icons.h"
+#include "styles/style_widgets.h"
namespace Iv::Editor {
namespace {
+struct ToolbarButton {
+ object_ptr widget;
+ Fn()> tooltipFactory;
+};
+
+class Toolbar final : public Ui::RpWidget {
+public:
+ Toolbar(
+ QWidget *parent,
+ not_null controller,
+ not_null peer,
+ not_null editor,
+ QPointer tooltipParent);
+
+ int resizeGetHeight(int width) override;
+
+protected:
+ bool eventFilter(QObject *object, QEvent *event) override;
+
+private:
+ not_null addButton(
+ QString label,
+ Fn()> tooltipFactory,
+ const style::icon *icon,
+ Fn callback);
+ void addInsertButtons();
+ void showHeadingMenu(not_null button);
+ void chooseMedia();
+ void applyMediaResult(FileDialog::OpenResult &&result);
+ void chooseMap();
+ void showTooltip(not_null button);
+ void hideTooltip();
+ void updateTooltipGeometry();
+ [[nodiscard]] ToolbarButton *buttonData(not_null button);
+
+ const not_null _controller;
+ const not_null _peer;
+ const QPointer _editor;
+ const QPointer _tooltipParent;
+ const Ui::LocationPickerConfig _mapsConfig;
+ std::vector _buttons;
+ base::unique_qptr _tooltip;
+ base::unique_qptr _menu;
+ Ui::IconButton *_hovered = nullptr;
+
+};
+
+[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig(
+ not_null session) {
+ const auto &appConfig = session->appConfig();
+ auto map = appConfig.get>(
+ u"tdesktop_config_map"_q,
+ base::flat_map());
+ return {
+ .mapsToken = map[u"maps"_q],
+ .geoToken = map[u"geo"_q],
+ };
+}
+
+[[nodiscard]] QString HeadingLabel(int level) {
+ switch (level) {
+ case 1: return tr::lng_article_insert_heading1(tr::now);
+ case 2: return tr::lng_article_insert_heading2(tr::now);
+ case 3: return tr::lng_article_insert_heading3(tr::now);
+ case 4: return tr::lng_article_insert_heading4(tr::now);
+ case 5: return tr::lng_article_insert_heading5(tr::now);
+ case 6: return tr::lng_article_insert_heading6(tr::now);
+ }
+ return tr::lng_article_insert_heading1(tr::now);
+}
+
+[[nodiscard]] std::optional MediaTypeForPath(
+ const QString &path) {
+ const auto mime = Core::MimeTypeForFile(QFileInfo(path)).name();
+ if (Core::FileIsImage(path, mime)) {
+ return State::InsertBlockType::Photo;
+ } else if (mime.startsWith(u"video/"_q)) {
+ return State::InsertBlockType::Video;
+ } else if (mime.startsWith(u"audio/"_q)) {
+ return State::InsertBlockType::Audio;
+ }
+ return std::nullopt;
+}
+
+Toolbar::Toolbar(
+ QWidget *parent,
+ not_null controller,
+ not_null peer,
+ not_null editor,
+ QPointer tooltipParent)
+: Ui::RpWidget(parent)
+, _controller(controller)
+, _peer(peer)
+, _editor(editor.get())
+, _tooltipParent(std::move(tooltipParent))
+, _mapsConfig(ResolveMapsConfig(&controller->session())) {
+ setMouseTracking(true);
+ addInsertButtons();
+}
+
+not_null Toolbar::addButton(
+ QString label,
+ Fn()> tooltipFactory,
+ const style::icon *icon,
+ Fn callback) {
+ auto button = object_ptr(this, st::ivEditorToolbarButton);
+ const auto raw = button.data();
+ raw->setAccessibleName(label);
+ raw->setIconOverride(icon, icon);
+ raw->setClickedCallback([=, callback = std::move(callback)] {
+ hideTooltip();
+ callback();
+ });
+ raw->installEventFilter(this);
+ _buttons.push_back({
+ std::move(button),
+ std::move(tooltipFactory),
+ });
+ return raw;
+}
+
+void Toolbar::addInsertButtons() {
+ const auto insert = [=](State::InsertAction action) {
+ if (_editor) {
+ _editor->insertBlock(action);
+ }
+ };
+ const auto insertType = [=](State::InsertBlockType type) {
+ insert({ .type = type });
+ };
+
+ const auto heading = addButton(
+ tr::lng_article_insert_heading(tr::now),
+ [] { return tr::lng_article_insert_heading(tr::marked); },
+ &st::ivEditorToolbarHeadingIcon,
+ [] {});
+ heading->setClickedCallback([=] {
+ hideTooltip();
+ showHeadingMenu(heading);
+ });
+ addButton(
+ tr::lng_article_insert_blockquote(tr::now),
+ [] { return tr::lng_article_insert_blockquote(tr::marked); },
+ &st::ivEditorToolbarBlockquoteIcon,
+ [=] { insertType(State::InsertBlockType::Blockquote); });
+ addButton(
+ tr::lng_article_insert_math(tr::now),
+ [] { return tr::lng_article_insert_math(tr::marked); },
+ &st::ivEditorToolbarMathIcon,
+ [=] { insertType(State::InsertBlockType::Math); });
+ addButton(
+ tr::lng_article_insert_divider(tr::now),
+ [] { return tr::lng_article_insert_divider(tr::marked); },
+ &st::ivEditorToolbarDividerIcon,
+ [=] { insertType(State::InsertBlockType::Divider); });
+ addButton(
+ tr::lng_article_insert_ordered_list(tr::now),
+ [] { return tr::lng_article_insert_ordered_list(tr::marked); },
+ &st::ivEditorToolbarOrderedListIcon,
+ [=] { insertType(State::InsertBlockType::OrderedList); });
+ addButton(
+ tr::lng_article_insert_bullet_list(tr::now),
+ [] { return tr::lng_article_insert_bullet_list(tr::marked); },
+ &st::ivEditorToolbarBulletListIcon,
+ [=] { insertType(State::InsertBlockType::BulletList); });
+ addButton(
+ tr::lng_article_insert_task_list(tr::now),
+ [] { return tr::lng_article_insert_task_list(tr::marked); },
+ &st::ivEditorToolbarTaskListIcon,
+ [=] { insertType(State::InsertBlockType::TaskList); });
+ addButton(
+ tr::lng_article_insert_pullquote(tr::now),
+ [] { return tr::lng_article_insert_pullquote(tr::marked); },
+ &st::ivEditorToolbarPullquoteIcon,
+ [=] { insertType(State::InsertBlockType::Pullquote); });
+ addButton(
+ tr::lng_article_insert_media(tr::now),
+ [] { return tr::lng_article_insert_media(tr::marked); },
+ &st::ivEditorToolbarAttachIcon,
+ [=] { chooseMedia(); });
+ addButton(
+ tr::lng_article_insert_details(tr::now),
+ [] { return tr::lng_article_insert_details(tr::marked); },
+ &st::ivEditorToolbarDetailsIcon,
+ [=] { insertType(State::InsertBlockType::Details); });
+ addButton(
+ tr::lng_article_insert_table(tr::now),
+ [] { return tr::lng_article_insert_table(tr::marked); },
+ &st::ivEditorToolbarTableIcon,
+ [=] { insertType(State::InsertBlockType::Table); });
+
+ if (Ui::LocationPicker::Available(_mapsConfig)) {
+ addButton(
+ tr::lng_article_insert_map(tr::now),
+ [] { return tr::lng_article_insert_map(tr::marked); },
+ &st::menuIconAddress,
+ [=] { chooseMap(); });
+ }
+}
+
+void Toolbar::showHeadingMenu(not_null button) {
+ _menu = base::make_unique_q(this);
+ for (const auto level : std::array{ 1, 2, 3, 4, 5, 6 }) {
+ _menu->addAction(
+ HeadingLabel(level),
+ [=] {
+ if (_editor) {
+ _editor->insertBlock({
+ .type = State::InsertBlockType::Heading,
+ .headingLevel = level,
+ });
+ }
+ });
+ }
+ _menu->popup(button->mapToGlobal(QPoint(0, button->height())));
+}
+
+void Toolbar::chooseMedia() {
+ const auto weak = QPointer(this);
+ FileDialog::GetOpenPath(
+ QPointer(this),
+ tr::lng_article_insert_media(tr::now),
+ FileDialog::AllFilesFilter(),
+ [=](FileDialog::OpenResult &&result) {
+ if (weak) {
+ weak->applyMediaResult(std::move(result));
+ }
+ });
+}
+
+void Toolbar::applyMediaResult(FileDialog::OpenResult &&result) {
+ if (result.paths.isEmpty()) {
+ return;
+ }
+ const auto type = MediaTypeForPath(result.paths.front());
+ if (!type) {
+ _controller->showToast(tr::lng_edit_media_invalid_file(tr::now));
+ return;
+ }
+ if (_editor) {
+ _editor->insertMedia(*type);
+ }
+}
+
+void Toolbar::chooseMap() {
+ const auto weak = QPointer(this);
+ const auto session = &_controller->session();
+ Ui::LocationPicker::Show({
+ .parent = _controller->widget().get(),
+ .config = _mapsConfig,
+ .chooseLabel = tr::lng_maps_point_send(),
+ .recipient = _peer,
+ .session = session,
+ .callback = [=](Data::InputVenue venue) {
+ if (weak && weak->_editor) {
+ weak->_editor->insertMap(venue.lat, venue.lon);
+ }
+ },
+ .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
+ .storageId = session->local().resolveStorageIdBots(),
+ .closeRequests = _controller->content()->death(),
+ });
+}
+
+int Toolbar::resizeGetHeight(int width) {
+ const auto padding = st::ivEditorToolbarPadding;
+ const auto buttonWidth = st::ivEditorToolbarButton.width;
+ const auto buttonHeight = st::ivEditorToolbarButton.height;
+ const auto right = width - padding.right();
+ auto left = padding.left();
+ auto top = padding.top();
+ for (const auto &button : _buttons) {
+ if (left > padding.left() && left + buttonWidth > right) {
+ left = padding.left();
+ top += buttonHeight + st::ivEditorToolbarRowSkip;
+ }
+ button.widget->moveToLeft(left, top, width);
+ left += buttonWidth + st::ivEditorToolbarButtonSkip;
+ }
+ updateTooltipGeometry();
+ return top + buttonHeight + padding.bottom();
+}
+
+bool Toolbar::eventFilter(QObject *object, QEvent *event) {
+ for (const auto &data : _buttons) {
+ if (data.widget.data() != object) {
+ continue;
+ }
+ const auto button = data.widget.data();
+ if (event->type() == QEvent::Enter) {
+ showTooltip(not_null(button));
+ } else if (event->type() == QEvent::Leave && _hovered == button) {
+ hideTooltip();
+ }
+ break;
+ }
+ return Ui::RpWidget::eventFilter(object, event);
+}
+
+void Toolbar::showTooltip(not_null button) {
+ hideTooltip();
+ const auto data = buttonData(button);
+ if (!data) {
+ return;
+ }
+ _hovered = button;
+ const auto tooltipParent = _tooltipParent
+ ? _tooltipParent.data()
+ : (parentWidget() ? parentWidget() : this);
+ _tooltip.reset(Ui::CreateChild(
+ tooltipParent,
+ Ui::MakeNiceTooltipLabel(
+ tooltipParent,
+ data->tooltipFactory(),
+ st::boxWideWidth,
+ st::defaultImportantTooltipLabel),
+ st::defaultImportantTooltip));
+ _tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
+ _tooltip->toggleFast(false);
+ updateTooltipGeometry();
+ _tooltip->raise();
+ _tooltip->toggleAnimated(true);
+}
+
+void Toolbar::hideTooltip() {
+ _hovered = nullptr;
+ if (_tooltip) {
+ _tooltip->toggleFast(false);
+ _tooltip = nullptr;
+ }
+}
+
+void Toolbar::updateTooltipGeometry() {
+ if (!_tooltip || !_hovered) {
+ return;
+ }
+ const auto tooltipParent = _tooltip->parentWidget();
+ const auto geometry = Ui::MapFrom(
+ tooltipParent,
+ _hovered,
+ _hovered->rect());
+ _tooltip->pointAt(geometry, RectPart::Top | RectPart::Center);
+}
+
+ToolbarButton *Toolbar::buttonData(not_null button) {
+ for (auto &data : _buttons) {
+ if (data.widget.data() == button.get()) {
+ return &data;
+ }
+ }
+ return nullptr;
+}
+
void SetupBox(
not_null box,
not_null controller,
not_null peer) {
- box->setTitle(tr::lng_article_editor_title());
box->setWidth(st::boxWideWidth);
box->setNoContentMargin(true);
@@ -43,21 +423,13 @@ void SetupBox(
peer,
state),
style::margins());
- const auto toolbar = box->setPinnedToTopContent(
- object_ptr(box));
-
- toolbar->add(object_ptr(
- toolbar,
- tr::lng_article_insert_heading1(),
- st::settingsButtonNoIcon))->setClickedCallback([=] {
- editor->insertHeading1();
- });
- toolbar->add(object_ptr(
- toolbar,
- tr::lng_article_insert_blockquote(),
- st::settingsButtonNoIcon))->setClickedCallback([=] {
- editor->insertBlockquote();
- });
+ const auto tooltipParent = box->getDelegate()->outerContainer();
+ box->setPinnedToTopContent(object_ptr(
+ box,
+ controller,
+ peer,
+ editor,
+ tooltipParent));
box->setFocusCallback([=] {
editor->activateInitialNode();
diff --git a/Telegram/SourceFiles/iv/iv_editor_state.cpp b/Telegram/SourceFiles/iv/iv_editor_state.cpp
index fb47015fc3..5ff7e19e7a 100644
--- a/Telegram/SourceFiles/iv/iv_editor_state.cpp
+++ b/Telegram/SourceFiles/iv/iv_editor_state.cpp
@@ -8,21 +8,72 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/iv_editor_state.h"
#include
+#include
namespace Iv::Editor {
namespace {
using Block = RichPage::Block;
-using RichText = RichPage::RichText;
+using BlockContainerKind = State::BlockContainerKind;
+using BlockContainerPath = State::BlockContainerPath;
using BlockKind = RichPage::BlockKind;
-using TextSlot = State::TextSlot;
+using BlockPath = State::BlockPath;
+using FieldMode = State::FieldMode;
+using InsertBlockType = State::InsertBlockType;
+using InsertionAnchor = State::InsertionAnchor;
+using LeafKind = State::LeafKind;
+using LeafPath = State::LeafPath;
+using ListItem = RichPage::ListItem;
+using ListKind = RichPage::ListKind;
+using RemovalKind = State::RemovalKind;
+using RemovalTarget = State::RemovalTarget;
+using RichText = RichPage::RichText;
+using TableCell = RichPage::TableCell;
+using TaskState = RichPage::TaskState;
using TextNodeDescriptor = State::TextNodeDescriptor;
+[[nodiscard]] BlockContainerPath BlockChildrenContainer(BlockPath path) {
+ auto result = std::move(path.container);
+ result.steps.push_back({
+ .kind = BlockContainerKind::BlockChildren,
+ .blockIndex = path.index,
+ });
+ return result;
+}
+
+[[nodiscard]] BlockContainerPath ListItemChildrenContainer(
+ BlockPath path,
+ int itemIndex) {
+ auto result = std::move(path.container);
+ result.steps.push_back({
+ .kind = BlockContainerKind::ListItemChildren,
+ .blockIndex = path.index,
+ .listItemIndex = itemIndex,
+ });
+ return result;
+}
+
+[[nodiscard]] bool ContainerHasPrefix(
+ const BlockContainerPath &path,
+ const BlockContainerPath &prefix) {
+ if (path.steps.size() < prefix.steps.size()) {
+ return false;
+ }
+ return std::equal(
+ prefix.steps.begin(),
+ prefix.steps.end(),
+ path.steps.begin());
+}
+
+[[nodiscard]] bool StringIsEmpty(const QString &text) {
+ return text.trimmed().isEmpty();
+}
+
} // namespace
State::State()
: _richPage(std::make_shared()) {
- _richPage->blocks.push_back(makeParagraphBlock());
+ _richPage->blocks.push_back(MakeParagraphBlock());
rebuild();
}
@@ -56,91 +107,403 @@ bool State::setActiveTextByOrdinal(int ordinal) {
TextWithEntities State::activeText() const {
const auto descriptor = textNode(_activeTextOrdinal);
- const auto current = descriptor ? richText(*descriptor) : nullptr;
- return current
- ? stripEditModeWrapperEntities(current->text)
- : TextWithEntities();
+ if (!descriptor) {
+ return TextWithEntities();
+ }
+ if (const auto current = richText(descriptor->leaf)) {
+ return StripEditModeWrapperEntities(current->text);
+ }
+ if (const auto current = rawText(descriptor->leaf)) {
+ return MakeText(*current);
+ }
+ return TextWithEntities();
}
void State::applyActiveText(TextWithEntities text) {
const auto descriptor = textNode(_activeTextOrdinal);
- const auto current = descriptor ? richText(*descriptor) : nullptr;
- if (!current) {
+ if (!descriptor) {
return;
}
- current->text = std::move(text);
+ if (auto current = richText(descriptor->leaf)) {
+ current->text = std::move(text);
+ rebuild();
+ return;
+ }
+ if (auto current = rawText(descriptor->leaf)) {
+ *current = std::move(text.text);
+ rebuild();
+ }
+}
+
+FieldMode State::activeFieldMode() const {
+ const auto descriptor = textNode(_activeTextOrdinal);
+ return descriptor ? descriptor->mode : FieldMode::Rich;
+}
+
+QString State::activeRawText() const {
+ const auto descriptor = textNode(_activeTextOrdinal);
+ if (!descriptor) {
+ return QString();
+ }
+ if (const auto current = rawText(descriptor->leaf)) {
+ return *current;
+ }
+ if (const auto current = richText(descriptor->leaf)) {
+ return StripEditModeWrapperEntities(current->text).text;
+ }
+ return QString();
+}
+
+void State::applyActiveRawText(QString text) {
+ const auto descriptor = textNode(_activeTextOrdinal);
+ if (!descriptor) {
+ return;
+ }
+ if (auto current = rawText(descriptor->leaf)) {
+ *current = std::move(text);
+ rebuild();
+ return;
+ }
+ if (auto current = richText(descriptor->leaf)) {
+ current->text = MakeText(std::move(text));
+ rebuild();
+ }
+}
+
+int State::activeTextLength() const {
+ return activeRawText().size();
+}
+
+std::optional State::previousEditableOrdinal() const {
+ return adjacentEditableOrdinal(false);
+}
+
+std::optional State::nextEditableOrdinal() const {
+ return adjacentEditableOrdinal(true);
+}
+
+bool State::isActiveTopLevelParagraph() const {
+ const auto descriptor = textNode(_activeTextOrdinal);
+ if (!descriptor || !descriptor->leaf.block.container.steps.empty()) {
+ return false;
+ }
+ const auto owner = block(descriptor->leaf.block);
+ return owner && owner->kind == BlockKind::Paragraph;
+}
+
+bool State::activeOwnerIsEmpty() const {
+ const auto descriptor = textNode(_activeTextOrdinal);
+ return descriptor && removalTargetIsEmpty(descriptor->removalTarget);
+}
+
+std::optional State::removeActiveOwnerAndSelectAdjacent(bool forward) {
+ const auto descriptor = textNode(_activeTextOrdinal);
+ if (!descriptor || !removalTargetIsEmpty(descriptor->removalTarget)) {
+ return std::nullopt;
+ }
+ const auto target = descriptor->removalTarget;
+ auto first = _activeTextOrdinal;
+ auto last = _activeTextOrdinal;
+ while (first > 0 && _textNodes[first - 1].removalTarget == target) {
+ --first;
+ }
+ while (last + 1 < textNodeCount()
+ && _textNodes[last + 1].removalTarget == target) {
+ ++last;
+ }
+ auto adjacent = std::optional();
+ if (forward) {
+ for (auto i = last + 1, count = textNodeCount(); i != count; ++i) {
+ if (!(_textNodes[i].removalTarget == target)) {
+ adjacent = _textNodes[i].leaf;
+ break;
+ }
+ }
+ } else {
+ for (auto i = first; i != 0; --i) {
+ if (!(_textNodes[i - 1].removalTarget == target)) {
+ adjacent = _textNodes[i - 1].leaf;
+ break;
+ }
+ }
+ }
+ if (!removeTarget(target)) {
+ return std::nullopt;
+ }
rebuild();
+ if (adjacent && (!forward || target.kind == RemovalKind::TableCell)) {
+ const auto ordinal = textNodeOrdinal(*adjacent);
+ if (setActiveTextByOrdinal(ordinal)) {
+ return _activeTextOrdinal;
+ }
+ }
+ if (!textNodeCount()) {
+ return std::nullopt;
+ }
+ const auto fallback = forward
+ ? std::min(first, textNodeCount() - 1)
+ : std::max(std::min(first - 1, textNodeCount() - 1), 0);
+ if (!setActiveTextByOrdinal(fallback)) {
+ ensureActiveTextOrdinal();
+ }
+ return _activeTextOrdinal;
}
int State::ensureTrailingParagraphActive() {
if (_richPage->blocks.empty()
|| _richPage->blocks.back().kind != BlockKind::Paragraph) {
- _richPage->blocks.push_back(makeParagraphBlock());
+ _richPage->blocks.push_back(MakeParagraphBlock());
}
- const auto path = std::vector{ int(_richPage->blocks.size()) - 1 };
+ const auto path = BlockPath{
+ .container = BlockContainerPath(),
+ .index = int(_richPage->blocks.size()) - 1,
+ };
rebuild();
- if (!setActiveTextByOrdinal(textNodeOrdinal(path, TextSlot::Text))) {
+ const auto ordinal = textNodeOrdinal({
+ .kind = LeafKind::BlockText,
+ .block = path,
+ });
+ if (!setActiveTextByOrdinal(ordinal)) {
ensureActiveTextOrdinal();
}
return _activeTextOrdinal;
}
void State::insertHeading1AfterActive() {
- insertBlockAfterActive(makeHeading1Block(), TextSlot::Text);
+ insertBlockAfterActive({
+ .type = InsertBlockType::Heading,
+ .headingLevel = 1,
+ });
}
void State::insertBlockquoteAfterActive() {
- insertBlockAfterActive(makeBlockquoteBlock(), TextSlot::Text);
+ insertBlockAfterActive({
+ .type = InsertBlockType::Blockquote,
+ });
}
-Block *State::blockFromPath(const std::vector &path) {
+void State::insertBlockAfterActive(InsertAction action) {
+ auto anchor = InsertionAnchor{
+ .container = BlockContainerPath(),
+ .blockIndex = int(_richPage->blocks.size()) - 1,
+ };
+ const auto descriptor = textNode(_activeTextOrdinal);
+ if (descriptor) {
+ anchor = descriptor->insertionAnchor;
+ }
+ auto *blocks = blockContainer(anchor.container);
+ if (!blocks) {
+ anchor = {
+ .container = BlockContainerPath(),
+ .blockIndex = int(_richPage->blocks.size()) - 1,
+ };
+ blocks = &_richPage->blocks;
+ }
+ const auto insertAt = std::clamp(
+ anchor.blockIndex + 1,
+ 0,
+ int(blocks->size()));
+ blocks->insert(blocks->begin() + insertAt, makeBlock(action));
+ rebuild();
+ focusInsertedBlock({
+ .container = anchor.container,
+ .index = insertAt,
+ });
+}
+
+std::vector *State::blockContainer(const BlockContainerPath &path) {
auto *blocks = &_richPage->blocks;
- auto *result = static_cast(nullptr);
- for (const auto index : path) {
- if (index < 0 || index >= int(blocks->size())) {
+ for (const auto &step : path.steps) {
+ if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
+ return nullptr;
+ }
+ auto &parent = (*blocks)[step.blockIndex];
+ if (step.kind == BlockContainerKind::BlockChildren) {
+ blocks = &parent.blocks;
+ } else if (step.kind == BlockContainerKind::ListItemChildren) {
+ if (step.listItemIndex < 0
+ || step.listItemIndex >= int(parent.listItems.size())) {
+ return nullptr;
+ }
+ blocks = &parent.listItems[step.listItemIndex].blocks;
+ } else {
return nullptr;
}
- result = &(*blocks)[index];
- blocks = &result->blocks;
}
- return result;
+ return blocks;
}
-const Block *State::blockFromPath(const std::vector &path) const {
+const std::vector *State::blockContainer(
+ const BlockContainerPath &path) const {
const auto *blocks = &_richPage->blocks;
- auto *result = static_cast(nullptr);
- for (const auto index : path) {
- if (index < 0 || index >= int(blocks->size())) {
+ for (const auto &step : path.steps) {
+ if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
+ return nullptr;
+ }
+ const auto &parent = (*blocks)[step.blockIndex];
+ if (step.kind == BlockContainerKind::BlockChildren) {
+ blocks = &parent.blocks;
+ } else if (step.kind == BlockContainerKind::ListItemChildren) {
+ if (step.listItemIndex < 0
+ || step.listItemIndex >= int(parent.listItems.size())) {
+ return nullptr;
+ }
+ blocks = &parent.listItems[step.listItemIndex].blocks;
+ } else {
return nullptr;
}
- result = &(*blocks)[index];
- blocks = &result->blocks;
}
- return result;
+ return blocks;
}
-std::vector *State::blockContainerFromPath(const std::vector &path) {
- if (path.empty()) {
- return &_richPage->blocks;
- }
- const auto parent = blockFromPath(path);
- return parent ? &parent->blocks : nullptr;
-}
-
-RichText *State::richText(const TextNodeDescriptor &descriptor) {
- const auto block = blockFromPath(descriptor.blockPath);
- if (!block) {
+Block *State::block(const BlockPath &path) {
+ const auto blocks = blockContainer(path.container);
+ if (!blocks || path.index < 0 || path.index >= int(blocks->size())) {
return nullptr;
}
- return (descriptor.slot == TextSlot::Caption) ? &block->caption : &block->text;
+ return &(*blocks)[path.index];
}
-const RichText *State::richText(const TextNodeDescriptor &descriptor) const {
- const auto block = blockFromPath(descriptor.blockPath);
- if (!block) {
+const Block *State::block(const BlockPath &path) const {
+ const auto blocks = blockContainer(path.container);
+ if (!blocks || path.index < 0 || path.index >= int(blocks->size())) {
return nullptr;
}
- return (descriptor.slot == TextSlot::Caption) ? &block->caption : &block->text;
+ return &(*blocks)[path.index];
+}
+
+ListItem *State::listItem(const BlockPath &blockPath, int itemIndex) {
+ const auto list = block(blockPath);
+ if (!list
+ || itemIndex < 0
+ || itemIndex >= int(list->listItems.size())) {
+ return nullptr;
+ }
+ return &list->listItems[itemIndex];
+}
+
+const ListItem *State::listItem(
+ const BlockPath &blockPath,
+ int itemIndex) const {
+ const auto list = block(blockPath);
+ if (!list
+ || itemIndex < 0
+ || itemIndex >= int(list->listItems.size())) {
+ return nullptr;
+ }
+ return &list->listItems[itemIndex];
+}
+
+TableCell *State::tableCell(
+ const BlockPath &blockPath,
+ int rowIndex,
+ int cellIndex) {
+ const auto table = block(blockPath);
+ if (!table
+ || rowIndex < 0
+ || rowIndex >= int(table->tableRows.size())) {
+ return nullptr;
+ }
+ auto &row = table->tableRows[rowIndex];
+ if (cellIndex < 0 || cellIndex >= int(row.cells.size())) {
+ return nullptr;
+ }
+ return &row.cells[cellIndex];
+}
+
+const TableCell *State::tableCell(
+ const BlockPath &blockPath,
+ int rowIndex,
+ int cellIndex) const {
+ const auto table = block(blockPath);
+ if (!table
+ || rowIndex < 0
+ || rowIndex >= int(table->tableRows.size())) {
+ return nullptr;
+ }
+ const auto &row = table->tableRows[rowIndex];
+ if (cellIndex < 0 || cellIndex >= int(row.cells.size())) {
+ return nullptr;
+ }
+ return &row.cells[cellIndex];
+}
+
+RichText *State::richText(const LeafPath &path) {
+ switch (path.kind) {
+ case LeafKind::BlockText:
+ if (const auto owner = block(path.block)) {
+ return &owner->text;
+ }
+ return nullptr;
+ case LeafKind::BlockCaption:
+ if (const auto owner = block(path.block)) {
+ return &owner->caption;
+ }
+ return nullptr;
+ case LeafKind::ListItemText:
+ if (const auto item = listItem(path.block, path.listItemIndex)) {
+ return &item->text;
+ }
+ return nullptr;
+ case LeafKind::TableCellText:
+ if (const auto cell = tableCell(
+ path.block,
+ path.tableRowIndex,
+ path.tableCellIndex)) {
+ return &cell->text;
+ }
+ return nullptr;
+ case LeafKind::MathFormula:
+ return nullptr;
+ }
+ return nullptr;
+}
+
+const RichText *State::richText(const LeafPath &path) const {
+ switch (path.kind) {
+ case LeafKind::BlockText:
+ if (const auto owner = block(path.block)) {
+ return &owner->text;
+ }
+ return nullptr;
+ case LeafKind::BlockCaption:
+ if (const auto owner = block(path.block)) {
+ return &owner->caption;
+ }
+ return nullptr;
+ case LeafKind::ListItemText:
+ if (const auto item = listItem(path.block, path.listItemIndex)) {
+ return &item->text;
+ }
+ return nullptr;
+ case LeafKind::TableCellText:
+ if (const auto cell = tableCell(
+ path.block,
+ path.tableRowIndex,
+ path.tableCellIndex)) {
+ return &cell->text;
+ }
+ return nullptr;
+ case LeafKind::MathFormula:
+ return nullptr;
+ }
+ return nullptr;
+}
+
+QString *State::rawText(const LeafPath &path) {
+ if (path.kind != LeafKind::MathFormula) {
+ return nullptr;
+ }
+ const auto owner = block(path.block);
+ return owner ? &owner->formula : nullptr;
+}
+
+const QString *State::rawText(const LeafPath &path) const {
+ if (path.kind != LeafKind::MathFormula) {
+ return nullptr;
+ }
+ const auto owner = block(path.block);
+ return owner ? &owner->formula : nullptr;
}
const TextNodeDescriptor *State::textNode(int ordinal) const {
@@ -149,12 +512,9 @@ const TextNodeDescriptor *State::textNode(int ordinal) const {
: nullptr;
}
-int State::textNodeOrdinal(
- const std::vector &blockPath,
- TextSlot slot) const {
+int State::textNodeOrdinal(const LeafPath &path) const {
for (auto i = 0, count = textNodeCount(); i != count; ++i) {
- const auto &descriptor = _textNodes[i];
- if (descriptor.slot == slot && descriptor.blockPath == blockPath) {
+ if (_textNodes[i].leaf == path) {
return i;
}
}
@@ -163,6 +523,7 @@ int State::textNodeOrdinal(
void State::rebuild() {
rebuildTextNodes();
+ ensureEditableNodes();
ensureActiveTextOrdinal();
_prepared = Markdown::TryPrepareNativeInstantView({
.richPage = _richPage,
@@ -173,34 +534,167 @@ void State::rebuild() {
void State::rebuildTextNodes() {
_textNodes.clear();
- _textNodes.reserve(_richPage->blocks.size() * 2);
- for (auto i = 0, count = int(_richPage->blocks.size()); i != count; ++i) {
- const auto path = std::vector{ i };
- const auto &block = _richPage->blocks[i];
+ _textNodes.reserve(_richPage->blocks.size() * 3);
+ rebuildTextNodes(_richPage->blocks, BlockContainerPath());
+}
+
+void State::rebuildTextNodes(
+ const std::vector &blocks,
+ const BlockContainerPath &container) {
+ for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
+ const auto path = BlockPath{
+ .container = container,
+ .index = i,
+ };
+ const auto &block = blocks[i];
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
- _textNodes.push_back({
- .blockPath = path,
- .slot = TextSlot::Text,
- });
+ appendBlockTextNode(path, LeafKind::BlockText);
break;
case BlockKind::Quote:
- _textNodes.push_back({
- .blockPath = path,
- .slot = TextSlot::Text,
- });
- _textNodes.push_back({
- .blockPath = path,
- .slot = TextSlot::Caption,
- });
+ appendBlockTextNode(
+ path,
+ LeafKind::BlockText,
+ FieldMode::Rich,
+ InsertionAnchor{
+ .container = BlockChildrenContainer(path),
+ .blockIndex = -1,
+ });
+ appendBlockTextNode(
+ path,
+ LeafKind::BlockCaption,
+ FieldMode::Rich,
+ InsertionAnchor{
+ .container = BlockChildrenContainer(path),
+ .blockIndex = -1,
+ });
+ rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
+ break;
+ case BlockKind::List:
+ for (auto j = 0, itemCount = int(block.listItems.size());
+ j != itemCount;
+ ++j) {
+ const auto &item = block.listItems[j];
+ if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
+ appendListItemTextNode(path, j);
+ }
+ if (!item.blocks.empty()) {
+ rebuildTextNodes(
+ item.blocks,
+ ListItemChildrenContainer(path, j));
+ }
+ }
+ break;
+ case BlockKind::Photo:
+ case BlockKind::Video:
+ case BlockKind::Audio:
+ case BlockKind::Map:
+ appendBlockTextNode(path, LeafKind::BlockCaption);
+ break;
+ case BlockKind::Math:
+ appendBlockTextNode(path, LeafKind::MathFormula, FieldMode::Raw);
+ break;
+ case BlockKind::Table:
+ if (!block.text.text.text.isEmpty()) {
+ appendBlockTextNode(path, LeafKind::BlockText);
+ }
+ for (auto j = 0, rowCount = int(block.tableRows.size());
+ j != rowCount;
+ ++j) {
+ const auto &row = block.tableRows[j];
+ for (auto k = 0, cellCount = int(row.cells.size());
+ k != cellCount;
+ ++k) {
+ appendTableCellTextNode(path, j, k);
+ }
+ }
+ break;
+ case BlockKind::Details:
+ appendBlockTextNode(
+ path,
+ LeafKind::BlockText,
+ FieldMode::Rich,
+ InsertionAnchor{
+ .container = BlockChildrenContainer(path),
+ .blockIndex = -1,
+ });
+ rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
break;
default:
+ rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
break;
}
}
}
+void State::appendBlockTextNode(
+ const BlockPath &path,
+ LeafKind kind,
+ FieldMode mode,
+ std::optional insertionAnchor) {
+ _textNodes.push_back({
+ .leaf = {
+ .kind = kind,
+ .block = path,
+ },
+ .insertionAnchor = insertionAnchor.value_or(InsertionAnchor{
+ .container = path.container,
+ .blockIndex = path.index,
+ }),
+ .removalTarget = {
+ .kind = RemovalKind::Block,
+ .block = path,
+ },
+ .mode = mode,
+ });
+}
+
+void State::appendListItemTextNode(const BlockPath &path, int itemIndex) {
+ _textNodes.push_back({
+ .leaf = {
+ .kind = LeafKind::ListItemText,
+ .block = path,
+ .listItemIndex = itemIndex,
+ },
+ .insertionAnchor = {
+ .container = ListItemChildrenContainer(path, itemIndex),
+ .blockIndex = -1,
+ },
+ .removalTarget = {
+ .kind = RemovalKind::ListItem,
+ .block = path,
+ .listItemIndex = itemIndex,
+ },
+ .mode = FieldMode::Rich,
+ });
+}
+
+void State::appendTableCellTextNode(
+ const BlockPath &path,
+ int rowIndex,
+ int cellIndex) {
+ _textNodes.push_back({
+ .leaf = {
+ .kind = LeafKind::TableCellText,
+ .block = path,
+ .tableRowIndex = rowIndex,
+ .tableCellIndex = cellIndex,
+ },
+ .insertionAnchor = {
+ .container = path.container,
+ .blockIndex = path.index,
+ },
+ .removalTarget = {
+ .kind = RemovalKind::TableCell,
+ .block = path,
+ .tableRowIndex = rowIndex,
+ .tableCellIndex = cellIndex,
+ },
+ .mode = FieldMode::Rich,
+ });
+}
+
void State::ensureActiveTextOrdinal() {
if (_textNodes.empty()) {
_activeTextOrdinal = -1;
@@ -209,54 +703,365 @@ void State::ensureActiveTextOrdinal() {
}
}
-void State::insertBlockAfterActive(Block block, TextSlot slotToFocus) {
- auto parentPath = std::vector();
- auto insertionIndex = int(_richPage->blocks.size());
- if (const auto descriptor = textNode(_activeTextOrdinal)) {
- parentPath = descriptor->blockPath;
- if (!parentPath.empty()) {
- insertionIndex = parentPath.back() + 1;
- parentPath.pop_back();
+void State::ensureEditableNodes() {
+ if (!_textNodes.empty()) {
+ return;
+ }
+ _richPage->blocks.push_back(MakeParagraphBlock());
+ rebuildTextNodes();
+}
+
+void State::focusInsertedBlock(const BlockPath &path) {
+ for (auto i = 0, count = textNodeCount(); i != count; ++i) {
+ if (descriptorBelongsToBlock(_textNodes[i], path)) {
+ if (setActiveTextByOrdinal(i)) {
+ return;
+ }
}
}
- auto *blocks = blockContainerFromPath(parentPath);
- if (!blocks) {
- parentPath.clear();
- blocks = &_richPage->blocks;
- insertionIndex = int(blocks->size());
- } else {
- insertionIndex = std::clamp(insertionIndex, 0, int(blocks->size()));
+ ensureActiveTextOrdinal();
+}
+
+std::optional State::adjacentEditableOrdinal(bool forward) const {
+ if (_activeTextOrdinal < 0) {
+ return std::nullopt;
}
- blocks->insert(blocks->begin() + insertionIndex, std::move(block));
- auto insertedPath = parentPath;
- insertedPath.push_back(insertionIndex);
- rebuild();
- if (!setActiveTextByOrdinal(textNodeOrdinal(insertedPath, slotToFocus))) {
- ensureActiveTextOrdinal();
+ const auto ordinal = _activeTextOrdinal + (forward ? 1 : -1);
+ return (ordinal >= 0 && ordinal < textNodeCount())
+ ? std::make_optional(ordinal)
+ : std::nullopt;
+}
+
+bool State::descriptorBelongsToBlock(
+ const TextNodeDescriptor &descriptor,
+ const BlockPath &path) const {
+ const auto &owner = descriptor.leaf.block;
+ if (owner == path) {
+ return true;
+ }
+ if (!ContainerHasPrefix(owner.container, path.container)) {
+ return false;
+ }
+ if (owner.container.steps.size() <= path.container.steps.size()) {
+ return false;
+ }
+ const auto &step = owner.container.steps[path.container.steps.size()];
+ return step.blockIndex == path.index
+ && (step.kind == BlockContainerKind::BlockChildren
+ || step.kind == BlockContainerKind::ListItemChildren);
+}
+
+bool State::removalTargetIsEmpty(const RemovalTarget &target) const {
+ switch (target.kind) {
+ case RemovalKind::Block:
+ if (const auto owner = block(target.block)) {
+ return BlockIsEmpty(*owner);
+ }
+ return false;
+ case RemovalKind::ListItem:
+ if (const auto item = listItem(target.block, target.listItemIndex)) {
+ return ListItemIsEmpty(*item);
+ }
+ return false;
+ case RemovalKind::TableCell:
+ if (const auto cell = tableCell(
+ target.block,
+ target.tableRowIndex,
+ target.tableCellIndex)) {
+ return RichTextIsEmpty(cell->text);
+ }
+ return false;
+ }
+ return false;
+}
+
+bool State::removeTarget(const RemovalTarget &target) {
+ switch (target.kind) {
+ case RemovalKind::Block: {
+ const auto blocks = blockContainer(target.block.container);
+ if (!blocks
+ || target.block.index < 0
+ || target.block.index >= int(blocks->size())) {
+ return false;
+ }
+ blocks->erase(blocks->begin() + target.block.index);
+ return true;
+ }
+ case RemovalKind::ListItem: {
+ const auto owner = block(target.block);
+ if (!owner
+ || target.listItemIndex < 0
+ || target.listItemIndex >= int(owner->listItems.size())) {
+ return false;
+ }
+ owner->listItems.erase(owner->listItems.begin() + target.listItemIndex);
+ return true;
+ }
+ case RemovalKind::TableCell: {
+ const auto cell = tableCell(
+ target.block,
+ target.tableRowIndex,
+ target.tableCellIndex);
+ if (!cell) {
+ return false;
+ }
+ cell->text = RichText();
+ return true;
+ }
+ }
+ return false;
+}
+
+bool State::anchorIdExists(const QString &id) const {
+ return anchorIdExists(_richPage->blocks, id);
+}
+
+bool State::anchorIdExists(
+ const std::vector &blocks,
+ const QString &id) const {
+ for (const auto &block : blocks) {
+ if (block.anchorId == id
+ || block.text.anchorId == id
+ || block.caption.anchorId == id) {
+ return true;
+ }
+ if (anchorIdExists(block.blocks, id)) {
+ return true;
+ }
+ for (const auto &item : block.listItems) {
+ if (item.anchorId == id || item.text.anchorId == id) {
+ return true;
+ }
+ if (anchorIdExists(item.blocks, id)) {
+ return true;
+ }
+ }
+ for (const auto &row : block.tableRows) {
+ for (const auto &cell : row.cells) {
+ if (cell.text.anchorId == id) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+QString State::nextAnchorId() const {
+ for (auto i = 1;; ++i) {
+ auto result = u"anchor-%1"_q.arg(i);
+ if (!anchorIdExists(result)) {
+ return result;
+ }
}
}
-Block State::makeParagraphBlock() {
+Block State::makeBlock(InsertAction action) const {
+ switch (action.type) {
+ case InsertBlockType::Heading:
+ return MakeHeadingBlock(action.headingLevel);
+ case InsertBlockType::Blockquote:
+ return MakeQuoteBlock(false);
+ case InsertBlockType::Math:
+ return MakeMathBlock();
+ case InsertBlockType::Footer:
+ return MakeParagraphBlock();
+ case InsertBlockType::Divider:
+ return MakeDividerBlock();
+ case InsertBlockType::Anchor:
+ return MakeAnchorBlock(nextAnchorId());
+ case InsertBlockType::OrderedList:
+ return MakeListBlock(ListKind::Ordered);
+ case InsertBlockType::BulletList:
+ return MakeListBlock(ListKind::Bullet);
+ case InsertBlockType::TaskList:
+ return MakeListBlock(ListKind::Bullet, TaskState::Unchecked);
+ case InsertBlockType::Pullquote:
+ return MakeQuoteBlock(true);
+ case InsertBlockType::Photo:
+ return MakeMediaBlock(BlockKind::Photo);
+ case InsertBlockType::Video:
+ return MakeMediaBlock(BlockKind::Video);
+ case InsertBlockType::Audio:
+ return MakeMediaBlock(BlockKind::Audio);
+ case InsertBlockType::Details:
+ return MakeDetailsBlock();
+ case InsertBlockType::Table:
+ return MakeTableBlock();
+ case InsertBlockType::Map:
+ return MakeMapBlock(action.latitude, action.longitude);
+ }
+ return MakeParagraphBlock();
+}
+
+TextWithEntities State::MakeText(QString text) {
+ auto result = TextWithEntities();
+ result.text = std::move(text);
+ return result;
+}
+
+Block State::MakeParagraphBlock() {
auto block = Block();
block.kind = BlockKind::Paragraph;
+ block.text.text = MakeText(u"Text"_q);
return block;
}
-Block State::makeHeading1Block() {
+Block State::MakeHeadingBlock(int level) {
auto block = Block();
block.kind = BlockKind::Heading;
- block.headingLevel = 1;
+ block.headingLevel = std::clamp(level, 1, 6);
+ block.text.text = MakeText(u"Header"_q);
return block;
}
-Block State::makeBlockquoteBlock() {
+Block State::MakeQuoteBlock(bool pullquote) {
auto block = Block();
block.kind = BlockKind::Quote;
- block.pullquote = false;
+ block.pullquote = pullquote;
+ block.text.text = MakeText(u"Text"_q);
+ block.caption.text = MakeText(u"Caption"_q);
return block;
}
-bool State::stripWrapperEntityInEditMode(EntityType type) {
+Block State::MakeMathBlock() {
+ auto block = Block();
+ block.kind = BlockKind::Math;
+ block.formula = u"x^2 + y^2"_q;
+ return block;
+}
+
+Block State::MakeDividerBlock() {
+ auto block = Block();
+ block.kind = BlockKind::Divider;
+ return block;
+}
+
+Block State::MakeAnchorBlock(QString anchorId) {
+ auto block = Block();
+ block.kind = BlockKind::Anchor;
+ block.anchorId = std::move(anchorId);
+ return block;
+}
+
+Block State::MakeListBlock(ListKind kind, TaskState taskState) {
+ auto block = Block();
+ block.kind = BlockKind::List;
+ block.listKind = kind;
+ block.listItems.reserve(3);
+ for (auto i = 0; i != 3; ++i) {
+ auto item = ListItem();
+ item.taskState = taskState;
+ item.text.text = MakeText(u"Text"_q);
+ block.listItems.push_back(std::move(item));
+ }
+ return block;
+}
+
+Block State::MakeDetailsBlock() {
+ auto block = Block();
+ block.kind = BlockKind::Details;
+ block.open = true;
+ block.text.text = MakeText(u"Header"_q);
+ block.blocks.push_back(MakeParagraphBlock());
+ return block;
+}
+
+Block State::MakeTableBlock() {
+ auto block = Block();
+ block.kind = BlockKind::Table;
+ block.tableRows.reserve(3);
+ for (auto rowIndex = 0; rowIndex != 3; ++rowIndex) {
+ auto row = RichPage::TableRow();
+ row.cells.reserve(3);
+ for (auto cellIndex = 0; cellIndex != 3; ++cellIndex) {
+ auto cell = TableCell();
+ cell.header = (rowIndex == 0);
+ cell.text.text = MakeText(u"Cell"_q);
+ row.cells.push_back(std::move(cell));
+ }
+ block.tableRows.push_back(std::move(row));
+ }
+ return block;
+}
+
+Block State::MakeMediaBlock(BlockKind kind) {
+ auto block = Block();
+ block.kind = kind;
+ block.caption.text = MakeText(u"Caption"_q);
+ return block;
+}
+
+Block State::MakeMapBlock(double latitude, double longitude) {
+ auto block = Block();
+ block.kind = BlockKind::Map;
+ block.latitude = latitude;
+ block.longitude = longitude;
+ block.caption.text = MakeText(u"Caption"_q);
+ return block;
+}
+
+bool State::RichTextIsEmpty(const RichText &text) {
+ return StringIsEmpty(text.text.text);
+}
+
+bool State::ListItemIsEmpty(const ListItem &item) {
+ if (!RichTextIsEmpty(item.text) || !item.anchorId.isEmpty()) {
+ return false;
+ }
+ for (const auto &block : item.blocks) {
+ if (!BlockIsEmpty(block)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool State::BlockIsEmpty(const Block &block) {
+ if (!RichTextIsEmpty(block.text)
+ || !RichTextIsEmpty(block.caption)
+ || !StringIsEmpty(block.formula)
+ || !block.anchorId.isEmpty()
+ || !block.url.isEmpty()
+ || !block.html.isEmpty()
+ || !block.author.isEmpty()
+ || !block.username.isEmpty()
+ || !block.channelTitle.isEmpty()
+ || !block.audioTitle.isEmpty()
+ || !block.audioPerformer.isEmpty()
+ || !block.audioFileName.isEmpty()
+ || block.photo
+ || block.document
+ || block.peer
+ || block.photoId
+ || block.documentId
+ || block.channelId
+ || block.accessHash
+ || block.latitude != 0.
+ || block.longitude != 0.) {
+ return false;
+ }
+ for (const auto &child : block.blocks) {
+ if (!BlockIsEmpty(child)) {
+ return false;
+ }
+ }
+ for (const auto &item : block.listItems) {
+ if (!ListItemIsEmpty(item)) {
+ return false;
+ }
+ }
+ for (const auto &row : block.tableRows) {
+ for (const auto &cell : row.cells) {
+ if (!RichTextIsEmpty(cell.text)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool State::StripWrapperEntityInEditMode(EntityType type) {
switch (type) {
case EntityType::Url:
case EntityType::Email:
@@ -272,11 +1077,11 @@ bool State::stripWrapperEntityInEditMode(EntityType type) {
}
}
-TextWithEntities State::stripEditModeWrapperEntities(TextWithEntities text) {
+TextWithEntities State::StripEditModeWrapperEntities(TextWithEntities text) {
auto filtered = EntitiesInText();
filtered.reserve(text.entities.size());
for (const auto &entity : text.entities) {
- if (!stripWrapperEntityInEditMode(entity.type())) {
+ if (!StripWrapperEntityInEditMode(entity.type())) {
filtered.push_back(entity);
}
}
diff --git a/Telegram/SourceFiles/iv/iv_editor_state.h b/Telegram/SourceFiles/iv/iv_editor_state.h
index d68904bf41..4b0b765e77 100644
--- a/Telegram/SourceFiles/iv/iv_editor_state.h
+++ b/Telegram/SourceFiles/iv/iv_editor_state.h
@@ -11,20 +11,146 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/markdown/iv_markdown_prepare.h"
#include
+#include
#include
namespace Iv::Editor {
class State final {
public:
- enum class TextSlot : uchar {
- Text,
- Caption,
+ enum class FieldMode : uchar {
+ Rich,
+ Raw,
+ };
+
+ enum class InsertBlockType : uchar {
+ Heading,
+ Blockquote,
+ Math,
+ Footer,
+ Divider,
+ Anchor,
+ OrderedList,
+ BulletList,
+ TaskList,
+ Pullquote,
+ Photo,
+ Video,
+ Audio,
+ Details,
+ Table,
+ Map,
+ };
+
+ struct InsertAction {
+ InsertBlockType type;
+ int headingLevel = 1;
+ double latitude = 0.;
+ double longitude = 0.;
+ };
+
+ enum class BlockContainerKind : uchar {
+ Root,
+ BlockChildren,
+ ListItemChildren,
+ };
+
+ struct BlockContainerStep {
+ BlockContainerKind kind = BlockContainerKind::BlockChildren;
+ int blockIndex = -1;
+ int listItemIndex = -1;
+
+ friend inline bool operator==(
+ const BlockContainerStep &a,
+ const BlockContainerStep &b) {
+ return (a.kind == b.kind)
+ && (a.blockIndex == b.blockIndex)
+ && (a.listItemIndex == b.listItemIndex);
+ }
+ };
+
+ struct BlockContainerPath {
+ std::vector steps;
+
+ friend inline bool operator==(
+ const BlockContainerPath &a,
+ const BlockContainerPath &b) {
+ return (a.steps == b.steps);
+ }
+ };
+
+ struct BlockPath {
+ BlockContainerPath container;
+ int index = -1;
+
+ friend inline bool operator==(
+ const BlockPath &a,
+ const BlockPath &b) {
+ return (a.container == b.container)
+ && (a.index == b.index);
+ }
+ };
+
+ enum class LeafKind : uchar {
+ BlockText,
+ BlockCaption,
+ ListItemText,
+ TableCellText,
+ MathFormula,
+ };
+
+ struct LeafPath {
+ LeafKind kind = LeafKind::BlockText;
+ BlockPath block;
+ int listItemIndex = -1;
+ int tableRowIndex = -1;
+ int tableCellIndex = -1;
+
+ friend inline bool operator==(
+ const LeafPath &a,
+ const LeafPath &b) {
+ return (a.kind == b.kind)
+ && (a.block == b.block)
+ && (a.listItemIndex == b.listItemIndex)
+ && (a.tableRowIndex == b.tableRowIndex)
+ && (a.tableCellIndex == b.tableCellIndex);
+ }
+ };
+
+ struct InsertionAnchor {
+ BlockContainerPath container;
+ int blockIndex = -1;
+ };
+
+ enum class RemovalKind : uchar {
+ Block,
+ ListItem,
+ TableCell,
+ };
+
+ struct RemovalTarget {
+ RemovalKind kind = RemovalKind::Block;
+ BlockPath block;
+ int listItemIndex = -1;
+ int tableRowIndex = -1;
+ int tableCellIndex = -1;
+
+ friend inline bool operator==(
+ const RemovalTarget &a,
+ const RemovalTarget &b) {
+ return (a.kind == b.kind)
+ && (a.block == b.block)
+ && (a.listItemIndex == b.listItemIndex)
+ && (a.tableRowIndex == b.tableRowIndex)
+ && (a.tableCellIndex == b.tableCellIndex);
+ }
};
struct TextNodeDescriptor {
- std::vector blockPath;
- TextSlot slot = TextSlot::Text;
+ LeafPath leaf;
+ InsertionAnchor insertionAnchor;
+ RemovalTarget removalTarget;
+ FieldMode mode = FieldMode::Rich;
};
State();
@@ -37,41 +163,113 @@ public:
[[nodiscard]] bool setActiveTextByOrdinal(int ordinal);
[[nodiscard]] TextWithEntities activeText() const;
void applyActiveText(TextWithEntities text);
+ [[nodiscard]] FieldMode activeFieldMode() const;
+ [[nodiscard]] QString activeRawText() const;
+ void applyActiveRawText(QString text);
+ [[nodiscard]] int activeTextLength() const;
+ [[nodiscard]] std::optional previousEditableOrdinal() const;
+ [[nodiscard]] std::optional nextEditableOrdinal() const;
+ [[nodiscard]] bool isActiveTopLevelParagraph() const;
+ [[nodiscard]] bool activeOwnerIsEmpty() const;
+ [[nodiscard]] std::optional removeActiveOwnerAndSelectAdjacent(
+ bool forward);
[[nodiscard]] int ensureTrailingParagraphActive();
void insertHeading1AfterActive();
void insertBlockquoteAfterActive();
+ void insertBlockAfterActive(InsertAction action);
private:
- [[nodiscard]] RichPage::Block *blockFromPath(const std::vector &path);
- [[nodiscard]] const RichPage::Block *blockFromPath(
- const std::vector &path) const;
- [[nodiscard]] std::vector *blockContainerFromPath(
- const std::vector &path);
- [[nodiscard]] RichPage::RichText *richText(
- const TextNodeDescriptor &descriptor);
+ [[nodiscard]] std::vector *blockContainer(
+ const BlockContainerPath &path);
+ [[nodiscard]] const std::vector *blockContainer(
+ const BlockContainerPath &path) const;
+ [[nodiscard]] RichPage::Block *block(const BlockPath &path);
+ [[nodiscard]] const RichPage::Block *block(const BlockPath &path) const;
+ [[nodiscard]] RichPage::ListItem *listItem(
+ const BlockPath &blockPath,
+ int itemIndex);
+ [[nodiscard]] const RichPage::ListItem *listItem(
+ const BlockPath &blockPath,
+ int itemIndex) const;
+ [[nodiscard]] RichPage::TableCell *tableCell(
+ const BlockPath &blockPath,
+ int rowIndex,
+ int cellIndex);
+ [[nodiscard]] const RichPage::TableCell *tableCell(
+ const BlockPath &blockPath,
+ int rowIndex,
+ int cellIndex) const;
+ [[nodiscard]] RichPage::RichText *richText(const LeafPath &path);
[[nodiscard]] const RichPage::RichText *richText(
- const TextNodeDescriptor &descriptor) const;
+ const LeafPath &path) const;
+ [[nodiscard]] QString *rawText(const LeafPath &path);
+ [[nodiscard]] const QString *rawText(const LeafPath &path) const;
[[nodiscard]] const TextNodeDescriptor *textNode(int ordinal) const;
- [[nodiscard]] int textNodeOrdinal(
- const std::vector &blockPath,
- TextSlot slot) const;
+ [[nodiscard]] int textNodeOrdinal(const LeafPath &path) const;
void rebuild();
void rebuildTextNodes();
+ void rebuildTextNodes(
+ const std::vector &blocks,
+ const BlockContainerPath &container);
+ void appendBlockTextNode(
+ const BlockPath &path,
+ LeafKind kind,
+ FieldMode mode = FieldMode::Rich,
+ std::optional insertionAnchor = std::nullopt);
+ void appendListItemTextNode(const BlockPath &path, int itemIndex);
+ void appendTableCellTextNode(
+ const BlockPath &path,
+ int rowIndex,
+ int cellIndex);
void ensureActiveTextOrdinal();
- void insertBlockAfterActive(RichPage::Block block, TextSlot slotToFocus);
+ void ensureEditableNodes();
+ void focusInsertedBlock(const BlockPath &path);
+ [[nodiscard]] std::optional adjacentEditableOrdinal(
+ bool forward) const;
+ [[nodiscard]] bool descriptorBelongsToBlock(
+ const TextNodeDescriptor &descriptor,
+ const BlockPath &path) const;
+ [[nodiscard]] bool removalTargetIsEmpty(
+ const RemovalTarget &target) const;
+ [[nodiscard]] bool removeTarget(const RemovalTarget &target);
+ [[nodiscard]] bool anchorIdExists(const QString &id) const;
+ [[nodiscard]] bool anchorIdExists(
+ const std::vector &blocks,
+ const QString &id) const;
+ [[nodiscard]] QString nextAnchorId() const;
+ [[nodiscard]] RichPage::Block makeBlock(InsertAction action) const;
- [[nodiscard]] static RichPage::Block makeParagraphBlock();
- [[nodiscard]] static RichPage::Block makeHeading1Block();
- [[nodiscard]] static RichPage::Block makeBlockquoteBlock();
- [[nodiscard]] static bool stripWrapperEntityInEditMode(EntityType type);
- [[nodiscard]] static TextWithEntities stripEditModeWrapperEntities(
+ [[nodiscard]] static TextWithEntities MakeText(QString text);
+ [[nodiscard]] static RichPage::Block MakeParagraphBlock();
+ [[nodiscard]] static RichPage::Block MakeHeadingBlock(int level);
+ [[nodiscard]] static RichPage::Block MakeQuoteBlock(bool pullquote);
+ [[nodiscard]] static RichPage::Block MakeMathBlock();
+ [[nodiscard]] static RichPage::Block MakeDividerBlock();
+ [[nodiscard]] static RichPage::Block MakeAnchorBlock(QString anchorId);
+ [[nodiscard]] static RichPage::Block MakeListBlock(
+ RichPage::ListKind kind,
+ RichPage::TaskState taskState = RichPage::TaskState::None);
+ [[nodiscard]] static RichPage::Block MakeDetailsBlock();
+ [[nodiscard]] static RichPage::Block MakeTableBlock();
+ [[nodiscard]] static RichPage::Block MakeMediaBlock(
+ RichPage::BlockKind kind);
+ [[nodiscard]] static RichPage::Block MakeMapBlock(
+ double latitude,
+ double longitude);
+ [[nodiscard]] static bool RichTextIsEmpty(const RichPage::RichText &text);
+ [[nodiscard]] static bool ListItemIsEmpty(
+ const RichPage::ListItem &item);
+ [[nodiscard]] static bool BlockIsEmpty(const RichPage::Block &block);
+ [[nodiscard]] static bool StripWrapperEntityInEditMode(EntityType type);
+ [[nodiscard]] static TextWithEntities StripEditModeWrapperEntities(
TextWithEntities text);
std::shared_ptr _richPage;
Markdown::MarkdownArticleContent _prepared;
std::vector _textNodes;
int _activeTextOrdinal = -1;
+
};
} // namespace Iv::Editor
diff --git a/Telegram/SourceFiles/iv/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/iv_editor_widget.cpp
index ce9f45333a..a89b0771cd 100644
--- a/Telegram/SourceFiles/iv/iv_editor_widget.cpp
+++ b/Telegram/SourceFiles/iv/iv_editor_widget.cpp
@@ -23,8 +23,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_iv.h"
#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -161,8 +163,8 @@ void Widget::activateInitialNode() {
? _activeOrdinal
: _state->activeTextOrdinal();
if (ordinal < 0) {
- const auto first = _article->firstTextSegmentIndex();
- const auto fallback = textOrdinalForSegment(first);
+ const auto first = _article->firstEditableSegmentIndex();
+ const auto fallback = editableOrdinalForSegment(first);
if (fallback < 0) {
return;
}
@@ -173,7 +175,7 @@ void Widget::activateInitialNode() {
}
void Widget::activateSegment(int segmentIndex, int cursorOffset) {
- const auto ordinal = textOrdinalForSegment(segmentIndex);
+ const auto ordinal = editableOrdinalForSegment(segmentIndex);
if (ordinal < 0) {
return;
}
@@ -184,18 +186,22 @@ void Widget::commitInlineField() {
applyFieldTextToState();
}
-void Widget::acceptInlineField() {
+void Widget::hideInlineFieldAndRefresh() {
if (_field->isHidden()) {
return;
}
commitInlineField();
_pendingOrdinal = -1;
_pendingCursorOffset = 0;
- _field->hide();
+ hideInlineField();
_article->clearTextLeafHeightOverride();
refreshPreparedContent();
}
+void Widget::acceptInlineField() {
+ hideInlineFieldAndRefresh();
+}
+
void Widget::refreshPreparedContent() {
setDocument(_state->prepared());
const auto width = std::max(
@@ -212,18 +218,42 @@ void Widget::syncInlineFieldGeometry() {
syncInlineFieldGeometry(widthNoMargins());
}
-void Widget::insertHeading1() {
+void Widget::insertBlock(State::InsertAction action) {
commitInlineField();
- _state->insertHeading1AfterActive();
+ _state->insertBlockAfterActive(action);
refreshPreparedContent();
activateTextOrdinal(_state->activeTextOrdinal(), 0);
}
+void Widget::insertMedia(State::InsertBlockType type) {
+ switch (type) {
+ case State::InsertBlockType::Photo:
+ case State::InsertBlockType::Video:
+ case State::InsertBlockType::Audio:
+ insertBlock({ .type = type });
+ break;
+ default:
+ break;
+ }
+}
+
+void Widget::insertMap(double latitude, double longitude) {
+ insertBlock({
+ .type = State::InsertBlockType::Map,
+ .latitude = latitude,
+ .longitude = longitude,
+ });
+}
+
+void Widget::insertHeading1() {
+ insertBlock({
+ .type = State::InsertBlockType::Heading,
+ .headingLevel = 1,
+ });
+}
+
void Widget::insertBlockquote() {
- commitInlineField();
- _state->insertBlockquoteAfterActive();
- refreshPreparedContent();
- activateTextOrdinal(_state->activeTextOrdinal(), 0);
+ insertBlock({ .type = State::InsertBlockType::Blockquote });
}
int Widget::resizeGetHeight(int newWidth) {
@@ -245,9 +275,19 @@ int Widget::resizeGetHeight(int newWidth) {
st::ivEditorMinHeight);
}
+bool Widget::eventFilter(QObject *object, QEvent *event) {
+ if (_field
+ && object == _field->rawTextEdit().get()
+ && event->type() == QEvent::KeyPress
+ && handleFieldKey(static_cast(event))) {
+ return true;
+ }
+ return Ui::RpWidget::eventFilter(object, event);
+}
+
void Widget::focusInEvent(QFocusEvent *e) {
Ui::RpWidget::focusInEvent(e);
- if (!_field->isHidden()) {
+ if (!_settingField && !_field->isHidden()) {
_field->setFocusFast();
}
}
@@ -321,6 +361,11 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
&& hit.direct
&& _article->segmentIsText(hit.segmentIndex);
};
+ const auto directEditableHit = [&] {
+ return hit.valid()
+ && hit.direct
+ && _article->segmentIsEditable(hit.segmentIndex);
+ };
const auto commitVisibleInlineField = [&] {
if (_field->isHidden()) {
return;
@@ -328,7 +373,7 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
commitInlineField();
_pendingOrdinal = -1;
_pendingCursorOffset = 0;
- _field->hide();
+ hideInlineField();
_article->clearTextLeafHeightOverride();
refreshPreparedContent();
};
@@ -346,7 +391,7 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
&& (selection.from.segment == selection.to.segment)
&& _article->segmentIsText(selection.from.segment);
const auto selectionOrdinal = sameSegmentSelection
- ? textOrdinalForSegment(selection.from.segment)
+ ? editableOrdinalForSegment(selection.from.segment)
: -1;
if (selectionOrdinal >= 0) {
const auto selectionFrom = selection.from.offset;
@@ -360,7 +405,8 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
e->accept();
return;
} else if (selection.empty() && directTextHit()) {
- const auto targetOrdinal = textOrdinalForSegment(hit.segmentIndex);
+ const auto targetOrdinal = editableOrdinalForSegment(
+ hit.segmentIndex);
const auto targetOffset = _article->selectionOffsetFromHit(
hit,
TextSelectType::Letters);
@@ -381,8 +427,8 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
e->accept();
return;
}
- if (directTextHit()) {
- const auto targetOrdinal = textOrdinalForSegment(hit.segmentIndex);
+ if (directEditableHit()) {
+ const auto targetOrdinal = editableOrdinalForSegment(hit.segmentIndex);
const auto offset = _article->selectionOffsetFromHit(
hit,
TextSelectType::Letters);
@@ -433,7 +479,7 @@ void Widget::setDocument(const Markdown::MarkdownArticleContent &prepared) {
Markdown::MarkdownArticleTextLeafStyle Widget::inlineFieldStyleForSegment(
int segmentIndex) const {
return _article
- ? _article->textLeafStyleForSegment(segmentIndex)
+ ? _article->editableStyleForSegment(segmentIndex)
: Markdown::MarkdownArticleTextLeafStyle();
}
@@ -453,6 +499,9 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
auto fieldStyle = std::make_shared(
st::ivEditorInputField);
fieldStyle->style = *data.textStyle;
+ fieldStyle->style.font = data.italic
+ ? data.textStyle->font->italic()
+ : data.textStyle->font;
fieldStyle->style.lineHeight = data.lineHeight;
fieldStyle->textFg = data.textFg;
fieldStyle->textAlign = data.align;
@@ -477,6 +526,7 @@ Widget::InlineFieldStyleData Widget::normalizedInlineFieldStyle(
.lineHeight = lineHeight,
.textFg = valid ? leafStyle.textColor : st::messageMarkdown.textColor,
.align = valid ? leafStyle.align : style::al_left,
+ .italic = valid ? leafStyle.italic : false,
};
}
@@ -486,46 +536,65 @@ Widget::InlineFieldStyleKey Widget::inlineFieldStyleKey(
? data.textStyle
: &st::messageMarkdown.body;
return {
- .font = textStyle->font,
+ .font = data.italic
+ ? textStyle->font->italic()
+ : textStyle->font,
.lineHeight = data.lineHeight,
.textFg = data.textFg,
.align = data.align,
};
}
-void Widget::ensureInlineFieldStyleForSegment(int segmentIndex) {
+void Widget::ensureInlineFieldForSegment(int segmentIndex) {
const auto data = normalizedInlineFieldStyle(
inlineFieldStyleForSegment(segmentIndex));
const auto key = inlineFieldStyleKey(data);
- if (_activeFieldStyleKey && *_activeFieldStyleKey == key) {
+ const auto mode = _state->activeFieldMode();
+ if (_activeFieldStyleKey
+ && *_activeFieldStyleKey == key
+ && _fieldMode == mode) {
return;
}
const auto &cached = inlineFieldStyleFor(data);
_activeFieldStyleKey = key;
+ _fieldMode = mode;
recreateInlineField(*cached.style);
}
void Widget::setupInlineField() {
- const auto allowPremiumEmoji = [peer = _peer](
- not_null emoji) {
- return Data::AllowEmojiWithoutPremium(peer, emoji);
- };
- InitMessageFieldHandlers({
- .session = &_controller->session(),
- .show = _controller->uiShow(),
- .field = _field.get(),
- .customEmojiPaused = [=] {
- return _controller->isGifPausedAtLeastFor(
- Window::GifPauseReason::Layer);
- },
- .allowPremiumEmoji = allowPremiumEmoji,
- .fieldStyle = &_field->st(),
- });
- _field->setMimeDataHook(WrappedMessageFieldMimeHook(
- Ui::InputField::MimeDataHook(),
- _field.get()));
+ if (_fieldMode == State::FieldMode::Rich) {
+ const auto allowPremiumEmoji = [peer = _peer](
+ not_null emoji) {
+ return Data::AllowEmojiWithoutPremium(peer, emoji);
+ };
+ InitMessageFieldHandlers({
+ .session = &_controller->session(),
+ .show = _controller->uiShow(),
+ .field = _field.get(),
+ .customEmojiPaused = [=] {
+ return _controller->isGifPausedAtLeastFor(
+ Window::GifPauseReason::Layer);
+ },
+ .allowPremiumEmoji = allowPremiumEmoji,
+ .fieldStyle = &_field->st(),
+ });
+ _field->setMimeDataHook(WrappedMessageFieldMimeHook(
+ Ui::InputField::MimeDataHook(),
+ _field.get()));
+ } else {
+ _field->setInstantReplacesEnabled(
+ rpl::single(false),
+ rpl::single(false));
+ _field->setMarkdownReplacesEnabled(
+ rpl::single(Ui::MarkdownEnabledState{
+ Ui::MarkdownDisabled()
+ }));
+ }
+ _field->setDocumentMargin(0.);
+ _field->setAdditionalMargins({});
_field->setSubmitSettings(Ui::InputField::SubmitSettings::None);
_field->setMaxHeight(std::numeric_limits::max());
+ _field->rawTextEdit()->installEventFilter(this);
_field->heightChanges(
) | rpl::on_next([=] {
@@ -539,7 +608,7 @@ void Widget::setupInlineField() {
}
}, _field->lifetime());
- _field->hide();
+ hideInlineField();
}
void Widget::recreateInlineField(const style::InputField &st) {
@@ -592,25 +661,32 @@ void Widget::activateTextOrdinal(
_pendingOrdinal = -1;
_pendingCursorOffset = 0;
- const auto segmentIndex = segmentIndexForTextOrdinal(ordinal);
+ const auto segmentIndex = segmentIndexForEditableOrdinal(ordinal);
if (segmentIndex < 0) {
_activeSegmentIndex = -1;
_pendingOrdinal = ordinal;
_pendingCursorOffset = selectionTo;
- _field->hide();
+ hideInlineField();
return;
}
_activeSegmentIndex = segmentIndex;
- ensureInlineFieldStyleForSegment(segmentIndex);
- const auto activeText = _state->activeText();
+ ensureInlineFieldForSegment(segmentIndex);
_settingField = true;
- _field->setTextWithTags(
- {
- activeText.text,
- TextUtilities::ConvertEntitiesToTextTags(activeText.entities),
- },
- Ui::InputField::HistoryAction::Clear);
+ if (_state->activeFieldMode() == State::FieldMode::Raw) {
+ _field->setTextWithTags(
+ { _state->activeRawText(), {} },
+ Ui::InputField::HistoryAction::Clear);
+ _article->clearTextLeafHeightOverride();
+ } else {
+ const auto activeText = _state->activeText();
+ _field->setTextWithTags(
+ {
+ activeText.text,
+ TextUtilities::ConvertEntitiesToTextTags(activeText.entities),
+ },
+ Ui::InputField::HistoryAction::Clear);
+ }
auto cursor = _field->textCursor();
const auto size = _field->getLastText().size();
const auto from = std::clamp(selectionFrom, 0, size);
@@ -639,6 +715,10 @@ void Widget::applyFieldTextToState() {
if (_settingField || _field->isHidden()) {
return;
}
+ if (_state->activeFieldMode() == State::FieldMode::Raw) {
+ _state->applyActiveRawText(_field->getLastText());
+ return;
+ }
const auto text = _field->getTextWithAppliedMarkdown();
_state->applyActiveText({
.text = text.text,
@@ -646,11 +726,144 @@ void Widget::applyFieldTextToState() {
});
}
+void Widget::hideInlineField() {
+ if (_field->isHidden()) {
+ return;
+ }
+ const auto wasSettingField = _settingField;
+ _settingField = true;
+ const auto guard = gsl::finally([&] {
+ _settingField = wasSettingField;
+ });
+ _field->hide();
+}
+
+void Widget::activateTextOrdinalAtEnd(int ordinal) {
+ if (!_state->setActiveTextByOrdinal(ordinal)) {
+ return;
+ }
+ activateTextOrdinal(ordinal, _state->activeTextLength());
+}
+
+bool Widget::handleFieldKey(QKeyEvent *e) {
+ if (_field->isHidden()) {
+ return false;
+ }
+ const auto key = e->key();
+ if (key == Qt::Key_Escape) {
+ hideInlineFieldAndRefresh();
+ e->accept();
+ return true;
+ }
+ const auto modifiers = e->modifiers()
+ & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
+ if (modifiers != Qt::NoModifier) {
+ return false;
+ }
+ const auto cursor = _field->textCursor();
+ if (cursor.hasSelection()) {
+ return false;
+ }
+ const auto position = cursor.position();
+ const auto length = _field->getLastText().size();
+ auto handled = false;
+ if (position <= 0
+ && (key == Qt::Key_Left
+ || key == Qt::Key_Up
+ || key == Qt::Key_PageUp)) {
+ handled = moveBoundary(false, false);
+ } else if (position >= length
+ && (key == Qt::Key_Right
+ || key == Qt::Key_Down
+ || key == Qt::Key_PageDown)) {
+ handled = moveBoundary(true, true);
+ } else if (position <= 0 && key == Qt::Key_Backspace) {
+ handled = removeBoundaryOwner(false);
+ } else if (position >= length && key == Qt::Key_Delete) {
+ handled = removeBoundaryOwner(true);
+ }
+ if (handled) {
+ e->accept();
+ }
+ return handled;
+}
+
+bool Widget::moveBoundary(bool forward, bool allowTrailing) {
+ const auto target = forward
+ ? _state->nextEditableOrdinal()
+ : _state->previousEditableOrdinal();
+ const auto addTrailing = forward
+ && allowTrailing
+ && !target
+ && !_state->isActiveTopLevelParagraph();
+ if (!target && !addTrailing) {
+ return false;
+ }
+ commitInlineField();
+ if (target) {
+ refreshPreparedContent();
+ if (forward) {
+ activateTextOrdinal(*target, 0);
+ } else {
+ activateTextOrdinalAtEnd(*target);
+ }
+ return true;
+ }
+ const auto ordinal = _state->ensureTrailingParagraphActive();
+ refreshPreparedContent();
+ activateTextOrdinal(ordinal, 0);
+ return true;
+}
+
+bool Widget::moveBoundaryAfterCommit(bool forward, bool allowTrailing) {
+ const auto target = forward
+ ? _state->nextEditableOrdinal()
+ : _state->previousEditableOrdinal();
+ if (target) {
+ refreshPreparedContent();
+ if (forward) {
+ activateTextOrdinal(*target, 0);
+ } else {
+ activateTextOrdinalAtEnd(*target);
+ }
+ return true;
+ }
+ if (forward && allowTrailing && !_state->isActiveTopLevelParagraph()) {
+ const auto ordinal = _state->ensureTrailingParagraphActive();
+ refreshPreparedContent();
+ activateTextOrdinal(ordinal, 0);
+ return true;
+ }
+ return false;
+}
+
+bool Widget::removeBoundaryOwner(bool forward) {
+ commitInlineField();
+ if (_state->activeOwnerIsEmpty()) {
+ const auto target = _state->removeActiveOwnerAndSelectAdjacent(
+ forward);
+ hideInlineField();
+ _article->clearTextLeafHeightOverride();
+ refreshPreparedContent();
+ if (target) {
+ if (forward) {
+ activateTextOrdinal(*target, 0);
+ } else {
+ activateTextOrdinalAtEnd(*target);
+ }
+ } else {
+ activateInitialNode();
+ }
+ return true;
+ }
+ return moveBoundaryAfterCommit(forward, forward);
+}
+
void Widget::ensurePendingActivation() {
if (_pendingOrdinal < 0) {
_activeSegmentIndex = (_activeOrdinal >= 0)
- ? segmentIndexForTextOrdinal(_activeOrdinal)
- : _article->firstTextSegmentIndex();
+ ? segmentIndexForEditableOrdinal(_activeOrdinal)
+ : _article->firstEditableSegmentIndex();
return;
}
const auto ordinal = _pendingOrdinal;
@@ -670,11 +883,17 @@ void Widget::updateInlineFieldHeightOverride() {
_pendingHeightOverrideUpdate = true;
return;
}
- const auto segmentRect = outerTextSegmentRect(_activeSegmentIndex);
+ const auto textLeafIndex = _article->textLeafIndexForSegment(
+ _activeSegmentIndex);
+ if (textLeafIndex < 0) {
+ _article->clearTextLeafHeightOverride();
+ return;
+ }
+ const auto segmentRect = outerEditableSegmentRect(_activeSegmentIndex);
const auto height = segmentRect.isEmpty()
? _field->height()
: std::max(_field->geometry().bottom() + 1 - segmentRect.y(), 1);
- _article->setTextLeafHeightOverride(_activeOrdinal, height);
+ _article->setTextLeafHeightOverride(textLeafIndex, height);
resizeToWidth(std::max(widthNoMargins(), 1));
update();
}
@@ -684,24 +903,27 @@ void Widget::syncInlineFieldGeometry(int width) {
return;
}
if (_activeSegmentIndex >= 0) {
- ensureInlineFieldStyleForSegment(_activeSegmentIndex);
+ ensureInlineFieldForSegment(_activeSegmentIndex);
}
- const auto segmentRect = outerTextSegmentRect(_activeSegmentIndex);
+ const auto segmentRect = outerEditableSegmentRect(_activeSegmentIndex);
if (segmentRect.isEmpty()) {
_pendingOrdinal = _activeOrdinal;
_pendingCursorOffset = _field->textCursor().position();
- _field->hide();
+ hideInlineField();
_article->clearTextLeafHeightOverride();
return;
}
+ const auto margins = _field->fullTextMargins();
+ const auto left = segmentRect.x() - margins.left();
+ const auto top = segmentRect.y() - margins.top();
const auto fieldWidth = std::max(
- std::min(segmentRect.width(), width - segmentRect.x()),
+ std::min(
+ segmentRect.width() + margins.left() + margins.right(),
+ width - left),
1);
_syncingInlineFieldGeometry = true;
_field->resizeToWidth(fieldWidth);
const auto fieldHeight = FieldNaturalHeight(_field.get());
- const auto left = segmentRect.x();
- const auto top = segmentRect.y();
_field->setGeometryToLeft(left, top, fieldWidth, fieldHeight, width);
_field->raise();
_syncingInlineFieldGeometry = false;
@@ -766,12 +988,12 @@ void Widget::updateTextSelection(
}
}
-int Widget::textOrdinalForSegment(int segmentIndex) const {
- return _article->textLeafIndexForSegment(segmentIndex);
+int Widget::editableOrdinalForSegment(int segmentIndex) const {
+ return _article->editableIndexForSegment(segmentIndex);
}
-int Widget::segmentIndexForTextOrdinal(int ordinal) const {
- return _article->segmentIndexForTextLeafIndex(ordinal);
+int Widget::segmentIndexForEditableOrdinal(int ordinal) const {
+ return _article->segmentIndexForEditableIndex(ordinal);
}
QPoint Widget::articleTopLeft() const {
@@ -786,8 +1008,8 @@ int Widget::articleWidth(int outerWidth) const {
1);
}
-QRect Widget::outerTextSegmentRect(int segmentIndex) const {
- const auto rect = _article->textSegmentRect(segmentIndex);
+QRect Widget::outerEditableSegmentRect(int segmentIndex) const {
+ const auto rect = _article->segmentRect(segmentIndex);
return rect.isEmpty() ? rect : rect.translated(articleTopLeft());
}
@@ -823,9 +1045,11 @@ Markdown::MarkdownArticlePaintContext Widget::textPaintContext(QRect clip) {
});
},
};
- context.hiddenTextSegmentIndex = _field->isHidden()
+ const auto hiddenSegmentIndex = _field->isHidden()
? -1
: _activeSegmentIndex;
+ context.hiddenTextSegmentIndex = hiddenSegmentIndex;
+ context.hiddenSegmentIndex = hiddenSegmentIndex;
context.selectionState.selection = _selection;
context.selectionState.endpoints = &_selectionEndpoints;
return context;
diff --git a/Telegram/SourceFiles/iv/iv_editor_widget.h b/Telegram/SourceFiles/iv/iv_editor_widget.h
index 46780093a3..5ad782b2eb 100644
--- a/Telegram/SourceFiles/iv/iv_editor_widget.h
+++ b/Telegram/SourceFiles/iv/iv_editor_widget.h
@@ -17,6 +17,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include
#include
+class QEvent;
+class QKeyEvent;
+class QObject;
+
namespace Ui {
class ChatStyle;
class ChatTheme;
@@ -48,12 +52,16 @@ public:
void commitInlineField();
void refreshPreparedContent();
void syncInlineFieldGeometry();
+ void insertBlock(State::InsertAction action);
+ void insertMedia(State::InsertBlockType type);
+ void insertMap(double latitude, double longitude);
void insertHeading1();
void insertBlockquote();
int resizeGetHeight(int newWidth) override;
protected:
+ bool eventFilter(QObject *object, QEvent *event) override;
void focusInEvent(QFocusEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
@@ -67,6 +75,7 @@ private:
int lineHeight = 0;
style::color textFg;
style::align align = style::al_left;
+ bool italic = false;
};
struct InlineFieldStyleKey {
@@ -109,22 +118,31 @@ private:
const Markdown::MarkdownArticleTextLeafStyle &style) const;
[[nodiscard]] InlineFieldStyleKey inlineFieldStyleKey(
const InlineFieldStyleData &data) const;
- void ensureInlineFieldStyleForSegment(int segmentIndex);
+ void ensureInlineFieldForSegment(int segmentIndex);
void setupInlineField();
void recreateInlineField(const style::InputField &st);
void activateTrailingParagraph();
void applyFieldTextToState();
+ void hideInlineField();
void acceptInlineField();
+ void hideInlineFieldAndRefresh();
+ void activateTextOrdinalAtEnd(int ordinal);
+ [[nodiscard]] bool handleFieldKey(QKeyEvent *e);
+ [[nodiscard]] bool moveBoundary(bool forward, bool allowTrailing);
+ [[nodiscard]] bool moveBoundaryAfterCommit(
+ bool forward,
+ bool allowTrailing);
+ [[nodiscard]] bool removeBoundaryOwner(bool forward);
void ensurePendingActivation();
void updateInlineFieldHeightOverride();
void syncInlineFieldGeometry(int width);
void clearTextSelection();
void updateTextSelection(const Markdown::MarkdownArticleHitTestResult &hit);
- [[nodiscard]] int textOrdinalForSegment(int segmentIndex) const;
- [[nodiscard]] int segmentIndexForTextOrdinal(int ordinal) const;
+ [[nodiscard]] int editableOrdinalForSegment(int segmentIndex) const;
+ [[nodiscard]] int segmentIndexForEditableOrdinal(int ordinal) const;
[[nodiscard]] QPoint articleTopLeft() const;
[[nodiscard]] int articleWidth(int outerWidth) const;
- [[nodiscard]] QRect outerTextSegmentRect(int segmentIndex) const;
+ [[nodiscard]] QRect outerEditableSegmentRect(int segmentIndex) const;
[[nodiscard]] Markdown::MarkdownArticlePaintContext textPaintContext(
QRect clip);
@@ -138,6 +156,7 @@ private:
std::vector _highlightColors;
std::vector _fieldStyles;
std::optional _activeFieldStyleKey;
+ State::FieldMode _fieldMode = State::FieldMode::Rich;
int _articleHeight = 0;
int _activeOrdinal = -1;
int _activeSegmentIndex = -1;
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp
index d32047cbf3..92b5663d8e 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp
@@ -119,6 +119,14 @@ void RestoreRelatedArticleImageStates(
}
}
+[[nodiscard]] bool IsDisplayMathSegment(const SelectableSegment &segment) {
+ return (segment.kind == SelectableSegmentKind::DisplayMath);
+}
+
+[[nodiscard]] bool IsEditableSegment(const SelectableSegment &segment) {
+ return segment.isTextLeaf() || IsDisplayMathSegment(segment);
+}
+
void CollectPlaceholderIds(
const std::vector &blocks,
std::unordered_set *result) {
@@ -747,19 +755,34 @@ public:
[[nodiscard]] bool segmentIsText(int index) const;
+ [[nodiscard]] bool segmentIsDisplayMath(int index) const;
+
+ [[nodiscard]] bool segmentIsEditable(int index) const;
+
[[nodiscard]] int segmentLength(int index) const;
[[nodiscard]] int firstTextSegmentIndex() const;
+ [[nodiscard]] int firstEditableSegmentIndex() const;
+
[[nodiscard]] int textLeafIndexForSegment(int segmentIndex) const;
[[nodiscard]] int segmentIndexForTextLeafIndex(int textLeafIndex) const;
+ [[nodiscard]] int editableIndexForSegment(int segmentIndex) const;
+
+ [[nodiscard]] int segmentIndexForEditableIndex(int editableIndex) const;
+
[[nodiscard]] QRect textSegmentRect(int segmentIndex) const;
+ [[nodiscard]] QRect segmentRect(int segmentIndex) const;
+
[[nodiscard]] MarkdownArticleTextLeafStyle textLeafStyleForSegment(
int segmentIndex) const;
+ [[nodiscard]] MarkdownArticleTextLeafStyle editableStyleForSegment(
+ int segmentIndex) const;
+
[[nodiscard]] int selectionOffsetFromHit(
const MarkdownArticleHitTestResult &result,
TextSelectType selectionType) const;
@@ -1087,6 +1110,16 @@ bool MarkdownArticle::Impl::segmentIsText(int index) const {
return segment && segment->isTextLeaf();
}
+bool MarkdownArticle::Impl::segmentIsDisplayMath(int index) const {
+ const auto segment = FindSegment(&_segments, index);
+ return segment && IsDisplayMathSegment(*segment);
+}
+
+bool MarkdownArticle::Impl::segmentIsEditable(int index) const {
+ const auto segment = FindSegment(&_segments, index);
+ return segment && IsEditableSegment(*segment);
+}
+
int MarkdownArticle::Impl::segmentLength(int index) const {
const auto segment = FindSegment(&_segments, index);
return segment ? SegmentLength(*segment) : 0;
@@ -1101,6 +1134,15 @@ int MarkdownArticle::Impl::firstTextSegmentIndex() const {
return -1;
}
+int MarkdownArticle::Impl::firstEditableSegmentIndex() const {
+ for (const auto &segment : _segments) {
+ if (IsEditableSegment(segment)) {
+ return segment.index;
+ }
+ }
+ return -1;
+}
+
int MarkdownArticle::Impl::textLeafIndexForSegment(int segmentIndex) const {
auto textLeafIndex = 0;
for (const auto &segment : _segments) {
@@ -1131,11 +1173,56 @@ int MarkdownArticle::Impl::segmentIndexForTextLeafIndex(
return -1;
}
+int MarkdownArticle::Impl::editableIndexForSegment(int segmentIndex) const {
+ auto editableIndex = 0;
+ for (const auto &segment : _segments) {
+ if (!IsEditableSegment(segment)) {
+ continue;
+ } else if (segment.index == segmentIndex) {
+ return editableIndex;
+ }
+ ++editableIndex;
+ }
+ return -1;
+}
+
+int MarkdownArticle::Impl::segmentIndexForEditableIndex(
+ int editableIndex) const {
+ if (editableIndex < 0) {
+ return -1;
+ }
+ auto current = 0;
+ for (const auto &segment : _segments) {
+ if (!IsEditableSegment(segment)) {
+ continue;
+ } else if (current == editableIndex) {
+ return segment.index;
+ }
+ ++current;
+ }
+ return -1;
+}
+
QRect MarkdownArticle::Impl::textSegmentRect(int segmentIndex) const {
const auto segment = FindSegment(&_segments, segmentIndex);
return (segment && segment->isTextLeaf()) ? segment->textRect : QRect();
}
+QRect MarkdownArticle::Impl::segmentRect(int segmentIndex) const {
+ const auto segment = FindSegment(&_segments, segmentIndex);
+ if (!segment) {
+ return QRect();
+ } else if (segment->isTextLeaf()) {
+ return segment->textRect;
+ } else if (IsDisplayMathSegment(*segment)) {
+ if (!segment->outerRect.isEmpty()) {
+ return segment->outerRect;
+ }
+ return segment->block ? segment->block->formulaRect : QRect();
+ }
+ return QRect();
+}
+
MarkdownArticleTextLeafStyle MarkdownArticle::Impl::textLeafStyleForSegment(
int segmentIndex) const {
const auto segment = FindSegment(&_segments, segmentIndex);
@@ -1149,6 +1236,26 @@ MarkdownArticleTextLeafStyle MarkdownArticle::Impl::textLeafStyleForSegment(
.textColor = TextColorForSegment(*segment, st),
.lineHeight = TextLineHeight(textStyle),
.align = segment->align,
+ .italic = segment->block && segment->block->pullquote,
+ };
+}
+
+MarkdownArticleTextLeafStyle MarkdownArticle::Impl::editableStyleForSegment(
+ int segmentIndex) const {
+ const auto segment = FindSegment(&_segments, segmentIndex);
+ if (!segment) {
+ return {};
+ } else if (segment->isTextLeaf()) {
+ return textLeafStyleForSegment(segmentIndex);
+ } else if (!IsDisplayMathSegment(*segment)) {
+ return {};
+ }
+ const auto &st = layoutStyle();
+ return {
+ .textStyle = &st.displayMath.fallbackStyle,
+ .textColor = st.displayMath.fg,
+ .lineHeight = TextLineHeight(st.displayMath.fallbackStyle),
+ .align = segment->align,
};
}
@@ -1788,6 +1895,14 @@ bool MarkdownArticle::segmentIsText(int index) const {
return _impl->segmentIsText(index);
}
+bool MarkdownArticle::segmentIsDisplayMath(int index) const {
+ return _impl->segmentIsDisplayMath(index);
+}
+
+bool MarkdownArticle::segmentIsEditable(int index) const {
+ return _impl->segmentIsEditable(index);
+}
+
int MarkdownArticle::segmentLength(int index) const {
return _impl->segmentLength(index);
}
@@ -1796,6 +1911,10 @@ int MarkdownArticle::firstTextSegmentIndex() const {
return _impl->firstTextSegmentIndex();
}
+int MarkdownArticle::firstEditableSegmentIndex() const {
+ return _impl->firstEditableSegmentIndex();
+}
+
int MarkdownArticle::textLeafIndexForSegment(int segmentIndex) const {
return _impl->textLeafIndexForSegment(segmentIndex);
}
@@ -1804,15 +1923,32 @@ int MarkdownArticle::segmentIndexForTextLeafIndex(int textLeafIndex) const {
return _impl->segmentIndexForTextLeafIndex(textLeafIndex);
}
+int MarkdownArticle::editableIndexForSegment(int segmentIndex) const {
+ return _impl->editableIndexForSegment(segmentIndex);
+}
+
+int MarkdownArticle::segmentIndexForEditableIndex(int editableIndex) const {
+ return _impl->segmentIndexForEditableIndex(editableIndex);
+}
+
QRect MarkdownArticle::textSegmentRect(int segmentIndex) const {
return _impl->textSegmentRect(segmentIndex);
}
+QRect MarkdownArticle::segmentRect(int segmentIndex) const {
+ return _impl->segmentRect(segmentIndex);
+}
+
MarkdownArticleTextLeafStyle MarkdownArticle::textLeafStyleForSegment(
int segmentIndex) const {
return _impl->textLeafStyleForSegment(segmentIndex);
}
+MarkdownArticleTextLeafStyle MarkdownArticle::editableStyleForSegment(
+ int segmentIndex) const {
+ return _impl->editableStyleForSegment(segmentIndex);
+}
+
int MarkdownArticle::selectionOffsetFromHit(
const MarkdownArticleHitTestResult &result,
TextSelectType selectionType) const {
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h
index afbea6ec82..c9ea433e98 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h
@@ -157,6 +157,7 @@ struct MarkdownArticlePaintContext final : Ui::ChatPaintContext {
PaintSelectionState selectionState;
MarkdownArticleRevealPaintState *reveal = nullptr;
int hiddenTextSegmentIndex = -1;
+ int hiddenSegmentIndex = -1;
bool debugBlockGeometry = false;
[[nodiscard]] MarkdownArticlePaintContext translated(int x, int y) const {
@@ -192,6 +193,7 @@ struct MarkdownArticleTextLeafStyle {
style::color textColor;
int lineHeight = 0;
style::align align = style::al_left;
+ bool italic = false;
[[nodiscard]] bool valid() const {
return textStyle && textColor;
@@ -237,13 +239,21 @@ public:
const QString &anchorId);
[[nodiscard]] bool toggleDetails(const QString &anchorId);
[[nodiscard]] bool segmentIsText(int index) const;
+ [[nodiscard]] bool segmentIsDisplayMath(int index) const;
+ [[nodiscard]] bool segmentIsEditable(int index) const;
[[nodiscard]] int segmentLength(int index) const;
[[nodiscard]] int firstTextSegmentIndex() const;
+ [[nodiscard]] int firstEditableSegmentIndex() const;
[[nodiscard]] int textLeafIndexForSegment(int segmentIndex) const;
[[nodiscard]] int segmentIndexForTextLeafIndex(int textLeafIndex) const;
+ [[nodiscard]] int editableIndexForSegment(int segmentIndex) const;
+ [[nodiscard]] int segmentIndexForEditableIndex(int editableIndex) const;
[[nodiscard]] QRect textSegmentRect(int segmentIndex) const;
+ [[nodiscard]] QRect segmentRect(int segmentIndex) const;
[[nodiscard]] MarkdownArticleTextLeafStyle textLeafStyleForSegment(
int segmentIndex) const;
+ [[nodiscard]] MarkdownArticleTextLeafStyle editableStyleForSegment(
+ int segmentIndex) const;
[[nodiscard]] int selectionOffsetFromHit(
const MarkdownArticleHitTestResult &result,
TextSelectType selectionType) const;
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp
index e46ab30941..a5e0505c22 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp
@@ -418,7 +418,7 @@ void LayoutMediaCaption(
int skip,
int *bottom,
LayoutContext context) {
- if (prepared.text.text.isEmpty()) {
+ if (prepared.text.text.isEmpty() && !prepared.forceTextSegment) {
return;
}
block->supplementary = prepared.supplementary;
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp
index 01994ebb33..e9eba91f71 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp
@@ -687,7 +687,8 @@ void PaintSelectableTextLeaf(
std::optional selection = std::nullopt,
int elisionLines = 0) {
if ((segmentIndex >= 0)
- && (context.hiddenTextSegmentIndex == segmentIndex)) {
+ && ((context.hiddenTextSegmentIndex == segmentIndex)
+ || (context.hiddenSegmentIndex == segmentIndex))) {
if (context.reveal && !elisionLines) {
context.reveal->nextLine += CountTextRevealLines(leaf, rect, width);
}
@@ -1362,6 +1363,10 @@ void PaintDisplayMathBlock(
context,
block.visibleFormulaRect,
[&](Painter &p, const MarkdownArticlePaintContext &formulaContext) {
+ if (block.segmentIndex >= 0
+ && (formulaContext.hiddenSegmentIndex == block.segmentIndex)) {
+ return;
+ }
const auto &paintSt = PaintStyle(formulaContext, st);
const auto formula = PreparedFormulaFor(formulas, block.formulaIndex);
p.setPen(paintSt.textColor->c);
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp
index afa470f225..363956346f 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp
@@ -405,13 +405,14 @@ void CollectSelectableSegments(
segment.kind = SelectableSegmentKind::DisplayMath;
segment.block = █
segment.outerRect = block.visibleFormulaRect;
+ segment.align = block.formulaAlign;
segment.length = 1;
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
} break;
case PreparedBlockKind::Table: {
- if (!block.textRect.isEmpty() && !block.leaf.isEmpty()) {
+ if (!block.textRect.isEmpty()) {
auto textSegment = SelectableSegment();
textSegment.kind = SelectableSegmentKind::TextLeaf;
textSegment.leaf = &block.leaf;
@@ -477,7 +478,7 @@ void CollectSelectableSegments(
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
- if (!block.textRect.isEmpty() && !block.leaf.isEmpty()) {
+ if (!block.textRect.isEmpty()) {
auto textSegment = SelectableSegment();
textSegment.kind = SelectableSegmentKind::TextLeaf;
textSegment.leaf = &block.leaf;
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h
index a3efd84fd4..5e0f4a49eb 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h
@@ -253,6 +253,7 @@ struct PreparedBlock {
bool tight = false;
bool supplementary = false;
bool pullquote = false;
+ bool forceTextSegment = false;
};
struct PreparedRenderDocument {
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp
index 796a6eb750..93f2479720 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp
@@ -144,47 +144,6 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
return block;
}
-[[nodiscard]] bool PrepareNativeIvMathBlock(
- const MTPDpageBlockMath &data,
- std::vector *result,
- NativeIvPrepareState *state) {
- const auto source = qs(data.vsource());
- if (source.trimmed().isEmpty()) {
- return true;
- }
- auto block = PreparedBlock();
- block.kind = PreparedBlockKind::DisplayMath;
- block.formulaTex = source;
- block.mathKind = MathKind::Display;
- block.formulaIndex = state->rememberFormula(block);
- result->push_back(std::move(block));
- return true;
-}
-
-[[nodiscard]] bool AppendNativeIvFlowBlock(
- std::vector *result,
- PreparedBlockKind kind,
- int headingLevel,
- const MTPRichText &text,
- NativeIvPrepareState *state,
- bool allowEmpty = false) {
- auto prepared = PreparedIvRichText();
- auto anchorId = QString();
- const auto context = NativeIvRichTextContextForTextSize(
- NativeIvFlowTextSize(kind, headingLevel, state->dimensions),
- state->dimensions);
- if (!PrepareNativeIvRichText(text, &prepared, &anchorId, state, context)) {
- return false;
- }
- return AppendPreparedIvRichBlock(
- result,
- kind,
- headingLevel,
- std::move(prepared),
- std::move(anchorId),
- allowEmpty || state->editMode);
-}
-
void WrapPreparedIvRichTextItalic(PreparedIvRichText *prepared) {
if (!prepared || prepared->text.text.isEmpty()) {
return;
@@ -217,6 +176,7 @@ bool AppendPreparedQuoteParagraph(
}
if (pullquote && (result->size() > count)) {
result->back().flowAlignment = TableAlignment::Center;
+ result->back().pullquote = true;
}
return true;
}
@@ -513,10 +473,20 @@ void MarkNativeIvTableSlots(
state->editMode)) {
return false;
}
+ if (!data.blocks.empty()
+ && !PrepareNativeIvBlocks(
+ RichPage{
+ .blocks = data.blocks,
+ },
+ &block.children,
+ state)) {
+ return false;
+ }
if (block.children.empty()) {
block.children.push_back(EmptyParagraphBlock());
if (data.pullquote) {
block.children.back().flowAlignment = TableAlignment::Center;
+ block.children.back().pullquote = true;
}
}
result->push_back(std::move(block));
@@ -578,7 +548,9 @@ void MarkNativeIvTableSlots(
state->editMode)) {
return false;
}
- } else if (!PrepareNativeIvBlocks(
+ }
+ if (!item.blocks.empty()
+ && !PrepareNativeIvBlocks(
RichPage{
.blocks = item.blocks,
},
@@ -767,7 +739,7 @@ void MarkNativeIvTableSlots(
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Details;
block.anchorId = std::move(anchorId);
- block.collapsed = !data.open;
+ block.collapsed = state->editMode ? false : !data.open;
block.text = std::move(summary.text);
block.links = std::move(summary.links);
block.anchorIds = std::move(summary.anchorIds);
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp
index ad43f6392f..d91f3eddc2 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp
@@ -292,6 +292,35 @@ void AppendCanonicalNativeIvRichText(
state);
}
+[[nodiscard]] bool PrepareNativeIvCanonicalPlaceholderBlock(
+ QString label,
+ const Iv::RichPage::RichText &caption,
+ QString anchorId,
+ std::vector *result,
+ NativeIvPrepareState *state) {
+ auto prepared = PreparedIvRichText();
+ if (!PrepareNativeIvCaption(caption, &prepared, &anchorId, state)) {
+ return state->result.failure.failed()
+ ? false
+ : PrepareNativeIvPlainPlaceholderBlock(std::move(label), result);
+ }
+ SortPreparedIvRichText(&prepared);
+ auto block = PreparedBlock();
+ block.kind = PreparedBlockKind::Placeholder;
+ block.text = std::move(prepared.text);
+ block.links = std::move(prepared.links);
+ block.anchorId = std::move(anchorId);
+ block.anchorIds = std::move(prepared.anchorIds);
+ block.supplementary = true;
+ block.forceTextSegment = state->editMode;
+ block.placeholder.label = std::move(label);
+ block.placeholder.copyText = block.text.text.isEmpty()
+ ? block.placeholder.label
+ : (block.placeholder.label + u"\n"_q + block.text.text);
+ result->push_back(std::move(block));
+ return true;
+}
+
} // namespace
bool PrepareNativeIvRichText(
@@ -351,7 +380,14 @@ bool PrepareNativeIvPhotoBlock(
if (!CanonicalPhotoId(data)
|| CanonicalWidth(data) <= 0
|| CanonicalHeight(data) <= 0) {
- return true;
+ return state->editMode
+ ? PrepareNativeIvCanonicalPlaceholderBlock(
+ u"Photo"_q,
+ data.caption,
+ data.anchorId,
+ result,
+ state)
+ : true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
@@ -366,6 +402,7 @@ bool PrepareNativeIvPhotoBlock(
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
+ block.forceTextSegment = state->editMode;
block.photo.id = GeneratePreparedMediaBlockId(state);
block.photo.photoId = CanonicalPhotoId(data);
block.photo.width = CanonicalWidth(data);
@@ -384,7 +421,14 @@ bool PrepareNativeIvVideoBlock(
if (!CanonicalDocumentId(data)
|| CanonicalWidth(data) <= 0
|| CanonicalHeight(data) <= 0) {
- return true;
+ return state->editMode
+ ? PrepareNativeIvCanonicalPlaceholderBlock(
+ u"Video"_q,
+ data.caption,
+ data.anchorId,
+ result,
+ state)
+ : true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
@@ -399,6 +443,7 @@ bool PrepareNativeIvVideoBlock(
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
+ block.forceTextSegment = state->editMode;
block.video.id = GeneratePreparedMediaBlockId(state);
block.video.media.kind = PreparedMediaItemKind::Document;
block.video.media.id = CanonicalDocumentId(data);
@@ -414,7 +459,14 @@ bool PrepareNativeIvAudioBlock(
std::vector *result,
NativeIvPrepareState *state) {
if (!CanonicalDocumentId(data)) {
- return true;
+ return state->editMode
+ ? PrepareNativeIvCanonicalPlaceholderBlock(
+ u"Audio"_q,
+ data.caption,
+ data.anchorId,
+ result,
+ state)
+ : true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
@@ -429,6 +481,7 @@ bool PrepareNativeIvAudioBlock(
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
+ block.forceTextSegment = state->editMode;
block.audio.id = GeneratePreparedMediaBlockId(state);
block.audio.documentId = CanonicalDocumentId(data);
block.audio.title = data.audioTitle;
@@ -444,7 +497,14 @@ bool PrepareNativeIvMapBlock(
std::vector *result,
NativeIvPrepareState *state) {
if (!data.accessHash || data.width <= 0 || data.height <= 0) {
- return true;
+ return state->editMode
+ ? PrepareNativeIvCanonicalPlaceholderBlock(
+ u"Map"_q,
+ data.caption,
+ data.anchorId,
+ result,
+ state)
+ : true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
@@ -459,6 +519,7 @@ bool PrepareNativeIvMapBlock(
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
+ block.forceTextSegment = state->editMode;
block.map.id = GeneratePreparedMediaBlockId(state);
block.map.latitude = data.latitude;
block.map.longitude = data.longitude;