mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 20:03:12 +00:00
1376 lines
36 KiB
C++
1376 lines
36 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "iv/markdown/iv_markdown_controller.h"
|
|
#include "base/event_filter.h"
|
|
#include "core/click_handler_types.h"
|
|
#include "core/credits_amount.h"
|
|
#include "core/file_utilities.h"
|
|
#include "iv/markdown/iv_markdown_article.h"
|
|
#include "iv/markdown/iv_markdown_parse.h"
|
|
#include "iv/markdown/iv_markdown_view.h"
|
|
#include "iv/iv_delegate_impl.h"
|
|
#include "iv/iv_search_controller.h"
|
|
#include "iv/iv_zoom_controls.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "ui/layers/layer_manager.h"
|
|
#include "ui/layers/show.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/dropdown_menu.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 "ui/ui_utility.h"
|
|
#include "logs.h"
|
|
|
|
#include "styles/palette.h"
|
|
#include "styles/style_iv.h"
|
|
#include "styles/style_menu_icons.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>
|
|
#include <memory>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace Iv::Markdown {
|
|
namespace {
|
|
|
|
constexpr auto kZoomStep = int(10);
|
|
constexpr auto kZoomDropdownHideDelay = 3 * crl::time(1000);
|
|
|
|
[[nodiscard]] OpenOptions PrepareOpenOptions(
|
|
OpenOptions options,
|
|
not_null<Delegate*> delegate,
|
|
const QString &title,
|
|
Fn<void()> zoomActivated) {
|
|
options.delegate = delegate;
|
|
options.zoomActivated = std::move(zoomActivated);
|
|
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>());
|
|
}
|
|
|
|
bool ProcessZoomShortcut(not_null<Delegate*> delegate, QKeyEvent *event) {
|
|
if (!(event->modifiers() & Qt::ControlModifier)) {
|
|
return false;
|
|
} else 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);
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct OpenTarget {
|
|
QString path;
|
|
QString fragment;
|
|
};
|
|
|
|
struct PageHistoryTarget {
|
|
uint64 pageId = 0;
|
|
QString sourceUrl;
|
|
QString hash;
|
|
};
|
|
|
|
[[nodiscard]] bool IsReadableLocalFile(const QFileInfo &info) {
|
|
return info.exists() && info.isFile() && info.isReadable();
|
|
}
|
|
|
|
struct ReadSource {
|
|
QString path;
|
|
QString name;
|
|
QByteArray bytes;
|
|
|
|
explicit operator bool() const {
|
|
return !path.isEmpty();
|
|
}
|
|
};
|
|
[[nodiscard]] ReadSource ReadLocalSource(
|
|
const QString &path,
|
|
const MarkdownParseLimits &limits) {
|
|
const auto info = QFileInfo(path);
|
|
auto name = info.fileName();
|
|
if (!IsReadableLocalFile(info) || !LooksLikeMarkdownFile(name)) {
|
|
return {};
|
|
} else if (info.size() > limits.maxSourceBytes) {
|
|
DEBUG_LOG(("Native Markdown IV: rejected local file too large: %1"
|
|
).arg(path));
|
|
return {};
|
|
}
|
|
auto file = QFile(path);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
return {};
|
|
}
|
|
auto data = file.read(limits.maxSourceBytes);
|
|
if (file.error() != QFileDevice::NoError || !file.atEnd()) {
|
|
DEBUG_LOG(("Native Markdown IV: could not read local file: %1"
|
|
).arg(path));
|
|
return {};
|
|
}
|
|
return {
|
|
.path = info.absoluteFilePath(),
|
|
.name = std::move(name),
|
|
.bytes = std::move(data),
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] QString NormalizeFragmentId(QString fragment) {
|
|
fragment = QString::fromUtf8(
|
|
QByteArray::fromPercentEncoding(fragment.toUtf8()));
|
|
fragment = fragment.trimmed().toLower();
|
|
while (fragment.startsWith(QChar('#'))) {
|
|
fragment.remove(0, 1);
|
|
}
|
|
return fragment;
|
|
}
|
|
|
|
[[nodiscard]] PageHistoryTarget ParsePageHistoryTarget(
|
|
uint64 pageId,
|
|
QString url) {
|
|
const auto hash = url.indexOf(QChar('#'));
|
|
return (hash < 0)
|
|
? PageHistoryTarget{
|
|
.pageId = pageId,
|
|
.sourceUrl = std::move(url),
|
|
}
|
|
: PageHistoryTarget{
|
|
.pageId = pageId,
|
|
.sourceUrl = url.mid(0, hash),
|
|
.hash = NormalizeFragmentId(url.mid(hash + 1)),
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] QString ComposePageHistoryUrl(const PageHistoryTarget &target) {
|
|
return target.hash.isEmpty()
|
|
? target.sourceUrl
|
|
: (target.sourceUrl + u"#"_q + target.hash);
|
|
}
|
|
|
|
[[nodiscard]] OpenTarget ParseOpenTarget(QString path) {
|
|
const auto direct = QFileInfo(path);
|
|
if (direct.exists()) {
|
|
return { path, QString() };
|
|
}
|
|
const auto hash = path.lastIndexOf(QChar('#'));
|
|
if (hash <= 0) {
|
|
return { path, QString() };
|
|
}
|
|
const auto candidate = path.mid(0, hash);
|
|
if (candidate.isEmpty()) {
|
|
return { path, QString() };
|
|
}
|
|
const auto info = QFileInfo(candidate);
|
|
return info.exists()
|
|
? OpenTarget{ candidate, NormalizeFragmentId(path.mid(hash + 1)) }
|
|
: OpenTarget{ path, QString() };
|
|
}
|
|
|
|
[[nodiscard]] bool HasPreviewableContent(const MarkdownNode &node) {
|
|
switch (node.kind) {
|
|
case NodeKind::Document:
|
|
case NodeKind::Unsupported:
|
|
break;
|
|
default:
|
|
return true;
|
|
}
|
|
return std::any_of(
|
|
node.children.begin(),
|
|
node.children.end(),
|
|
[](const MarkdownNode &child) {
|
|
return HasPreviewableContent(child);
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] bool AcceptsPreview(const PreparedDocument &document) {
|
|
return HasPreviewableContent(document.document)
|
|
|| !document.formulas.empty();
|
|
}
|
|
|
|
void LogDocumentWarnings(
|
|
const PreparedDocument &document,
|
|
const QString &path) {
|
|
for (const auto &warning : document.warnings) {
|
|
DEBUG_LOG(("Native Markdown IV: warning (%1): %2"
|
|
).arg(warning
|
|
).arg(path));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Controller::Controller(
|
|
not_null<Delegate*> delegate,
|
|
PreparedDocument document,
|
|
QString title,
|
|
OpenOptions options)
|
|
: _delegate(delegate)
|
|
, _document(std::make_shared<PreparedDocument>(std::move(document)))
|
|
, _title(std::move(title))
|
|
, _renderer(nullptr)
|
|
, _options(PrepareOpenOptions(
|
|
std::move(options),
|
|
delegate,
|
|
_title,
|
|
zoomActivatedCallback())) {
|
|
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
|
_clickHandlerContextRef,
|
|
_options);
|
|
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
|
createWindow();
|
|
}
|
|
|
|
Controller::Controller(
|
|
not_null<Delegate*> delegate,
|
|
MarkdownArticleContent content,
|
|
QString title,
|
|
std::shared_ptr<MathRenderer> renderer,
|
|
OpenOptions options)
|
|
: _delegate(delegate)
|
|
, _preparedContent(std::move(content))
|
|
, _title(std::move(title))
|
|
, _renderer(renderer ? std::move(renderer) : std::make_shared<MathRenderer>())
|
|
, _options(PrepareOpenOptions(
|
|
std::move(options),
|
|
delegate,
|
|
_title,
|
|
zoomActivatedCallback())) {
|
|
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
|
_clickHandlerContextRef,
|
|
_options);
|
|
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
|
createWindow();
|
|
}
|
|
|
|
Controller::~Controller() = default;
|
|
|
|
rpl::lifetime &Controller::lifetime() {
|
|
return _lifetime;
|
|
}
|
|
|
|
void Controller::activate() {
|
|
if (_window->isMinimized()) {
|
|
_window->showNormal();
|
|
} else if (_window->isHidden()) {
|
|
_window->show();
|
|
}
|
|
_window->raise();
|
|
_window->activateWindow();
|
|
_window->setFocus();
|
|
if (_preview) {
|
|
_preview->setFocus();
|
|
}
|
|
}
|
|
|
|
void Controller::update(
|
|
MarkdownArticleContent content,
|
|
QString title,
|
|
OpenOptions options) {
|
|
setContent(
|
|
std::move(content),
|
|
std::move(title),
|
|
std::move(options),
|
|
true);
|
|
}
|
|
|
|
void Controller::show(
|
|
MarkdownArticleContent content,
|
|
QString title,
|
|
OpenOptions options) {
|
|
setContent(
|
|
std::move(content),
|
|
std::move(title),
|
|
std::move(options),
|
|
false);
|
|
activate();
|
|
}
|
|
|
|
void Controller::setContent(
|
|
MarkdownArticleContent content,
|
|
QString title,
|
|
OpenOptions options,
|
|
bool preserveScroll) {
|
|
const auto scrollTop = (preserveScroll && _preview)
|
|
? MarkdownPreviewScrollTop(_preview.get())
|
|
: 0;
|
|
if (preserveScroll) {
|
|
saveCurrentHistoryScroll(scrollTop);
|
|
}
|
|
auto refreshed = PrepareOpenOptions(
|
|
std::move(options),
|
|
_delegate,
|
|
title,
|
|
zoomActivatedCallback());
|
|
if (preserveScroll && historyEnabled(refreshed)) {
|
|
auto activeIndex = -1;
|
|
if (_shownHistoryIndex >= 0
|
|
&& _shownHistoryIndex < int(_history.size())) {
|
|
activeIndex = _shownHistoryIndex;
|
|
} else if (_historyIndex >= 0
|
|
&& _historyIndex < int(_history.size())) {
|
|
activeIndex = _historyIndex;
|
|
}
|
|
if (activeIndex >= 0
|
|
&& sameHistoryPage(
|
|
_history[activeIndex],
|
|
refreshed.currentPageId,
|
|
refreshed.sourceUrl)) {
|
|
refreshed.initialFragment = _history[activeIndex].hash;
|
|
}
|
|
}
|
|
if (preserveScroll
|
|
&& _preview
|
|
&& historyEnabled(refreshed)) {
|
|
auto activeIndex = -1;
|
|
if (_shownHistoryIndex >= 0
|
|
&& _shownHistoryIndex < int(_history.size())) {
|
|
activeIndex = _shownHistoryIndex;
|
|
} else if (_historyIndex >= 0
|
|
&& _historyIndex < int(_history.size())) {
|
|
activeIndex = _historyIndex;
|
|
}
|
|
if (activeIndex >= 0
|
|
&& sameHistoryLocation(
|
|
_history[activeIndex],
|
|
refreshed.currentPageId,
|
|
refreshed.sourceUrl,
|
|
refreshed.initialFragment)) {
|
|
_title = std::move(title);
|
|
if (_menu) {
|
|
_menu = nullptr;
|
|
_menuToggle->setForceRippled(false);
|
|
}
|
|
const auto updated = updateExistingPreview(
|
|
std::move(content),
|
|
std::move(refreshed),
|
|
scrollTop);
|
|
Assert(updated);
|
|
if (updated) {
|
|
refreshTitle();
|
|
if (_window && _window->isActiveWindow() && _preview) {
|
|
_preview->setFocus();
|
|
}
|
|
updateHistoryButtons();
|
|
if (_search) {
|
|
_search->refresh();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (historyEnabled(refreshed)
|
|
&& _shownHistoryIndex >= 0
|
|
&& _shownHistoryIndex < int(_history.size())
|
|
&& !sameHistoryPage(
|
|
_history[_shownHistoryIndex],
|
|
refreshed.currentPageId,
|
|
refreshed.sourceUrl)) {
|
|
saveCurrentHistoryScroll();
|
|
}
|
|
if (historyEnabled(refreshed)) {
|
|
auto index = findHistoryEntry(
|
|
refreshed.currentPageId,
|
|
refreshed.sourceUrl,
|
|
refreshed.initialFragment);
|
|
if (index < 0) {
|
|
if (_historyIndex < 0) {
|
|
_history.push_back({});
|
|
index = 0;
|
|
} else {
|
|
_history.resize(_historyIndex + 1);
|
|
index = int(_history.size());
|
|
_history.push_back({});
|
|
}
|
|
_historyIndex = index;
|
|
} else if (index != _historyIndex) {
|
|
updateCurrentHistoryEntry(content, title, refreshed);
|
|
updateHistoryButtons();
|
|
return;
|
|
}
|
|
}
|
|
_preparedContent = std::move(content);
|
|
_title = std::move(title);
|
|
_options = refreshed;
|
|
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
|
_clickHandlerContextRef,
|
|
_options);
|
|
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
|
if (_menu) {
|
|
_menu = nullptr;
|
|
_menuToggle->setForceRippled(false);
|
|
}
|
|
refreshTitle();
|
|
createPreview();
|
|
if (preserveScroll && _preview) {
|
|
ScrollMarkdownPreviewToY(_preview.get(), scrollTop);
|
|
}
|
|
if (_window && _window->isActiveWindow() && _preview) {
|
|
_preview->setFocus();
|
|
}
|
|
updateHistoryButtons();
|
|
}
|
|
|
|
bool Controller::updateExistingPreview(
|
|
MarkdownArticleContent content,
|
|
OpenOptions options,
|
|
int scrollTop) {
|
|
Expects(_preview != nullptr);
|
|
|
|
_preparedContent = std::move(content);
|
|
_options = std::move(options);
|
|
_clickHandlerContextRef = ResolveClickHandlerContextRef(
|
|
_clickHandlerContextRef,
|
|
_options);
|
|
_options.clickHandlerContextRef = _clickHandlerContextRef;
|
|
auto previewOptions = _options;
|
|
previewOptions.clickHandlerContextRef = _clickHandlerContextRef;
|
|
previewOptions.clickHandlerContext = ExtendClickHandlerContext(
|
|
std::move(previewOptions.clickHandlerContext),
|
|
_show);
|
|
if (previewOptions.clickHandlerContextRef) {
|
|
*previewOptions.clickHandlerContextRef
|
|
= previewOptions.clickHandlerContext;
|
|
}
|
|
if (historyEnabled(_options)) {
|
|
updateCurrentHistoryEntry(*_preparedContent, _title, _options);
|
|
const auto index = findHistoryEntry(
|
|
_options.currentPageId,
|
|
_options.sourceUrl,
|
|
_options.initialFragment);
|
|
Assert(index >= 0);
|
|
if (index >= 0) {
|
|
_historyIndex = index;
|
|
_shownHistoryIndex = index;
|
|
}
|
|
} else {
|
|
_historyIndex = -1;
|
|
_shownHistoryIndex = -1;
|
|
}
|
|
previewOptions.initialFragment = QString();
|
|
if (!UpdateMarkdownPreviewWidget(
|
|
_preview.get(),
|
|
std::move(*_preparedContent),
|
|
previewOptions)) {
|
|
return false;
|
|
}
|
|
_preparedContent.reset();
|
|
ScrollMarkdownPreviewToY(_preview.get(), scrollTop);
|
|
return true;
|
|
}
|
|
|
|
void Controller::updateOptions(OpenOptions options) {
|
|
const auto initialFragment = options.initialFragment;
|
|
auto refreshed = PrepareOpenOptions(
|
|
std::move(options),
|
|
_delegate,
|
|
_title,
|
|
zoomActivatedCallback());
|
|
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.ivWebviewStorageId) {
|
|
refreshed.ivWebviewStorageId = _options.ivWebviewStorageId;
|
|
}
|
|
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();
|
|
}
|
|
|
|
void Controller::showJoinedTooltip() {
|
|
if (_show) {
|
|
_show->showToast(tr::lng_action_you_joined(tr::now));
|
|
}
|
|
}
|
|
|
|
void Controller::minimize() {
|
|
if (_window) {
|
|
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
|
|
}
|
|
}
|
|
|
|
void Controller::close() {
|
|
_events.fire({ Event::Type::Close });
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
bool Controller::historyEnabled(const OpenOptions &options) const {
|
|
return (ResolveViewerKind(options) == ViewerKind::InstantView)
|
|
&& (options.currentPageId != 0);
|
|
}
|
|
|
|
bool Controller::sameHistoryPage(
|
|
const HistoryEntry &entry,
|
|
uint64 pageId,
|
|
const QString &sourceUrl) const {
|
|
return entry.pageId
|
|
? (entry.pageId == pageId)
|
|
: (!sourceUrl.isEmpty() && entry.sourceUrl == sourceUrl);
|
|
}
|
|
|
|
bool Controller::sameCurrentPage(uint64 pageId, const QString &sourceUrl) const {
|
|
return (pageId != 0
|
|
&& _options.currentPageId != 0
|
|
&& pageId == _options.currentPageId)
|
|
|| (!sourceUrl.isEmpty()
|
|
&& !_options.sourceUrl.isEmpty()
|
|
&& sourceUrl == _options.sourceUrl);
|
|
}
|
|
|
|
bool Controller::sameHistoryLocation(
|
|
const HistoryEntry &entry,
|
|
uint64 pageId,
|
|
const QString &sourceUrl,
|
|
const QString &hash) const {
|
|
return sameHistoryPage(entry, pageId, sourceUrl)
|
|
&& (entry.hash == hash);
|
|
}
|
|
|
|
int Controller::findHistoryEntry(
|
|
uint64 pageId,
|
|
const QString &sourceUrl,
|
|
const QString &hash) const {
|
|
if (_historyIndex >= 0
|
|
&& _historyIndex < int(_history.size())
|
|
&& sameHistoryLocation(
|
|
_history[_historyIndex],
|
|
pageId,
|
|
sourceUrl,
|
|
hash)) {
|
|
return _historyIndex;
|
|
}
|
|
if (_shownHistoryIndex >= 0
|
|
&& _shownHistoryIndex < int(_history.size())
|
|
&& _shownHistoryIndex != _historyIndex
|
|
&& sameHistoryLocation(
|
|
_history[_shownHistoryIndex],
|
|
pageId,
|
|
sourceUrl,
|
|
hash)) {
|
|
return _shownHistoryIndex;
|
|
}
|
|
for (auto i = 0, count = int(_history.size()); i != count; ++i) {
|
|
if (sameHistoryLocation(_history[i], pageId, sourceUrl, hash)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void Controller::saveCurrentHistoryScroll(std::optional<int> scrollTop) {
|
|
const auto index = (_shownHistoryIndex >= 0)
|
|
? _shownHistoryIndex
|
|
: _historyIndex;
|
|
if (!_preview
|
|
|| index < 0
|
|
|| index >= int(_history.size())) {
|
|
return;
|
|
}
|
|
_history[index].scrollTop = scrollTop.value_or(
|
|
MarkdownPreviewScrollTop(_preview.get()));
|
|
}
|
|
|
|
void Controller::updateCurrentHistoryEntry(
|
|
const MarkdownArticleContent &content,
|
|
const QString &title,
|
|
const OpenOptions &options) {
|
|
if (!historyEnabled(options)) {
|
|
return;
|
|
}
|
|
auto index = findHistoryEntry(
|
|
options.currentPageId,
|
|
options.sourceUrl,
|
|
options.initialFragment);
|
|
if (index < 0) {
|
|
if (_historyIndex < 0) {
|
|
_history.push_back({});
|
|
index = 0;
|
|
_historyIndex = 0;
|
|
} else {
|
|
_history.resize(_historyIndex + 1);
|
|
index = int(_history.size());
|
|
_history.push_back({});
|
|
_historyIndex = index;
|
|
}
|
|
}
|
|
auto &entry = _history[index];
|
|
entry.pageId = options.currentPageId;
|
|
entry.sourceUrl = options.sourceUrl;
|
|
entry.hash = options.initialFragment;
|
|
entry.title = title;
|
|
entry.preparedContent = std::make_shared<MarkdownArticleContent>(content);
|
|
entry.options = options;
|
|
}
|
|
|
|
void Controller::handleOpenPage(Event event) {
|
|
if (!historyEnabled(_options)) {
|
|
_events.fire(std::move(event));
|
|
return;
|
|
}
|
|
const auto target = ParsePageHistoryTarget(event.webpageId, event.url);
|
|
if (!target.pageId || target.sourceUrl.isEmpty()) {
|
|
_events.fire(std::move(event));
|
|
return;
|
|
}
|
|
const auto currentIndex = (_historyIndex >= 0
|
|
&& _historyIndex < int(_history.size()))
|
|
? _historyIndex
|
|
: -1;
|
|
const auto current = (currentIndex >= 0) ? &_history[currentIndex] : nullptr;
|
|
const auto samePage = sameCurrentPage(target.pageId, target.sourceUrl)
|
|
|| (current
|
|
&& sameHistoryPage(*current, target.pageId, target.sourceUrl));
|
|
if (samePage) {
|
|
if (target.hash.isEmpty()) {
|
|
if (_preview) {
|
|
ScrollMarkdownPreviewToY(
|
|
_preview.get(),
|
|
0,
|
|
MarkdownPreviewScrollMode::Animated);
|
|
}
|
|
return;
|
|
}
|
|
if (_preview
|
|
&& ScrollMarkdownPreviewToAnchor(
|
|
_preview.get(),
|
|
target.hash,
|
|
MarkdownPreviewScrollMode::Animated)) {
|
|
return;
|
|
}
|
|
DEBUG_LOG(("Native Markdown IV: unresolved anchor: %1"
|
|
).arg(target.hash));
|
|
return;
|
|
}
|
|
saveCurrentHistoryScroll();
|
|
auto targetIndex = -1;
|
|
if ((_historyIndex + 1) < int(_history.size())
|
|
&& sameHistoryLocation(
|
|
_history[_historyIndex + 1],
|
|
target.pageId,
|
|
target.sourceUrl,
|
|
target.hash)) {
|
|
targetIndex = _historyIndex + 1;
|
|
} else {
|
|
auto options = _options;
|
|
options.sourceUrl = target.sourceUrl;
|
|
options.initialFragment = target.hash;
|
|
options.currentPageId = target.pageId;
|
|
_history.resize(_historyIndex + 1);
|
|
_history.push_back({
|
|
.pageId = target.pageId,
|
|
.sourceUrl = target.sourceUrl,
|
|
.hash = target.hash,
|
|
.options = std::move(options),
|
|
});
|
|
targetIndex = int(_history.size()) - 1;
|
|
}
|
|
auto &entry = _history[targetIndex];
|
|
entry.pageId = target.pageId;
|
|
entry.sourceUrl = target.sourceUrl;
|
|
entry.hash = target.hash;
|
|
entry.options.sourceUrl = target.sourceUrl;
|
|
entry.options.initialFragment = target.hash;
|
|
entry.options.currentPageId = target.pageId;
|
|
_historyIndex = targetIndex;
|
|
updateHistoryButtons();
|
|
if (!showHistoryEntry(targetIndex)) {
|
|
event.url = ComposePageHistoryUrl(target);
|
|
_events.fire(std::move(event));
|
|
}
|
|
}
|
|
|
|
bool Controller::showHistoryEntry(int index) {
|
|
if (index < 0 || index >= int(_history.size())) {
|
|
return false;
|
|
}
|
|
const auto &entry = _history[index];
|
|
if (!entry.hydrated()) {
|
|
return false;
|
|
}
|
|
auto options = entry.options;
|
|
options.sourceUrl = entry.sourceUrl;
|
|
options.initialFragment = entry.hash;
|
|
options.currentPageId = entry.pageId;
|
|
setContent(*entry.preparedContent, entry.title, std::move(options), false);
|
|
if (_preview) {
|
|
ScrollMarkdownPreviewToY(_preview.get(), entry.scrollTop);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Controller::stepHistory(int delta) {
|
|
const auto index = _historyIndex + delta;
|
|
if (index < 0 || index >= int(_history.size())) {
|
|
return;
|
|
}
|
|
saveCurrentHistoryScroll();
|
|
_historyIndex = index;
|
|
updateHistoryButtons();
|
|
if (!showHistoryEntry(index)) {
|
|
const auto &entry = _history[index];
|
|
_events.fire({
|
|
.type = Event::Type::OpenPage,
|
|
.webpageId = entry.pageId,
|
|
.url = ComposePageHistoryUrl(PageHistoryTarget{
|
|
.pageId = entry.pageId,
|
|
.sourceUrl = entry.sourceUrl,
|
|
.hash = entry.hash,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
|
|
void Controller::updateHistoryButtons() {
|
|
if (!_back || !_forward) {
|
|
return;
|
|
}
|
|
const auto canGoBack = (_historyIndex > 0);
|
|
const auto canGoForward = (_historyIndex >= 0)
|
|
&& ((_historyIndex + 1) < int(_history.size()));
|
|
const auto updateButton = [](
|
|
Ui::FadeWrapScaled<Ui::IconButton> *button,
|
|
bool enabled,
|
|
const style::icon &disabledIcon) {
|
|
Expects(button != nullptr);
|
|
button->entity()->setDisabled(!enabled);
|
|
button->entity()->setIconOverride(
|
|
enabled ? nullptr : &disabledIcon,
|
|
enabled ? nullptr : &disabledIcon);
|
|
button->entity()->setPointerCursor(enabled);
|
|
button->setAttribute(Qt::WA_TransparentForMouseEvents, !enabled);
|
|
if (!enabled) {
|
|
button->entity()->clearState();
|
|
}
|
|
};
|
|
_back->toggle(canGoBack || canGoForward, anim::type::normal);
|
|
_forward->toggle(canGoForward, anim::type::normal);
|
|
updateButton(_back, canGoBack, st::ivBackIconDisabled);
|
|
updateButton(_forward, canGoForward, st::ivForwardIconDisabled);
|
|
if (_window) {
|
|
updateTitleGeometry(_window->body()->width());
|
|
}
|
|
}
|
|
|
|
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);
|
|
const auto progressBack = _back
|
|
? _subtitleBackShift.value(_back->toggled() ? 1. : 0.)
|
|
: 0.;
|
|
const auto progressForward = _forward
|
|
? _subtitleForwardShift.value(_forward->toggled() ? 1. : 0.)
|
|
: 0.;
|
|
const auto backAdded = (_back
|
|
? (_back->width() + st::ivSubtitleSkip - st::ivSubtitleLeft)
|
|
: 0);
|
|
const auto forwardAdded = _forward ? _forward->width() : 0;
|
|
const auto left = int(st::ivSubtitleLeft
|
|
+ anim::interpolate(0, backAdded, progressBack)
|
|
+ anim::interpolate(0, forwardAdded, progressForward));
|
|
_subtitle->resizeToWidth(newWidth - left - _menuToggle->width());
|
|
_subtitle->moveToLeft(left, st::ivSubtitleTop);
|
|
if (_back) {
|
|
_back->moveToLeft(0, 0);
|
|
}
|
|
if (_forward) {
|
|
_forward->moveToLeft(
|
|
_back ? int(_back->width() * progressBack) : 0,
|
|
0);
|
|
}
|
|
_menuToggle->moveToRight(0, 0);
|
|
_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;
|
|
}
|
|
hideZoomDropdown();
|
|
_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 hasOpenSource = canOpenSource();
|
|
if (hasOpenSource) {
|
|
_menu->addAction(
|
|
OpenSourceLabel(viewerKind()),
|
|
crl::guard(_window.get(), [=] {
|
|
openSource();
|
|
}),
|
|
OpenSourceIcon(viewerKind()));
|
|
}
|
|
|
|
if (canShare()) {
|
|
_menu->addAction(
|
|
tr::lng_iv_share(tr::now),
|
|
crl::guard(_window.get(), [=, share = _options.share] {
|
|
share(_show);
|
|
}),
|
|
&st::menuIconShare);
|
|
}
|
|
|
|
if (hasOpenSource || canShare()) {
|
|
_menu->addSeparator();
|
|
}
|
|
_menu->addAction(CreateZoomMenuAction(_menu->menu(), _delegate));
|
|
|
|
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
|
_menu->popup(_window->body()->mapToGlobal(
|
|
QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
|
|
}
|
|
|
|
void Controller::createZoomDropdown() {
|
|
_zoomDropdown = base::make_unique_q<Ui::DropdownMenu>(
|
|
_window->body().get(),
|
|
st::dropdownMenuWithIcons);
|
|
_zoomDropdown->addAction(
|
|
CreateZoomMenuAction(_zoomDropdown->menu(), _delegate));
|
|
_zoomDropdown->events() | rpl::on_next([=](not_null<QEvent*> e) {
|
|
if (e->type() == QEvent::Enter) {
|
|
_zoomDropdownHideTimer.cancel();
|
|
}
|
|
}, _zoomDropdown->lifetime());
|
|
_zoomDropdownHideTimer.setCallback([=] {
|
|
if (_zoomDropdown) {
|
|
_zoomDropdown->hideAnimated();
|
|
}
|
|
});
|
|
}
|
|
|
|
void Controller::showZoomDropdown() {
|
|
if (!_window || _menu) {
|
|
return;
|
|
}
|
|
if (!_zoomDropdown) {
|
|
createZoomDropdown();
|
|
}
|
|
updateZoomDropdownGeometry();
|
|
_zoomDropdown->raise();
|
|
_zoomDropdown->showAnimated(Ui::PanelAnimation::Origin::TopRight);
|
|
if (!_zoomDropdown->underMouse()) {
|
|
_zoomDropdownHideTimer.callOnce(kZoomDropdownHideDelay);
|
|
}
|
|
}
|
|
|
|
void Controller::hideZoomDropdown() {
|
|
_zoomDropdownHideTimer.cancel();
|
|
if (_zoomDropdown) {
|
|
_zoomDropdown->hideFast();
|
|
}
|
|
}
|
|
|
|
void Controller::updateZoomDropdownGeometry() {
|
|
if (!_zoomDropdown || !_window) {
|
|
return;
|
|
}
|
|
_zoomDropdown->resizeToContent();
|
|
Ui::SendPendingMoveResizeEvents(_zoomDropdown.get());
|
|
const auto &padding = st::dropdownMenuWithIcons.wrap.padding;
|
|
const auto visibleHeight = _zoomDropdown->height()
|
|
- padding.top()
|
|
- padding.bottom();
|
|
const auto visibleRight = _window->body()->width()
|
|
- st::ivMenuToggle.width
|
|
+ st::ivZoomDropdownPosition.x();
|
|
const auto visibleTop = ((st::ivSubtitleHeight - visibleHeight) / 2)
|
|
+ st::ivZoomDropdownPosition.y();
|
|
_zoomDropdown->move(
|
|
visibleRight + padding.right() - _zoomDropdown->width(),
|
|
visibleTop - padding.top());
|
|
}
|
|
|
|
Fn<void()> Controller::zoomActivatedCallback() {
|
|
return crl::guard(this, [=] {
|
|
showZoomDropdown();
|
|
});
|
|
}
|
|
|
|
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) {
|
|
switch (event.type) {
|
|
case Event::Type::OpenPage:
|
|
handleOpenPage(std::move(event));
|
|
break;
|
|
case Event::Type::Close:
|
|
case Event::Type::Quit:
|
|
case Event::Type::OpenFile:
|
|
case Event::Type::Report:
|
|
_events.fire(std::move(event));
|
|
break;
|
|
}
|
|
};
|
|
_preview = nullptr;
|
|
const auto preparedForHistory = (_preparedContent && historyEnabled(options))
|
|
? std::make_shared<MarkdownArticleContent>(*_preparedContent)
|
|
: nullptr;
|
|
_preview = _preparedContent
|
|
? CreateMarkdownPreviewWidget(
|
|
parent,
|
|
std::move(*_preparedContent),
|
|
_renderer,
|
|
callback,
|
|
options)
|
|
: CreateMarkdownPreviewWidget(
|
|
parent,
|
|
*_document,
|
|
callback,
|
|
options);
|
|
_preparedContent.reset();
|
|
if (preparedForHistory) {
|
|
updateCurrentHistoryEntry(*preparedForHistory, _title, options);
|
|
_shownHistoryIndex = _historyIndex;
|
|
} else {
|
|
_shownHistoryIndex = -1;
|
|
}
|
|
_preview->setGeometry(parent->rect());
|
|
parent->sizeValue() | rpl::on_next([=](QSize size) {
|
|
_preview->resize(size);
|
|
}, _preview->lifetime());
|
|
|
|
_titleShadow->show(anim::type::instant);
|
|
|
|
_preview->show();
|
|
updateHistoryButtons();
|
|
if (_search) {
|
|
_search->refresh();
|
|
}
|
|
}
|
|
|
|
void Controller::createSearchController() {
|
|
auto host = SearchHost{
|
|
.ready = [=] { return _preview != nullptr; },
|
|
.sources = [=] {
|
|
return MarkdownPreviewSearchSources(_preview.get());
|
|
},
|
|
.applyMatches = [=](
|
|
std::vector<MarkdownArticleSearchMatch> matches,
|
|
int current) {
|
|
SetMarkdownPreviewSearchMatches(
|
|
_preview.get(),
|
|
std::move(matches),
|
|
current);
|
|
},
|
|
.scrollToSegment = [=](int segmentIndex) {
|
|
ScrollMarkdownPreviewToSegment(
|
|
_preview.get(),
|
|
segmentIndex,
|
|
0);
|
|
},
|
|
.expandDetails = [=](const QString &anchorId) {
|
|
return ExpandMarkdownPreviewDetails(
|
|
_preview.get(),
|
|
anchorId);
|
|
},
|
|
.focusContent = [=] {
|
|
_preview->setFocus();
|
|
},
|
|
};
|
|
_search = std::make_unique<SearchController>(
|
|
_window->body().get(),
|
|
_window->body()->widthValue(),
|
|
std::move(host));
|
|
_searchBarHeight = _search->barHeightValue();
|
|
_search->moveBar(0, st::ivSubtitleHeight);
|
|
_search->raiseBar();
|
|
_titleShadow->raise();
|
|
}
|
|
|
|
void Controller::toggleSearchBar() {
|
|
if (!_window) {
|
|
return;
|
|
} else if (_search && _search->shown()) {
|
|
_search->hide();
|
|
return;
|
|
}
|
|
if (!_search) {
|
|
createSearchController();
|
|
}
|
|
_search->toggle();
|
|
}
|
|
|
|
void Controller::createWindow() {
|
|
_window = std::make_unique<Ui::RpWindow>();
|
|
const auto window = _window.get();
|
|
_titleShadow.create(window->body().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();
|
|
});
|
|
_back.create(
|
|
_subtitleWrap.get(),
|
|
object_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivBack));
|
|
_back->entity()->setClickedCallback([=] {
|
|
stepHistory(-1);
|
|
});
|
|
_forward.create(
|
|
_subtitleWrap.get(),
|
|
object_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivForward));
|
|
_forward->entity()->setClickedCallback([=] {
|
|
stepHistory(1);
|
|
});
|
|
_back->toggledValue(
|
|
) | rpl::on_next([=](bool toggled) {
|
|
_subtitleBackShift.start(
|
|
[=] { updateTitleGeometry(_window->body()->width()); },
|
|
toggled ? 0. : 1.,
|
|
toggled ? 1. : 0.,
|
|
st::fadeWrapDuration);
|
|
}, _back->lifetime());
|
|
_back->hide(anim::type::instant);
|
|
_forward->toggledValue(
|
|
) | rpl::on_next([=](bool toggled) {
|
|
_subtitleForwardShift.start(
|
|
[=] { updateTitleGeometry(_window->body()->width()); },
|
|
toggled ? 0. : 1.,
|
|
toggled ? 1. : 0.,
|
|
st::fadeWrapDuration);
|
|
}, _forward->lifetime());
|
|
_forward->hide(anim::type::instant);
|
|
_subtitleBackShift.stop();
|
|
_subtitleForwardShift.stop();
|
|
_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);
|
|
updateZoomDropdownGeometry();
|
|
}, _subtitle->lifetime());
|
|
window->setTitle(_title);
|
|
window->setWindowTitle(_title);
|
|
window->setGeometry(_delegate->ivGeometry(window));
|
|
window->setMinimumSize({ st::ivWidthMin, st::ivHeightMin });
|
|
window->geometryValue(
|
|
) | rpl::distinct_until_changed(
|
|
) | rpl::skip(1) | rpl::on_next([=] {
|
|
_delegate->ivSaveGeometry(window);
|
|
}, window->lifetime());
|
|
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(),
|
|
_searchBarHeight.value()
|
|
) | rpl::on_next([=](QSize size, int titleHeight, int barHeight) {
|
|
_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
|
|
{ 0, titleHeight + barHeight, 0, 0 }));
|
|
}, _container->lifetime());
|
|
_container->paintRequest() | rpl::on_next([=](QRect clip) {
|
|
QPainter(_container).fillRect(clip, st::windowBg);
|
|
}, _container->lifetime());
|
|
updateTitleGeometry(window->body()->width());
|
|
|
|
createLayerManager();
|
|
createPreview();
|
|
_container->show();
|
|
updateHistoryButtons();
|
|
|
|
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->key() == Qt::Key_Escape) {
|
|
event->accept();
|
|
if (_search && _search->shown()) {
|
|
_search->hide();
|
|
} else {
|
|
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());
|
|
const auto previousAccepted = event->isAccepted();
|
|
if (ProcessZoomShortcut(_delegate, event)) {
|
|
showZoomDropdown();
|
|
}
|
|
if (!event->isAccepted()
|
|
&& (event->modifiers() & Qt::ControlModifier)
|
|
&& event->key() == Qt::Key_F) {
|
|
event->accept();
|
|
toggleSearchBar();
|
|
}
|
|
return event->isAccepted() && !previousAccepted
|
|
? base::EventFilterResult::Cancel
|
|
: base::EventFilterResult::Continue;
|
|
});
|
|
|
|
_titleShadow->raise();
|
|
window->show();
|
|
}
|
|
|
|
std::unique_ptr<Controller> TryOpenLocalFile(
|
|
not_null<Delegate*> delegate,
|
|
const QString &path,
|
|
OpenOptions options) {
|
|
const auto &limits = ParseLimitsForIv();
|
|
const auto target = ParseOpenTarget(path);
|
|
|
|
const auto source = ReadLocalSource(target.path, limits);
|
|
if (!source) {
|
|
return nullptr;
|
|
}
|
|
const auto &bytes = source.bytes;
|
|
const auto &fallbackTitle = source.name;
|
|
|
|
const auto start = crl::now();
|
|
auto validateResult = ValidateMarkdownSourceForIv(
|
|
bytes,
|
|
ParseOptions{ fallbackTitle });
|
|
const auto validated = crl::now();
|
|
if (!validateResult.ok) {
|
|
DEBUG_LOG(("Native Markdown IV: "
|
|
"source validation failure (%1, %2 ms): %3"
|
|
).arg(validateResult.error
|
|
).arg(validated - start
|
|
).arg(target.path));
|
|
return nullptr;
|
|
}
|
|
|
|
auto parseResult = ParseMarkdownForIv(std::move(validateResult.source));
|
|
const auto parsed = crl::now();
|
|
if (!parseResult.ok) {
|
|
const auto &error = parseResult.error;
|
|
if (error.startsWith(u"cmark-"_q)) {
|
|
DEBUG_LOG(("Native Markdown IV: "
|
|
"cmark parse failure (%1, %2 ms): %3"
|
|
).arg(error
|
|
).arg(parsed - validated
|
|
).arg(target.path));
|
|
} else {
|
|
DEBUG_LOG(("Native Markdown IV: parse failure (%1, %2 ms): %3"
|
|
).arg(error
|
|
).arg(parsed - validated
|
|
).arg(target.path));
|
|
}
|
|
return nullptr;
|
|
} else if (!AcceptsPreview(parseResult.document)) {
|
|
DEBUG_LOG(("Native Markdown IV: "
|
|
"unsupported or empty document (%1 ms): %2"
|
|
).arg(crl::now() - parsed
|
|
).arg(target.path));
|
|
return nullptr;
|
|
}
|
|
LogDocumentWarnings(parseResult.document, target.path);
|
|
|
|
DEBUG_LOG(("Native Markdown IV: "
|
|
"opening as native Markdown IV (%1 ms validate, %2 ms parse): %3"
|
|
).arg(validated - start
|
|
).arg(parsed - validated
|
|
).arg(target.path));
|
|
|
|
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),
|
|
(parseResult.document.title.trimmed().isEmpty()
|
|
? fallbackTitle
|
|
: parseResult.document.title.trimmed()),
|
|
std::move(options));
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|