mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 03:42:56 +00:00
Add markdown viewer header, sharing.
This commit is contained in:
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/abstract_box.h" // Ui::show().
|
||||
#include "chat_helpers/ttl_media_layer_widget.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "data/data_document.h"
|
||||
@@ -251,7 +252,17 @@ void ResolveDocument(
|
||||
if (!openImageInApp()) {
|
||||
const auto path = document->filepath(true);
|
||||
if (!path.isEmpty()) {
|
||||
if (!Core::App().iv().showMarkdown(path)) {
|
||||
auto context = QVariant();
|
||||
if (item) {
|
||||
auto clickHandlerContext = ClickHandlerContext();
|
||||
clickHandlerContext.itemId = item->fullId();
|
||||
if (controller) {
|
||||
clickHandlerContext.sessionWindow = controller;
|
||||
clickHandlerContext.show = controller->uiShow();
|
||||
}
|
||||
context = QVariant::fromValue(clickHandlerContext);
|
||||
}
|
||||
if (!Core::App().iv().showMarkdown(path, context)) {
|
||||
LaunchWithWarning(path, item);
|
||||
}
|
||||
} else if (document->status == FileReady
|
||||
|
||||
@@ -175,7 +175,7 @@ defaultMarkdownBodyStyle: TextStyle(historyTextStyle) {
|
||||
lineHeight: 24px;
|
||||
}
|
||||
defaultMarkdownCodeStyle: TextStyle(defaultMarkdownBodyStyle) {
|
||||
font: font(15px);
|
||||
font: font(14px);
|
||||
lineHeight: 22px;
|
||||
}
|
||||
defaultMarkdownDisplayMathFallbackStyle: TextStyle(defaultMarkdownCodeStyle) {
|
||||
@@ -200,7 +200,7 @@ defaultMarkdownList: MarkdownList {
|
||||
continuationIndent: 22px;
|
||||
markerWidth: 22px;
|
||||
markerSkip: 8px;
|
||||
bulletRadius: 3px;
|
||||
bulletRadius: 2px;
|
||||
bulletLeftShift: 0px;
|
||||
bulletFg: windowFg;
|
||||
taskCheck: defaultCheck;
|
||||
|
||||
@@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QJsonValue>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QKeySequence>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtGui/QWindow>
|
||||
#include <charconv>
|
||||
@@ -689,6 +690,10 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
const auto event = static_cast<QKeyEvent*>(e.get());
|
||||
if (event->matches(QKeySequence::Close)) {
|
||||
close();
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
if (event->modifiers() & Qt::ControlModifier) {
|
||||
if (event->key() == Qt::Key_Plus
|
||||
|| event->key() == Qt::Key_Equal) {
|
||||
|
||||
@@ -26,6 +26,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_thread.h"
|
||||
#include "data/data_web_page.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "iv/markdown/iv_markdown_controller.h"
|
||||
@@ -54,9 +56,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_session_controller_link_info.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace Iv {
|
||||
namespace {
|
||||
|
||||
@@ -278,6 +284,119 @@ private:
|
||||
|
||||
};
|
||||
|
||||
struct MarkdownMessageContext {
|
||||
ClickHandlerContext clickHandlerContext;
|
||||
base::weak_ptr<Window::SessionController> sessionWindow;
|
||||
};
|
||||
|
||||
struct LocalMarkdownTarget {
|
||||
QString key;
|
||||
QString path;
|
||||
QString sourceName;
|
||||
QString fragment;
|
||||
};
|
||||
|
||||
[[nodiscard]] QString NormalizeLocalMarkdownFragment(QString fragment) {
|
||||
fragment = QString::fromUtf8(
|
||||
QByteArray::fromPercentEncoding(fragment.toUtf8()));
|
||||
fragment = fragment.trimmed().toLower();
|
||||
while (fragment.startsWith(QChar('#'))) {
|
||||
fragment.remove(0, 1);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
[[nodiscard]] LocalMarkdownTarget ParseLocalMarkdownTarget(QString path) {
|
||||
auto sourcePath = path;
|
||||
auto fragment = QString();
|
||||
if (!QFileInfo(sourcePath).exists()) {
|
||||
const auto hash = sourcePath.lastIndexOf(QChar('#'));
|
||||
const auto candidate = (hash > 0) ? sourcePath.mid(0, hash) : QString();
|
||||
if (!candidate.isEmpty() && QFileInfo(candidate).exists()) {
|
||||
fragment = NormalizeLocalMarkdownFragment(sourcePath.mid(hash + 1));
|
||||
sourcePath = candidate;
|
||||
}
|
||||
}
|
||||
const auto info = QFileInfo(sourcePath);
|
||||
if (!info.exists()) {
|
||||
return {
|
||||
.key = path,
|
||||
.path = std::move(path),
|
||||
};
|
||||
}
|
||||
auto result = LocalMarkdownTarget{
|
||||
.key = info.absoluteFilePath(),
|
||||
.path = info.absoluteFilePath(),
|
||||
.sourceName = info.fileName(),
|
||||
.fragment = std::move(fragment),
|
||||
};
|
||||
if (!result.fragment.isEmpty()) {
|
||||
result.path += u"#"_q + result.fragment;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ExtractMarkdownMessageContext(const QVariant &context) {
|
||||
if (!context.isValid() || !context.canConvert<ClickHandlerContext>()) {
|
||||
return std::optional<MarkdownMessageContext>();
|
||||
}
|
||||
const auto clickHandlerContext = context.value<ClickHandlerContext>();
|
||||
return std::make_optional(MarkdownMessageContext{
|
||||
.clickHandlerContext = clickHandlerContext,
|
||||
.sessionWindow = clickHandlerContext.sessionWindow,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Main::Session *ResolveMarkdownSession(
|
||||
const MarkdownMessageContext &context) {
|
||||
if (const auto controller = context.sessionWindow.get()) {
|
||||
return &controller->session();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] HistoryItem *ResolveMarkdownItem(
|
||||
const MarkdownMessageContext &context) {
|
||||
const auto session = ResolveMarkdownSession(context);
|
||||
const auto itemId = context.clickHandlerContext.itemId;
|
||||
return (session && itemId) ? session->data().message(itemId) : nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool CanShareMarkdownItem(not_null<HistoryItem*> item) {
|
||||
const auto peer = item->history()->peer;
|
||||
return peer->allowsForwarding() && !item->forbidsForward();
|
||||
}
|
||||
|
||||
[[nodiscard]] Markdown::OpenOptions PrepareLocalMarkdownOptions(
|
||||
QVariant context) {
|
||||
auto options = Markdown::OpenOptions{
|
||||
.viewerKind = Markdown::ViewerKind::LocalFile,
|
||||
.clickHandlerContext = std::move(context),
|
||||
};
|
||||
const auto messageContext = ExtractMarkdownMessageContext(
|
||||
options.clickHandlerContext);
|
||||
const auto item = messageContext
|
||||
? ResolveMarkdownItem(*messageContext)
|
||||
: nullptr;
|
||||
if (item && CanShareMarkdownItem(not_null{ item })) {
|
||||
options.share = [context = *messageContext](
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
const auto session = ResolveMarkdownSession(context);
|
||||
const auto itemId = context.clickHandlerContext.itemId;
|
||||
const auto current = (session && itemId)
|
||||
? session->data().message(itemId)
|
||||
: nullptr;
|
||||
if (!show || !current || !CanShareMarkdownItem(not_null{ current })) {
|
||||
return;
|
||||
}
|
||||
FastShareMessage(
|
||||
Main::MakeSessionShow(show, not_null{ session }),
|
||||
not_null{ current });
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class Shown final : public base::has_weak_ptr {
|
||||
@@ -322,7 +441,7 @@ private:
|
||||
};
|
||||
struct FileStream {
|
||||
not_null<DocumentData*> document;
|
||||
std::unique_ptr<Media::Streaming::Loader> loader;
|
||||
std::unique_ptr<::Media::Streaming::Loader> loader;
|
||||
std::vector<PartRequest> requests;
|
||||
std::string mime;
|
||||
rpl::lifetime lifetime;
|
||||
@@ -339,6 +458,9 @@ private:
|
||||
QString title,
|
||||
QString initialFragment,
|
||||
not_null<WebPageData*> page);
|
||||
[[nodiscard]] Markdown::OpenOptions markdownOpenOptions(
|
||||
QString initialFragment,
|
||||
not_null<WebPageData*> page) const;
|
||||
|
||||
void showWindowed(Prepared result, Source source, bool refresh);
|
||||
void showHtmlWindowed(Prepared result, bool refresh);
|
||||
@@ -352,7 +474,8 @@ private:
|
||||
not_null<WebPageData*> page) const;
|
||||
[[nodiscard]] bool activateMarkdownMedia(
|
||||
const Markdown::MediaActivation &activation,
|
||||
Qt::MouseButton button) const;
|
||||
Qt::MouseButton button,
|
||||
const QVariant &clickHandlerContext) const;
|
||||
|
||||
[[nodiscard]] ::Data::FileOrigin fileOrigin(
|
||||
not_null<WebPageData*> page) const;
|
||||
@@ -361,10 +484,10 @@ private:
|
||||
void streamFile(FileStream &file, Webview::DataRequest request);
|
||||
void processPartInFile(
|
||||
FileStream &file,
|
||||
Media::Streaming::LoadedPart &&part);
|
||||
::Media::Streaming::LoadedPart &&part);
|
||||
bool finishRequestWithPart(
|
||||
PartRequest &request,
|
||||
const Media::Streaming::LoadedPart &part);
|
||||
const ::Media::Streaming::LoadedPart &part);
|
||||
void streamMap(QString params, Webview::DataRequest request);
|
||||
void sendEmbed(QByteArray hash, Webview::DataRequest request);
|
||||
|
||||
@@ -640,6 +763,33 @@ void Shown::createController() {
|
||||
}, _controller->lifetime());
|
||||
}
|
||||
|
||||
Markdown::OpenOptions Shown::markdownOpenOptions(
|
||||
QString initialFragment,
|
||||
not_null<WebPageData*> page) const {
|
||||
const auto clickHandlerContext = std::make_shared<QVariant>();
|
||||
auto options = Markdown::OpenOptions{
|
||||
.sourceName = page->displayedSiteName(),
|
||||
.sourceUrl = page->url,
|
||||
.initialFragment = std::move(initialFragment),
|
||||
.viewerKind = Markdown::ViewerKind::InstantView,
|
||||
.clickHandlerContextRef = clickHandlerContext,
|
||||
.activateMedia = [=](
|
||||
const Markdown::MediaActivation &activation,
|
||||
Qt::MouseButton button) {
|
||||
return activateMarkdownMedia(activation, button, *clickHandlerContext);
|
||||
},
|
||||
};
|
||||
if (!page->url.isEmpty()) {
|
||||
options.share = [=, url = page->url](std::shared_ptr<Ui::Show> show) {
|
||||
if (!show) {
|
||||
return;
|
||||
}
|
||||
FastShareLink(Main::MakeSessionShow(show, _session), url);
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
void Shown::createMarkdownController(
|
||||
Markdown::MarkdownArticleContent content,
|
||||
QString title,
|
||||
@@ -647,14 +797,7 @@ void Shown::createMarkdownController(
|
||||
not_null<WebPageData*> page) {
|
||||
Expects(!_markdownController);
|
||||
|
||||
auto options = Markdown::OpenOptions{
|
||||
.initialFragment = std::move(initialFragment),
|
||||
.activateMedia = [=](
|
||||
const Markdown::MediaActivation &activation,
|
||||
Qt::MouseButton button) {
|
||||
return activateMarkdownMedia(activation, button);
|
||||
},
|
||||
};
|
||||
auto options = markdownOpenOptions(std::move(initialFragment), page);
|
||||
_markdownController = std::make_unique<Markdown::Controller>(
|
||||
_delegate,
|
||||
std::move(content),
|
||||
@@ -717,15 +860,8 @@ void Shown::showMarkdownWindowed(
|
||||
QString initialFragment,
|
||||
not_null<WebPageData*> page) {
|
||||
_controller = nullptr;
|
||||
auto options = markdownOpenOptions(std::move(initialFragment), page);
|
||||
if (_markdownController) {
|
||||
auto options = Markdown::OpenOptions{
|
||||
.initialFragment = std::move(initialFragment),
|
||||
.activateMedia = [=](
|
||||
const Markdown::MediaActivation &activation,
|
||||
Qt::MouseButton button) {
|
||||
return activateMarkdownMedia(activation, button);
|
||||
},
|
||||
};
|
||||
_markdownController->update(
|
||||
std::move(content),
|
||||
std::move(title),
|
||||
@@ -746,7 +882,8 @@ std::shared_ptr<Markdown::MediaRuntime> Shown::createMediaRuntime(
|
||||
|
||||
bool Shown::activateMarkdownMedia(
|
||||
const Markdown::MediaActivation &activation,
|
||||
Qt::MouseButton button) const {
|
||||
Qt::MouseButton button,
|
||||
const QVariant &clickHandlerContext) const {
|
||||
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
|
||||
return false;
|
||||
}
|
||||
@@ -757,7 +894,7 @@ bool Shown::activateMarkdownMedia(
|
||||
if (activation.url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
HiddenUrlClickHandler::Open(activation.url);
|
||||
HiddenUrlClickHandler::Open(activation.url, clickHandlerContext);
|
||||
return true;
|
||||
case Markdown::MediaActivationKind::Photo:
|
||||
if (!activation.photo) {
|
||||
@@ -862,7 +999,7 @@ void Shown::streamFile(
|
||||
}).first->second;
|
||||
|
||||
file.loader->parts(
|
||||
) | rpl::on_next([=](Media::Streaming::LoadedPart &&part) {
|
||||
) | rpl::on_next([=](::Media::Streaming::LoadedPart &&part) {
|
||||
const auto i = _streams.find(documentId);
|
||||
Assert(i != end(_streams));
|
||||
processPartInFile(i->second, std::move(part));
|
||||
@@ -872,7 +1009,7 @@ void Shown::streamFile(
|
||||
}
|
||||
|
||||
void Shown::streamFile(FileStream &file, Webview::DataRequest request) {
|
||||
constexpr auto kPart = Media::Streaming::Loader::kPartSize;
|
||||
constexpr auto kPart = ::Media::Streaming::Loader::kPartSize;
|
||||
const auto size = file.document->size;
|
||||
const auto last = int((size + kPart - 1) / kPart);
|
||||
const auto from = int(std::min(int64(request.offset), size) / kPart);
|
||||
@@ -938,7 +1075,7 @@ QByteArray Shown::readFile(
|
||||
|
||||
void Shown::processPartInFile(
|
||||
FileStream &file,
|
||||
Media::Streaming::LoadedPart &&part) {
|
||||
::Media::Streaming::LoadedPart &&part) {
|
||||
for (auto i = begin(file.requests); i != end(file.requests);) {
|
||||
if (finishRequestWithPart(*i, part)) {
|
||||
auto done = base::take(*i);
|
||||
@@ -957,16 +1094,16 @@ void Shown::processPartInFile(
|
||||
|
||||
bool Shown::finishRequestWithPart(
|
||||
PartRequest &request,
|
||||
const Media::Streaming::LoadedPart &part) {
|
||||
const ::Media::Streaming::LoadedPart &part) {
|
||||
const auto offset = part.offset;
|
||||
if (offset == Media::Streaming::LoadedPart::kFailedOffset) {
|
||||
if (offset == ::Media::Streaming::LoadedPart::kFailedOffset) {
|
||||
request.data = QByteArray();
|
||||
return true;
|
||||
} else if (offset < request.offset
|
||||
|| offset >= request.offset + request.data.size()) {
|
||||
return false;
|
||||
}
|
||||
constexpr auto kPart = Media::Streaming::Loader::kPartSize;
|
||||
constexpr auto kPart = ::Media::Streaming::Loader::kPartSize;
|
||||
const auto copy = std::min(
|
||||
int(part.bytes.size()),
|
||||
int(request.data.size() - (offset - request.offset)));
|
||||
@@ -1493,20 +1630,30 @@ void Instance::showTonSite(
|
||||
bool Instance::showMarkdown(
|
||||
const QString &path,
|
||||
QVariant context) {
|
||||
auto i = _markdowns.find(path);
|
||||
const auto target = ParseLocalMarkdownTarget(path);
|
||||
auto options = PrepareLocalMarkdownOptions(context);
|
||||
if (!target.sourceName.isEmpty()) {
|
||||
options.sourceName = target.sourceName;
|
||||
options.sourcePath = target.key;
|
||||
}
|
||||
options.initialFragment = target.fragment;
|
||||
auto i = _markdowns.find(target.key);
|
||||
if (i == end(_markdowns)) {
|
||||
if (auto controller = Markdown::TryOpenLocalFile(_delegate, path)) {
|
||||
if (auto controller = Markdown::TryOpenLocalFile(
|
||||
_delegate,
|
||||
target.path,
|
||||
std::move(options))) {
|
||||
controller->events() | rpl::on_next([=](Markdown::Event event) {
|
||||
using Type = Markdown::Event::Type;
|
||||
switch (event.type) {
|
||||
case Type::Close:
|
||||
_markdowns.take(path);
|
||||
_markdowns.take(target.key);
|
||||
break;
|
||||
case Type::Quit:
|
||||
Shortcuts::Launch(Shortcuts::Command::Quit);
|
||||
break;
|
||||
case Type::OpenFile:
|
||||
if (!showMarkdown(event.url)) {
|
||||
if (!showMarkdown(event.url, event.context)) {
|
||||
DEBUG_LOG(("Native Markdown IV: "
|
||||
"failed local markdown link: %1"
|
||||
).arg(event.url));
|
||||
@@ -1515,10 +1662,12 @@ bool Instance::showMarkdown(
|
||||
}
|
||||
}, controller->lifetime());
|
||||
|
||||
i = _markdowns.emplace(path, std::move(controller)).first;
|
||||
i = _markdowns.emplace(target.key, std::move(controller)).first;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
i->second->updateOptions(std::move(options));
|
||||
}
|
||||
i->second->activate();
|
||||
return true;
|
||||
|
||||
@@ -24,6 +24,13 @@ constexpr auto kCodeTabColumns = 4;
|
||||
constexpr auto kCodeTrailingGuard = 0x2060;
|
||||
const auto kPhotoCopyLabel = u"Photo"_q;
|
||||
|
||||
[[nodiscard]] int SingleDigitOrderedMarkerWidth(
|
||||
const style::Markdown &markdown) {
|
||||
return std::max(
|
||||
markdown.body.font->width(u"8."_q),
|
||||
markdown.body.font->width(u"8)"_q));
|
||||
}
|
||||
|
||||
[[nodiscard]] QString CodeBlockDisplayText(const QString &text) {
|
||||
auto result = QString();
|
||||
result.reserve(text.size());
|
||||
@@ -210,8 +217,9 @@ QPoint BulletMarkerCenter(
|
||||
const style::Markdown &markdown) {
|
||||
const auto &list = markdown.list;
|
||||
const auto lineHeight = TextLineHeight(markdown.body);
|
||||
const auto markerWidth = SingleDigitOrderedMarkerWidth(markdown);
|
||||
return QPoint(
|
||||
left + list.markerWidth - list.bulletLeftShift - (lineHeight / 2),
|
||||
left + list.markerWidth - list.bulletLeftShift - ((markerWidth + 1) / 2),
|
||||
top + (lineHeight / 2));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
#include <QtCore/QSize>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QVariant>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace Ui {
|
||||
class DynamicImage;
|
||||
class Show;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Iv {
|
||||
@@ -53,11 +55,23 @@ struct MediaActivation {
|
||||
std::shared_ptr<PhotoRuntime> photo;
|
||||
};
|
||||
|
||||
enum class ViewerKind {
|
||||
Auto,
|
||||
LocalFile,
|
||||
InstantView,
|
||||
};
|
||||
|
||||
struct OpenOptions {
|
||||
QString sourceName;
|
||||
QString sourcePath;
|
||||
QString sourceUrl;
|
||||
QString initialFragment;
|
||||
ViewerKind viewerKind = ViewerKind::Auto;
|
||||
Iv::Delegate *delegate = nullptr;
|
||||
QVariant clickHandlerContext;
|
||||
std::shared_ptr<QVariant> clickHandlerContextRef;
|
||||
std::function<void()> openSource;
|
||||
std::function<void(std::shared_ptr<Ui::Show>)> share;
|
||||
std::function<bool(const MediaActivation &, Qt::MouseButton)> activateMedia;
|
||||
};
|
||||
|
||||
@@ -77,6 +91,7 @@ struct Event {
|
||||
};
|
||||
Type type = Type::Close;
|
||||
QString url;
|
||||
QVariant context;
|
||||
};
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
#include "iv/markdown/iv_markdown_controller.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "core/credits_amount.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "iv/markdown/iv_markdown_parse.h"
|
||||
#include "iv/markdown/iv_markdown_view.h"
|
||||
#include "iv/iv_delegate_impl.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "logs.h"
|
||||
#include "ui/layers/layer_manager.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/rp_window.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
|
||||
#include "styles/style_iv.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/palette.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtGui/QKeyEvent>
|
||||
#include <QtGui/QKeySequence>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -30,12 +46,80 @@ constexpr auto kZoomStep = int(10);
|
||||
not_null<Delegate*> delegate,
|
||||
const QString &title) {
|
||||
options.delegate = delegate;
|
||||
if (options.sourceName.isEmpty()) {
|
||||
options.sourceName = title;
|
||||
}
|
||||
Q_UNUSED(title);
|
||||
return options;
|
||||
}
|
||||
|
||||
[[nodiscard]] ViewerKind ResolveViewerKind(const OpenOptions &options) {
|
||||
return (options.viewerKind != ViewerKind::Auto)
|
||||
? options.viewerKind
|
||||
: options.sourcePath.isEmpty()
|
||||
? ViewerKind::InstantView
|
||||
: ViewerKind::LocalFile;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString SubtitleText(
|
||||
const OpenOptions &options,
|
||||
const QString &title) {
|
||||
if (!options.sourceName.trimmed().isEmpty()) {
|
||||
return options.sourceName.trimmed();
|
||||
}
|
||||
const auto host = QUrl(options.sourceUrl).host().trimmed();
|
||||
return !host.isEmpty() ? host : title.trimmed();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString OpenSourceLabel(ViewerKind kind) {
|
||||
return (kind == ViewerKind::InstantView)
|
||||
? tr::lng_iv_open_in_browser(tr::now)
|
||||
: tr::lng_markdown_preview_open_file(tr::now);
|
||||
}
|
||||
|
||||
[[nodiscard]] const style::icon *OpenSourceIcon(ViewerKind kind) {
|
||||
return (kind == ViewerKind::InstantView)
|
||||
? &st::menuIconIpAddress
|
||||
: &st::menuIconFile;
|
||||
}
|
||||
|
||||
[[nodiscard]] QVariant ExtendClickHandlerContext(
|
||||
QVariant context,
|
||||
const std::shared_ptr<Ui::Show> &show) {
|
||||
if (!show) {
|
||||
return context;
|
||||
} else if (!context.isValid()
|
||||
|| context.canConvert<ClickHandlerContext>()) {
|
||||
auto clickContext = context.isValid()
|
||||
? context.value<ClickHandlerContext>()
|
||||
: ClickHandlerContext();
|
||||
clickContext.show = show;
|
||||
return QVariant::fromValue(clickContext);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<QVariant> ResolveClickHandlerContextRef(
|
||||
const std::shared_ptr<QVariant> ¤t,
|
||||
const OpenOptions &options) {
|
||||
return options.clickHandlerContextRef
|
||||
? options.clickHandlerContextRef
|
||||
: (current ? current : std::make_shared<QVariant>());
|
||||
}
|
||||
|
||||
void ProcessZoomShortcut(not_null<Delegate*> delegate, QKeyEvent *event) {
|
||||
if (!(event->modifiers() & Qt::ControlModifier)) {
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Plus || event->key() == Qt::Key_Equal) {
|
||||
event->accept();
|
||||
delegate->ivSetZoom(delegate->ivZoom() + kZoomStep);
|
||||
} else if (event->key() == Qt::Key_Minus) {
|
||||
event->accept();
|
||||
delegate->ivSetZoom(delegate->ivZoom() - kZoomStep);
|
||||
} else if (event->key() == Qt::Key_0) {
|
||||
event->accept();
|
||||
delegate->ivSetZoom(0);
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenTarget {
|
||||
QString path;
|
||||
QString fragment;
|
||||
@@ -155,6 +239,10 @@ Controller::Controller(
|
||||
, _title(std::move(title))
|
||||
, _renderer(nullptr)
|
||||
, _options(PrepareOpenOptions(std::move(options), delegate, _title)) {
|
||||
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
||||
_clickHandlerContextRef,
|
||||
_options);
|
||||
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
||||
createWindow();
|
||||
}
|
||||
|
||||
@@ -169,6 +257,10 @@ Controller::Controller(
|
||||
, _title(std::move(title))
|
||||
, _renderer(renderer ? std::move(renderer) : std::make_shared<MathRenderer>())
|
||||
, _options(PrepareOpenOptions(std::move(options), delegate, _title)) {
|
||||
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
||||
_clickHandlerContextRef,
|
||||
_options);
|
||||
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
||||
createWindow();
|
||||
}
|
||||
|
||||
@@ -199,16 +291,68 @@ void Controller::update(
|
||||
_preparedContent = std::move(content);
|
||||
_title = std::move(title);
|
||||
_options = PrepareOpenOptions(std::move(options), _delegate, _title);
|
||||
if (_window) {
|
||||
_window->setTitle(_title);
|
||||
_window->setWindowTitle(_title);
|
||||
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
||||
_clickHandlerContextRef,
|
||||
_options);
|
||||
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
||||
if (_menu) {
|
||||
_menu = nullptr;
|
||||
_menuToggle->setForceRippled(false);
|
||||
}
|
||||
refreshTitle();
|
||||
createPreview();
|
||||
if (_window && _window->isActiveWindow() && _preview) {
|
||||
_preview->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::updateOptions(OpenOptions options) {
|
||||
const auto initialFragment = options.initialFragment;
|
||||
auto refreshed = PrepareOpenOptions(std::move(options), _delegate, _title);
|
||||
if (refreshed.sourceName.isEmpty()) {
|
||||
refreshed.sourceName = _options.sourceName;
|
||||
}
|
||||
if (refreshed.sourcePath.isEmpty()) {
|
||||
refreshed.sourcePath = _options.sourcePath;
|
||||
}
|
||||
if (refreshed.sourceUrl.isEmpty()) {
|
||||
refreshed.sourceUrl = _options.sourceUrl;
|
||||
}
|
||||
if (refreshed.viewerKind == ViewerKind::Auto) {
|
||||
refreshed.viewerKind = _options.viewerKind;
|
||||
}
|
||||
if (!refreshed.openSource) {
|
||||
refreshed.openSource = _options.openSource;
|
||||
}
|
||||
if (!refreshed.activateMedia) {
|
||||
refreshed.activateMedia = _options.activateMedia;
|
||||
}
|
||||
_options = std::move(refreshed);
|
||||
if (!_clickHandlerContextRef) {
|
||||
_clickHandlerContextRef = std::make_shared<QVariant>();
|
||||
}
|
||||
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
||||
if (_clickHandlerContextRef) {
|
||||
*_clickHandlerContextRef = ExtendClickHandlerContext(
|
||||
_options.clickHandlerContext,
|
||||
_show);
|
||||
}
|
||||
if (_menu) {
|
||||
_menu = nullptr;
|
||||
_menuToggle->setForceRippled(false);
|
||||
}
|
||||
refreshTitle();
|
||||
if (!initialFragment.isEmpty() && _preview) {
|
||||
const auto scrolled = ScrollMarkdownPreviewToAnchor(
|
||||
_preview.get(),
|
||||
initialFragment);
|
||||
static_cast<void>(scrolled);
|
||||
}
|
||||
if (_window && _window->isActiveWindow() && _preview) {
|
||||
_preview->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::active() const {
|
||||
return _window && _window->isActiveWindow();
|
||||
}
|
||||
@@ -223,11 +367,122 @@ void Controller::close() {
|
||||
_events.fire({ Event::Type::Close });
|
||||
}
|
||||
|
||||
void Controller::createPreview() {
|
||||
if (!_window) {
|
||||
ViewerKind Controller::viewerKind() const {
|
||||
return ResolveViewerKind(_options);
|
||||
}
|
||||
|
||||
QString Controller::subtitleText() const {
|
||||
return SubtitleText(_options, _title);
|
||||
}
|
||||
|
||||
bool Controller::canOpenSource() const {
|
||||
if (_options.openSource) {
|
||||
return true;
|
||||
}
|
||||
return (viewerKind() == ViewerKind::InstantView)
|
||||
? !_options.sourceUrl.isEmpty()
|
||||
: !_options.sourcePath.isEmpty();
|
||||
}
|
||||
|
||||
bool Controller::canShare() const {
|
||||
return static_cast<bool>(_options.share);
|
||||
}
|
||||
|
||||
void Controller::refreshTitle() {
|
||||
if (_window) {
|
||||
_window->setTitle(_title);
|
||||
_window->setWindowTitle(_title);
|
||||
}
|
||||
if (_subtitle) {
|
||||
_subtitle->setText(subtitleText());
|
||||
updateTitleGeometry(_window->body()->width());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::updateTitleGeometry(int newWidth) const {
|
||||
_subtitleWrap->setGeometry(0, 0, newWidth, st::ivSubtitleHeight);
|
||||
_subtitle->resizeToWidth(newWidth
|
||||
- st::ivSubtitleLeft
|
||||
- _menuToggle->width());
|
||||
_subtitle->moveToLeft(st::ivSubtitleLeft, st::ivSubtitleTop);
|
||||
_menuToggle->moveToRight(0, 0);
|
||||
if (_titleShadow) {
|
||||
_titleShadow->resizeToWidth(newWidth);
|
||||
_titleShadow->move(0, st::ivSubtitleHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::openSource() {
|
||||
if (_options.openSource) {
|
||||
_options.openSource();
|
||||
} else if (viewerKind() == ViewerKind::InstantView) {
|
||||
File::OpenUrl(_options.sourceUrl);
|
||||
} else {
|
||||
File::Launch(_options.sourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::showMenu() {
|
||||
if (!_window || _menu) {
|
||||
return;
|
||||
}
|
||||
const auto parent = _window->body();
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
_window.get(),
|
||||
st::popupMenuWithIcons);
|
||||
_menu->setDestroyedCallback(crl::guard(_window.get(), [
|
||||
this,
|
||||
menu = _menu.get()] {
|
||||
if (_menu == menu) {
|
||||
_menuToggle->setForceRippled(false);
|
||||
}
|
||||
}));
|
||||
_menuToggle->setForceRippled(true);
|
||||
|
||||
const auto action = _menu->addAction(
|
||||
OpenSourceLabel(viewerKind()),
|
||||
crl::guard(_window.get(), [=] {
|
||||
openSource();
|
||||
}),
|
||||
OpenSourceIcon(viewerKind()));
|
||||
action->setEnabled(canOpenSource());
|
||||
|
||||
if (canShare()) {
|
||||
_menu->addAction(
|
||||
tr::lng_iv_share(tr::now),
|
||||
crl::guard(_window.get(), [=, share = _options.share] {
|
||||
share(_show);
|
||||
}),
|
||||
&st::menuIconShare);
|
||||
}
|
||||
|
||||
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
||||
_menu->popup(_window->body()->mapToGlobal(
|
||||
QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
|
||||
}
|
||||
|
||||
void Controller::createLayerManager() {
|
||||
if (!_window || _layerManager) {
|
||||
return;
|
||||
}
|
||||
_layerManager = std::make_unique<Ui::LayerManager>(
|
||||
not_null{ _window->body().get() });
|
||||
_layerManager->setHideByBackgroundClick(false);
|
||||
_show = _layerManager->uiShow();
|
||||
}
|
||||
|
||||
void Controller::createPreview() {
|
||||
if (!_container) {
|
||||
return;
|
||||
}
|
||||
const auto parent = _container;
|
||||
auto options = _options;
|
||||
options.clickHandlerContextRef = _clickHandlerContextRef;
|
||||
options.clickHandlerContext = ExtendClickHandlerContext(
|
||||
std::move(options.clickHandlerContext),
|
||||
_show);
|
||||
if (options.clickHandlerContextRef) {
|
||||
*options.clickHandlerContextRef = options.clickHandlerContext;
|
||||
}
|
||||
const auto callback = [=](Event event) {
|
||||
_events.fire(std::move(event));
|
||||
};
|
||||
@@ -238,23 +493,48 @@ void Controller::createPreview() {
|
||||
std::move(*_preparedContent),
|
||||
_renderer,
|
||||
callback,
|
||||
_options)
|
||||
options)
|
||||
: CreateMarkdownPreviewWidget(
|
||||
parent,
|
||||
*_document,
|
||||
callback,
|
||||
_options);
|
||||
options);
|
||||
_preparedContent.reset();
|
||||
_preview->setGeometry(parent->rect());
|
||||
parent->sizeValue() | rpl::on_next([=](QSize size) {
|
||||
_preview->resize(size);
|
||||
}, _preview->lifetime());
|
||||
if (_titleShadow) {
|
||||
MarkdownPreviewScrollTopValue(
|
||||
_preview.get()
|
||||
) | rpl::on_next([=](int scrollTop) {
|
||||
_titleShadow->toggle(
|
||||
(scrollTop > 0),
|
||||
anim::type::normal);
|
||||
}, _preview->lifetime());
|
||||
}
|
||||
_preview->show();
|
||||
}
|
||||
|
||||
void Controller::createWindow() {
|
||||
_window = std::make_unique<Ui::RpWindow>();
|
||||
const auto window = _window.get();
|
||||
_subtitleWrap = std::make_unique<Ui::RpWidget>(window->body().get());
|
||||
_subtitle = std::make_unique<Ui::FlatLabel>(
|
||||
_subtitleWrap.get(),
|
||||
subtitleText(),
|
||||
st::ivSubtitle);
|
||||
_subtitle->setSelectable(true);
|
||||
_menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle);
|
||||
_menuToggle->setClickedCallback([=] {
|
||||
showMenu();
|
||||
});
|
||||
_subtitleWrap->paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_subtitleWrap.get()).fillRect(clip, st::windowBg);
|
||||
}, _subtitleWrap->lifetime());
|
||||
window->body()->widthValue() | rpl::on_next([=](int width) {
|
||||
updateTitleGeometry(width);
|
||||
}, _subtitle->lifetime());
|
||||
window->setTitle(_title);
|
||||
window->setWindowTitle(_title);
|
||||
window->setGeometry(_delegate->ivGeometry(window));
|
||||
@@ -267,43 +547,58 @@ void Controller::createWindow() {
|
||||
window->body()->paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(window->body().get()).fillRect(clip, st::windowBg);
|
||||
}, window->body()->lifetime());
|
||||
_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());
|
||||
rpl::combine(
|
||||
window->body()->sizeValue(),
|
||||
_subtitleWrap->heightValue()
|
||||
) | rpl::on_next([=](QSize size, int titleHeight) {
|
||||
_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
|
||||
{ 0, titleHeight, 0, 0 }));
|
||||
}, _container->lifetime());
|
||||
_container->paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_container).fillRect(clip, st::windowBg);
|
||||
}, _container->lifetime());
|
||||
_titleShadow.create(window->body().get());
|
||||
updateTitleGeometry(window->body()->width());
|
||||
|
||||
createLayerManager();
|
||||
createPreview();
|
||||
_container->show();
|
||||
|
||||
window->events() | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::Close) {
|
||||
close();
|
||||
} else if (e->type() == QEvent::KeyPress) {
|
||||
const auto event = static_cast<QKeyEvent*>(e.get());
|
||||
if (event->modifiers() & Qt::ControlModifier) {
|
||||
if (event->key() == Qt::Key_Plus
|
||||
|| event->key() == Qt::Key_Equal) {
|
||||
event->accept();
|
||||
_delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep);
|
||||
return;
|
||||
} else if (event->key() == Qt::Key_Minus) {
|
||||
event->accept();
|
||||
_delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep);
|
||||
return;
|
||||
} else if (event->key() == Qt::Key_0) {
|
||||
event->accept();
|
||||
_delegate->ivSetZoom(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event->key() == Qt::Key_Escape) {
|
||||
event->accept();
|
||||
close();
|
||||
}
|
||||
}
|
||||
}, window->lifetime());
|
||||
base::install_event_filter(window, qApp, [=](not_null<QEvent*> e) {
|
||||
if (e->type() != QEvent::ShortcutOverride || !window->isActiveWindow()) {
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
const auto event = static_cast<QKeyEvent*>(e.get());
|
||||
if (event->matches(QKeySequence::Close)) {
|
||||
close();
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
const auto previousAccepted = event->isAccepted();
|
||||
ProcessZoomShortcut(_delegate, event);
|
||||
return event->isAccepted() && !previousAccepted
|
||||
? base::EventFilterResult::Cancel
|
||||
: base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
window->show();
|
||||
}
|
||||
|
||||
std::unique_ptr<Controller> TryOpenLocalFile(
|
||||
not_null<Delegate*> delegate,
|
||||
const QString &path) {
|
||||
const QString &path,
|
||||
OpenOptions options) {
|
||||
const auto &limits = ParseLimitsForIv();
|
||||
const auto target = ParseOpenTarget(path);
|
||||
|
||||
@@ -360,9 +655,14 @@ std::unique_ptr<Controller> TryOpenLocalFile(
|
||||
).arg(parsed - validated
|
||||
).arg(target.path));
|
||||
|
||||
auto options = OpenOptions();
|
||||
if (options.sourceName.isEmpty()) {
|
||||
options.sourceName = source.name;
|
||||
}
|
||||
options.sourcePath = std::move(source.path);
|
||||
options.initialFragment = std::move(target.fragment);
|
||||
if (options.viewerKind == ViewerKind::Auto) {
|
||||
options.viewerKind = ViewerKind::LocalFile;
|
||||
}
|
||||
return std::make_unique<Controller>(
|
||||
delegate,
|
||||
std::move(parseResult.document),
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "iv/markdown/iv_markdown_document.h"
|
||||
#include "iv/markdown/iv_markdown_prepare.h"
|
||||
#include "ui/widgets/rp_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QVariant>
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class IconButton;
|
||||
class LayerManager;
|
||||
class PopupMenu;
|
||||
class Show;
|
||||
class FadeShadow;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Iv::Markdown {
|
||||
|
||||
@@ -30,6 +41,7 @@ public:
|
||||
MarkdownArticleContent content,
|
||||
QString title,
|
||||
OpenOptions options = {});
|
||||
void updateOptions(OpenOptions options = {});
|
||||
|
||||
[[nodiscard]] bool active() const;
|
||||
void minimize();
|
||||
@@ -43,7 +55,16 @@ public:
|
||||
private:
|
||||
void close();
|
||||
void createWindow();
|
||||
void createLayerManager();
|
||||
void createPreview();
|
||||
void updateTitleGeometry(int newWidth) const;
|
||||
void showMenu();
|
||||
void openSource();
|
||||
[[nodiscard]] ViewerKind viewerKind() const;
|
||||
[[nodiscard]] QString subtitleText() const;
|
||||
[[nodiscard]] bool canOpenSource() const;
|
||||
[[nodiscard]] bool canShare() const;
|
||||
void refreshTitle();
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
|
||||
@@ -52,7 +73,16 @@ private:
|
||||
QString _title;
|
||||
const std::shared_ptr<MathRenderer> _renderer;
|
||||
OpenOptions _options;
|
||||
std::shared_ptr<QVariant> _clickHandlerContextRef;
|
||||
std::unique_ptr<Ui::RpWindow> _window;
|
||||
std::unique_ptr<Ui::RpWidget> _subtitleWrap;
|
||||
std::unique_ptr<Ui::FlatLabel> _subtitle;
|
||||
object_ptr<Ui::IconButton> _menuToggle = { nullptr };
|
||||
object_ptr<Ui::FadeShadow> _titleShadow = { nullptr };
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
Ui::RpWidget *_container = nullptr;
|
||||
std::unique_ptr<Ui::LayerManager> _layerManager;
|
||||
std::shared_ptr<Ui::Show> _show;
|
||||
std::unique_ptr<Ui::RpWidget> _preview;
|
||||
|
||||
rpl::event_stream<Event> _events;
|
||||
@@ -63,6 +93,7 @@ private:
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Controller> TryOpenLocalFile(
|
||||
not_null<Delegate*> delegate,
|
||||
const QString &path);
|
||||
const QString &path,
|
||||
OpenOptions options = {});
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -37,6 +37,18 @@ constexpr auto kMaxVisualQuoteDepth = 3;
|
||||
return std::max(int((numerator + denominator - 1) / denominator), 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString StripOneTrailingNewline(QString text) {
|
||||
if (text.endsWith(u"\r\n"_q)) {
|
||||
text.chop(2);
|
||||
} else if (!text.isEmpty()) {
|
||||
const auto last = text.back();
|
||||
if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
|
||||
text.chop(1);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
[[nodiscard]] int FlowFormulaTextSize(
|
||||
PreparedBlockKind kind,
|
||||
int headingLevel,
|
||||
@@ -300,7 +312,7 @@ void AppendRichBlock(
|
||||
[[nodiscard]] PreparedBlock PrepareCodeBlock(const MarkdownNode &node) {
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::CodeBlock;
|
||||
block.text.text = node.text;
|
||||
block.text.text = StripOneTrailingNewline(node.text);
|
||||
block.codeLanguage = FirstInfoToken(node.info);
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,18 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
|
||||
SortEntities(&text->text);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString StripOneTrailingNewline(QString text) {
|
||||
if (text.endsWith(u"\r\n"_q)) {
|
||||
text.chop(2);
|
||||
} else if (!text.isEmpty()) {
|
||||
const auto last = text.back();
|
||||
if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
|
||||
text.chop(1);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
[[nodiscard]] PreparedBlock EmptyParagraphBlock() {
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::Paragraph;
|
||||
@@ -456,7 +468,7 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
|
||||
block.kind = PreparedBlockKind::CodeBlock;
|
||||
block.anchorId = std::move(anchorId);
|
||||
block.codeLanguage = qs(data.vlanguage()).trimmed();
|
||||
block.text.text = prepared.text.text;
|
||||
block.text.text = StripOneTrailingNewline(prepared.text.text);
|
||||
result->push_back(std::move(block));
|
||||
return true;
|
||||
}, [&](const MTPDpageBlockFooter &data) {
|
||||
|
||||
@@ -53,6 +53,12 @@ namespace {
|
||||
: PrepareTerminalFailureName(failure.terminal);
|
||||
}
|
||||
|
||||
[[nodiscard]] QVariant CurrentClickHandlerContext(const OpenOptions &options) {
|
||||
return options.clickHandlerContextRef
|
||||
? *options.clickHandlerContextRef
|
||||
: options.clickHandlerContext;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class MarkdownPreviewRoot final : public Ui::RpWidget {
|
||||
@@ -66,15 +72,16 @@ public:
|
||||
QWidget *parent,
|
||||
MarkdownArticleContent content,
|
||||
std::shared_ptr<MathRenderer> renderer,
|
||||
Fn<void(Event)> callback,
|
||||
const OpenOptions &options);
|
||||
Fn<void(Event)> callback,
|
||||
const OpenOptions &options);
|
||||
bool scrollToAnchor(const QString &anchorId);
|
||||
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
|
||||
|
||||
private:
|
||||
void setup();
|
||||
void prepareArticle();
|
||||
void activateLink(const PreparedLink &link, Qt::MouseButton button);
|
||||
void applyPreparedContent(MarkdownArticleContent prepared, int prepareMs);
|
||||
[[nodiscard]] bool scrollToAnchor(const QString &anchorId);
|
||||
void updateBodyVisibleTopBottom();
|
||||
void updateChildrenGeometry(QSize size);
|
||||
void updateFailureGeometry();
|
||||
@@ -141,6 +148,9 @@ void MarkdownPreviewRoot::setup() {
|
||||
_scroll->hide();
|
||||
if (_body) {
|
||||
_body->hide();
|
||||
_body->setClickHandlerContext(
|
||||
CurrentClickHandlerContext(_options),
|
||||
_options.clickHandlerContextRef);
|
||||
_body->setLinkActivationCallback([=](
|
||||
const PreparedLink &link,
|
||||
Qt::MouseButton button) {
|
||||
@@ -234,7 +244,9 @@ void MarkdownPreviewRoot::activateLink(
|
||||
}
|
||||
switch (link.kind) {
|
||||
case PreparedLinkKind::External:
|
||||
HiddenUrlClickHandler::Open(link.target);
|
||||
HiddenUrlClickHandler::Open(
|
||||
link.target,
|
||||
CurrentClickHandlerContext(_options));
|
||||
break;
|
||||
case PreparedLinkKind::Anchor:
|
||||
case PreparedLinkKind::Footnote:
|
||||
@@ -252,6 +264,7 @@ void MarkdownPreviewRoot::activateLink(
|
||||
_callback({
|
||||
.type = Event::Type::OpenFile,
|
||||
.url = std::move(target),
|
||||
.context = CurrentClickHandlerContext(_options),
|
||||
});
|
||||
} break;
|
||||
case PreparedLinkKind::RejectedRelative:
|
||||
@@ -332,6 +345,24 @@ bool MarkdownPreviewRoot::scrollToAnchor(const QString &anchorId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
rpl::producer<int> MarkdownPreviewRoot::scrollTopValue() const {
|
||||
return _scroll
|
||||
? _scroll->scrollTopValue()
|
||||
: rpl::single(0);
|
||||
}
|
||||
|
||||
bool ScrollMarkdownPreviewToAnchor(
|
||||
Ui::RpWidget *preview,
|
||||
const QString &anchorId) {
|
||||
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
|
||||
return root ? root->scrollToAnchor(anchorId) : false;
|
||||
}
|
||||
|
||||
rpl::producer<int> MarkdownPreviewScrollTopValue(Ui::RpWidget *preview) {
|
||||
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
|
||||
return root ? root->scrollTopValue() : rpl::single(0);
|
||||
}
|
||||
|
||||
void MarkdownPreviewRoot::updateBodyVisibleTopBottom() {
|
||||
if (_body) {
|
||||
const auto scrollTop = _scroll->scrollTop();
|
||||
|
||||
@@ -22,5 +22,10 @@ namespace Iv::Markdown {
|
||||
std::shared_ptr<MathRenderer> renderer,
|
||||
Fn<void(Event)> callback,
|
||||
const OpenOptions &options = {});
|
||||
bool ScrollMarkdownPreviewToAnchor(
|
||||
Ui::RpWidget *preview,
|
||||
const QString &anchorId);
|
||||
[[nodiscard]] rpl::producer<int> MarkdownPreviewScrollTopValue(
|
||||
Ui::RpWidget *preview);
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "iv/markdown/iv_markdown_view_widget.h"
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "core/credits_amount.h"
|
||||
#include "core/click_handler_types.h"
|
||||
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QContextMenuEvent>
|
||||
@@ -10,9 +14,9 @@
|
||||
#include <QtGui/QMouseEvent>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/integration.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
@@ -114,6 +118,13 @@ void MarkdownDocumentWidget::setMediaActivationCallback(
|
||||
_activateMedia = std::move(callback);
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::setClickHandlerContext(
|
||||
QVariant context,
|
||||
std::shared_ptr<QVariant> contextRef) {
|
||||
_clickHandlerContext = std::move(context);
|
||||
_clickHandlerContextRef = std::move(contextRef);
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::setArticle(
|
||||
std::shared_ptr<MarkdownArticle> article) {
|
||||
ClickHandler::clearActive(this);
|
||||
@@ -275,8 +286,9 @@ void MarkdownDocumentWidget::contextMenuEvent(QContextMenuEvent *e) {
|
||||
} else if (!contextText.empty()) {
|
||||
_contextMenu->addAction(
|
||||
tr::lng_context_copy_text(tr::now),
|
||||
[text = contextText] {
|
||||
[text = contextText, this] {
|
||||
TextUtilities::SetClipboardText(text);
|
||||
showToast(tr::lng_text_copied(tr::now));
|
||||
},
|
||||
&st::menuIconCopy);
|
||||
}
|
||||
@@ -549,9 +561,23 @@ TextForMimeData MarkdownDocumentWidget::getSelectedText() const {
|
||||
: TextForMimeData();
|
||||
}
|
||||
|
||||
QVariant MarkdownDocumentWidget::clickHandlerContext() const {
|
||||
return _clickHandlerContextRef
|
||||
? *_clickHandlerContextRef
|
||||
: _clickHandlerContext;
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::showToast(const QString &text) const {
|
||||
const auto context = clickHandlerContext().value<ClickHandlerContext>();
|
||||
if (context.show) {
|
||||
context.show->showToast(text);
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownDocumentWidget::copySelectedText() {
|
||||
if (const auto text = getSelectedText(); !text.empty()) {
|
||||
TextUtilities::SetClipboardText(text);
|
||||
showToast(tr::lng_text_copied(tr::now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +791,31 @@ MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
|
||||
if (state.preparedLink && _activateLink) {
|
||||
_activateLink(*state.preparedLink, button);
|
||||
} else {
|
||||
ActivateClickHandler(window(), activated, button);
|
||||
auto clickHandlerContext = this->clickHandlerContext();
|
||||
if (std::dynamic_pointer_cast<MonospaceClickHandler>(activated)) {
|
||||
const auto context = clickHandlerContext.value<ClickHandlerContext>();
|
||||
if (context.show) {
|
||||
auto sanitized = ClickHandlerContext();
|
||||
sanitized.itemId = context.itemId;
|
||||
sanitized.elementDelegate = context.elementDelegate;
|
||||
sanitized.botWebviewContext = context.botWebviewContext;
|
||||
sanitized.show = context.show;
|
||||
sanitized.mayShowConfirmation = context.mayShowConfirmation;
|
||||
sanitized.skipBotAutoLogin = context.skipBotAutoLogin;
|
||||
sanitized.botStartAutoSubmit = context.botStartAutoSubmit;
|
||||
sanitized.ignoreIv = context.ignoreIv;
|
||||
sanitized.dark = context.dark;
|
||||
sanitized.peer = context.peer;
|
||||
clickHandlerContext = QVariant::fromValue(sanitized);
|
||||
const auto handled = Ui::Integration::Instance().copyPreOnClick(
|
||||
clickHandlerContext);
|
||||
static_cast<void>(handled);
|
||||
}
|
||||
}
|
||||
auto context = ClickContext();
|
||||
context.button = button;
|
||||
context.other = std::move(clickHandlerContext);
|
||||
ActivateClickHandler(window(), activated, context);
|
||||
}
|
||||
} else if ((button == Qt::LeftButton || button == Qt::MiddleButton)
|
||||
&& state.mediaActivation.kind != MediaActivationKind::None
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <QtCore/QVariant>
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
@@ -31,6 +32,9 @@ public:
|
||||
std::function<void(const PreparedLink &, Qt::MouseButton)> callback);
|
||||
void setMediaActivationCallback(
|
||||
std::function<bool(const MediaActivation &, Qt::MouseButton)> callback);
|
||||
void setClickHandlerContext(
|
||||
QVariant context,
|
||||
std::shared_ptr<QVariant> contextRef = nullptr);
|
||||
void setArticle(std::shared_ptr<MarkdownArticle> article);
|
||||
void setZoom(int value);
|
||||
void refreshPalette();
|
||||
@@ -77,6 +81,8 @@ private:
|
||||
[[nodiscard]] MarkdownArticleSelection selectionFromHit(
|
||||
const MarkdownArticleHitTestResult &result) const;
|
||||
[[nodiscard]] TextForMimeData getSelectedText() const;
|
||||
[[nodiscard]] QVariant clickHandlerContext() const;
|
||||
void showToast(const QString &text) const;
|
||||
void copySelectedText();
|
||||
|
||||
void syncArticleVisibleTopBottom();
|
||||
@@ -102,6 +108,8 @@ private:
|
||||
std::unique_ptr<Ui::Text::QuotePaintCache> _blockquotePaintCache;
|
||||
std::function<void(const PreparedLink &, Qt::MouseButton)> _activateLink;
|
||||
std::function<bool(const MediaActivation &, Qt::MouseButton)> _activateMedia;
|
||||
QVariant _clickHandlerContext;
|
||||
std::shared_ptr<QVariant> _clickHandlerContextRef;
|
||||
MarkdownArticleSelection _selection;
|
||||
MarkdownArticleSelection _savedSelection;
|
||||
MarkdownArticleSelectionEndpoints _selectionEndpoints;
|
||||
|
||||
@@ -1382,6 +1382,18 @@ void ForEachPreparedLink(
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<const PreparedBlock*> CollectPreparedBlocksByKind(
|
||||
const std::vector<PreparedBlock> &blocks,
|
||||
PreparedBlockKind kind) {
|
||||
auto result = std::vector<const PreparedBlock*>();
|
||||
ForEachPreparedBlock(blocks, [&](const PreparedBlock &block) {
|
||||
if (block.kind == kind) {
|
||||
result.push_back(&block);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
struct InlineTextObjectMatch {
|
||||
EntityInText entity;
|
||||
InlineTextObjectEntity object;
|
||||
@@ -2263,6 +2275,96 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
|
||||
}
|
||||
}
|
||||
|
||||
void CheckCodeBlockTrailingNewlineTrim(bool *ok) {
|
||||
const auto markdownLabel = u"generated-code-block-trailing-newline"_q;
|
||||
const auto parsed = ParseMarkdownForIv(QByteArray(R"(```cpp
|
||||
alpha
|
||||
|
||||
```
|
||||
|
||||
beta
|
||||
)"), ParseOptions{ markdownLabel });
|
||||
Check(
|
||||
parsed.ok,
|
||||
markdownLabel + u" parse failed: "_q + parsed.error,
|
||||
ok);
|
||||
if (!parsed.ok) {
|
||||
return;
|
||||
}
|
||||
auto renderer = std::make_shared<MathRenderer>();
|
||||
const auto prepared = PrepareParsedDocumentForTest(
|
||||
parsed.document,
|
||||
markdownLabel,
|
||||
renderer);
|
||||
Check(
|
||||
!prepared.failure.failed(),
|
||||
markdownLabel + u" prepare failed: "_q
|
||||
+ PrepareFailureReason(prepared.failure),
|
||||
ok);
|
||||
if (prepared.failure.failed()) {
|
||||
return;
|
||||
}
|
||||
const auto markdownCodeBlocks = CollectPreparedBlocksByKind(
|
||||
prepared.blocks.blocks,
|
||||
PreparedBlockKind::CodeBlock);
|
||||
Check(
|
||||
markdownCodeBlocks.size() == 2,
|
||||
markdownLabel + u" code block count"_q,
|
||||
ok);
|
||||
if (markdownCodeBlocks.size() == 2) {
|
||||
Check(
|
||||
markdownCodeBlocks[0]->text.text == u"alpha\n"_q,
|
||||
markdownLabel + u" fenced block trims one newline"_q,
|
||||
ok);
|
||||
Check(
|
||||
markdownCodeBlocks[1]->text.text == u"beta"_q,
|
||||
markdownLabel + u" indented block trims one newline"_q,
|
||||
ok);
|
||||
}
|
||||
|
||||
const auto nativeLabel = u"native-iv-preformatted-trailing-newline"_q;
|
||||
auto nativeBlocks = QVector<MTPPageBlock>();
|
||||
nativeBlocks.push_back(MTP_pageBlockPreformatted(
|
||||
NativeIvText(u"single\n"_q),
|
||||
MTP_string("txt")));
|
||||
nativeBlocks.push_back(MTP_pageBlockPreformatted(
|
||||
NativeIvText(u"double\n\n"_q),
|
||||
MTP_string("txt")));
|
||||
auto nativeSource = NativeIvSource(std::move(nativeBlocks));
|
||||
const auto nativePrepared = TryPrepareNativeInstantView({
|
||||
.source = &nativeSource,
|
||||
});
|
||||
Check(
|
||||
nativePrepared.supported(),
|
||||
nativeLabel + u" prepare supported"_q,
|
||||
ok);
|
||||
Check(
|
||||
!nativePrepared.content.failure.failed(),
|
||||
nativeLabel + u" prepare failed"_q,
|
||||
ok);
|
||||
if (!nativePrepared.supported()
|
||||
|| nativePrepared.content.failure.failed()) {
|
||||
return;
|
||||
}
|
||||
const auto nativeCodeBlocks = CollectPreparedBlocksByKind(
|
||||
nativePrepared.content.blocks.blocks,
|
||||
PreparedBlockKind::CodeBlock);
|
||||
Check(
|
||||
nativeCodeBlocks.size() == 2,
|
||||
nativeLabel + u" code block count"_q,
|
||||
ok);
|
||||
if (nativeCodeBlocks.size() == 2) {
|
||||
Check(
|
||||
nativeCodeBlocks[0]->text.text == u"single"_q,
|
||||
nativeLabel + u" single trailing newline trimmed"_q,
|
||||
ok);
|
||||
Check(
|
||||
nativeCodeBlocks[1]->text.text == u"double\n"_q,
|
||||
nativeLabel + u" extra trailing newline preserved"_q,
|
||||
ok);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNativeInstantViewArticleCoverage(bool *ok) {
|
||||
const auto placeholderLabel = u"native-iv-placeholder-article"_q;
|
||||
auto placeholderBlocks = QVector<MTPPageBlock>();
|
||||
@@ -4208,6 +4310,7 @@ ThisIsALongUnbrokenStringToTestWrappingBehavior_ABCD1234EFGH5678IJKL
|
||||
|
||||
CheckInlineTextObjectPrepareCoverage(&ok);
|
||||
CheckNativeInstantViewPrepareCoverage(&ok);
|
||||
CheckCodeBlockTrailingNewlineTrim(&ok);
|
||||
CheckPrepareCoverage(markdownFixture, latexFixture, &ok);
|
||||
CheckPrepareLinkClassification(markdownFixture.path, &ok);
|
||||
CheckArticleRenderSmoke(markdownFixture, latexFixture, &ok);
|
||||
|
||||
Reference in New Issue
Block a user