mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 22:27:35 +00:00
Ladybird: Move Qt-specific classes and functions to a Qt subdirectory
This will help a lot with developing chromes for different UI frameworks where we can see which helper classes and processes are really using Qt vs just using it to get at helper data. As a bonus, remove Qt dependency from WebDriver.
This commit is contained in:
parent
ccaa423372
commit
391beef707
53 changed files with 160 additions and 157 deletions
198
Ladybird/Qt/AndroidPlatform.cpp
Normal file
198
Ladybird/Qt/AndroidPlatform.cpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Platform.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <LibArchive/Tar.h>
|
||||
#include <LibArchive/TarStream.h>
|
||||
#include <LibCompress/Gzip.h>
|
||||
#include <LibCore/Directory.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibMain/Main.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QJniObject>
|
||||
#include <QSslSocket>
|
||||
|
||||
#ifndef AK_OS_ANDROID
|
||||
# error This file is for Android only, check CMake config!
|
||||
#endif
|
||||
|
||||
// HACK ALERT, we need to include LibMain manually here because the Qt build system doesn't include LibMain.a in the actual executable,
|
||||
// nor include it in libladybird_<arch>.so
|
||||
#include <LibMain/Main.cpp> // NOLINT(bugprone-suspicious-include)
|
||||
|
||||
extern DeprecatedString s_serenity_resource_root;
|
||||
|
||||
void android_platform_init();
|
||||
static void extract_ladybird_resources();
|
||||
static ErrorOr<void> extract_tar_archive(String archive_file, DeprecatedString output_directory);
|
||||
|
||||
void android_platform_init()
|
||||
{
|
||||
qDebug() << "Device supports OpenSSL: " << QSslSocket::supportsSsl();
|
||||
|
||||
QJniObject res = QJniObject::callStaticMethod<jstring>("org/serenityos/ladybird/TransferAssets",
|
||||
"transferAssets",
|
||||
"(Landroid/content/Context;)Ljava/lang/String;",
|
||||
QNativeInterface::QAndroidApplication::context());
|
||||
s_serenity_resource_root = res.toString().toUtf8().data();
|
||||
|
||||
extract_ladybird_resources();
|
||||
}
|
||||
|
||||
void extract_ladybird_resources()
|
||||
{
|
||||
qDebug() << "serenity resource root is " << s_serenity_resource_root.characters();
|
||||
auto file_or_error = Core::System::open(DeprecatedString::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root), O_RDONLY);
|
||||
if (file_or_error.is_error()) {
|
||||
qDebug() << "Unable to open test file file as expected, extracting asssets...";
|
||||
|
||||
MUST(extract_tar_archive(MUST(String::formatted("{}/ladybird-assets.tar", s_serenity_resource_root)), s_serenity_resource_root));
|
||||
} else {
|
||||
qDebug() << "Opened app-browser.png test file, good to go!";
|
||||
qDebug() << "Hopefully no developer changed the asset files and expected them to be re-extracted!";
|
||||
}
|
||||
}
|
||||
|
||||
ErrorOr<void> extract_tar_archive(String archive_file, DeprecatedString output_directory)
|
||||
{
|
||||
constexpr size_t buffer_size = 4096;
|
||||
|
||||
auto file = TRY(Core::InputBufferedFile::create(TRY(Core::File::open(archive_file, Core::File::OpenMode::Read))));
|
||||
|
||||
DeprecatedString old_pwd = TRY(Core::System::getcwd());
|
||||
|
||||
TRY(Core::System::chdir(output_directory));
|
||||
ScopeGuard go_back = [&old_pwd] { MUST(Core::System::chdir(old_pwd)); };
|
||||
|
||||
auto tar_stream = TRY(Archive::TarInputStream::construct(make<Compress::GzipCompressor>(move(file))));
|
||||
|
||||
HashMap<DeprecatedString, DeprecatedString> global_overrides;
|
||||
HashMap<DeprecatedString, DeprecatedString> local_overrides;
|
||||
|
||||
auto get_override = [&](StringView key) -> Optional<DeprecatedString> {
|
||||
Optional<DeprecatedString> maybe_local = local_overrides.get(key);
|
||||
|
||||
if (maybe_local.has_value())
|
||||
return maybe_local;
|
||||
|
||||
Optional<DeprecatedString> maybe_global = global_overrides.get(key);
|
||||
|
||||
if (maybe_global.has_value())
|
||||
return maybe_global;
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
while (!tar_stream->finished()) {
|
||||
Archive::TarFileHeader const& header = tar_stream->header();
|
||||
|
||||
// Handle meta-entries earlier to avoid consuming the file content stream.
|
||||
if (header.content_is_like_extended_header()) {
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::GlobalExtendedHeader: {
|
||||
TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) {
|
||||
if (value.length() == 0)
|
||||
global_overrides.remove(key);
|
||||
else
|
||||
global_overrides.set(key, value);
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::ExtendedHeader: {
|
||||
TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) {
|
||||
local_overrides.set(key, value);
|
||||
}));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
warnln("Unknown extended header type '{}' of {}", (char)header.type_flag(), header.filename());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
TRY(tar_stream->advance());
|
||||
continue;
|
||||
}
|
||||
|
||||
Archive::TarFileStream file_stream = tar_stream->file_contents();
|
||||
|
||||
// Handle other header types that don't just have an effect on extraction.
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::LongName: {
|
||||
StringBuilder long_name;
|
||||
|
||||
Array<u8, buffer_size> buffer;
|
||||
|
||||
while (!file_stream.is_eof()) {
|
||||
auto slice = TRY(file_stream.read_some(buffer));
|
||||
long_name.append(reinterpret_cast<char*>(slice.data()), slice.size());
|
||||
}
|
||||
|
||||
local_overrides.set("path", long_name.to_deprecated_string());
|
||||
TRY(tar_stream->advance());
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
// None of the relevant headers, so continue as normal.
|
||||
break;
|
||||
}
|
||||
|
||||
LexicalPath path = LexicalPath(header.filename());
|
||||
if (!header.prefix().is_empty())
|
||||
path = path.prepend(header.prefix());
|
||||
DeprecatedString filename = get_override("path"sv).value_or(path.string());
|
||||
|
||||
DeprecatedString absolute_path = TRY(FileSystem::absolute_path(filename)).to_deprecated_string();
|
||||
auto parent_path = LexicalPath(absolute_path).parent();
|
||||
auto header_mode = TRY(header.mode());
|
||||
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::NormalFile:
|
||||
case Archive::TarFileType::AlternateNormalFile: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
int fd = TRY(Core::System::open(absolute_path, O_CREAT | O_WRONLY, header_mode));
|
||||
|
||||
Array<u8, buffer_size> buffer;
|
||||
while (!file_stream.is_eof()) {
|
||||
auto slice = TRY(file_stream.read_some(buffer));
|
||||
TRY(Core::System::write(fd, slice));
|
||||
}
|
||||
|
||||
TRY(Core::System::close(fd));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::SymLink: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
TRY(Core::System::symlink(header.link_name(), absolute_path));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::Directory: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
auto result_or_error = Core::System::mkdir(absolute_path, header_mode);
|
||||
if (result_or_error.is_error() && result_or_error.error().code() != EEXIST)
|
||||
return result_or_error.release_error();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// FIXME: Implement other file types
|
||||
warnln("file type '{}' of {} is not yet supported", (char)header.type_flag(), header.filename());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// Non-global headers should be cleared after every file.
|
||||
local_overrides.clear();
|
||||
|
||||
TRY(tar_stream->advance());
|
||||
}
|
||||
return {};
|
||||
}
|
67
Ladybird/Qt/AudioCodecPluginQt.cpp
Normal file
67
Ladybird/Qt/AudioCodecPluginQt.cpp
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "AudioCodecPluginQt.h"
|
||||
#include "AudioThread.h"
|
||||
#include <LibAudio/Loader.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
ErrorOr<NonnullOwnPtr<AudioCodecPluginQt>> AudioCodecPluginQt::create(NonnullRefPtr<Audio::Loader> loader)
|
||||
{
|
||||
auto audio_thread = TRY(AudioThread::create(move(loader)));
|
||||
audio_thread->start();
|
||||
|
||||
return adopt_nonnull_own_or_enomem(new (nothrow) AudioCodecPluginQt(move(audio_thread)));
|
||||
}
|
||||
|
||||
AudioCodecPluginQt::AudioCodecPluginQt(NonnullOwnPtr<AudioThread> audio_thread)
|
||||
: m_audio_thread(move(audio_thread))
|
||||
{
|
||||
connect(m_audio_thread, &AudioThread::playback_position_updated, this, [this](auto position) {
|
||||
if (on_playback_position_updated)
|
||||
on_playback_position_updated(position);
|
||||
});
|
||||
}
|
||||
|
||||
AudioCodecPluginQt::~AudioCodecPluginQt()
|
||||
{
|
||||
m_audio_thread->stop().release_value_but_fixme_should_propagate_errors();
|
||||
}
|
||||
|
||||
void AudioCodecPluginQt::resume_playback()
|
||||
{
|
||||
m_audio_thread->queue_task({ AudioTask::Type::Play }).release_value_but_fixme_should_propagate_errors();
|
||||
}
|
||||
|
||||
void AudioCodecPluginQt::pause_playback()
|
||||
{
|
||||
m_audio_thread->queue_task({ AudioTask::Type::Pause }).release_value_but_fixme_should_propagate_errors();
|
||||
}
|
||||
|
||||
void AudioCodecPluginQt::set_volume(double volume)
|
||||
{
|
||||
|
||||
AudioTask task { AudioTask::Type::Volume };
|
||||
task.data = volume;
|
||||
|
||||
m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors();
|
||||
}
|
||||
|
||||
void AudioCodecPluginQt::seek(double position)
|
||||
{
|
||||
AudioTask task { AudioTask::Type::Seek };
|
||||
task.data = position;
|
||||
|
||||
m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors();
|
||||
}
|
||||
|
||||
Duration AudioCodecPluginQt::duration()
|
||||
{
|
||||
return m_audio_thread->duration();
|
||||
}
|
||||
|
||||
}
|
42
Ladybird/Qt/AudioCodecPluginQt.h
Normal file
42
Ladybird/Qt/AudioCodecPluginQt.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <LibAudio/Forward.h>
|
||||
#include <LibWeb/Platform/AudioCodecPlugin.h>
|
||||
#include <QObject>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class AudioThread;
|
||||
|
||||
class AudioCodecPluginQt final
|
||||
: public QObject
|
||||
, public Web::Platform::AudioCodecPlugin {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ErrorOr<NonnullOwnPtr<AudioCodecPluginQt>> create(NonnullRefPtr<Audio::Loader>);
|
||||
virtual ~AudioCodecPluginQt() override;
|
||||
|
||||
virtual void resume_playback() override;
|
||||
virtual void pause_playback() override;
|
||||
virtual void set_volume(double) override;
|
||||
virtual void seek(double) override;
|
||||
|
||||
virtual Duration duration() override;
|
||||
|
||||
private:
|
||||
explicit AudioCodecPluginQt(NonnullOwnPtr<AudioThread>);
|
||||
|
||||
NonnullOwnPtr<AudioThread> m_audio_thread;
|
||||
};
|
||||
|
||||
}
|
212
Ladybird/Qt/AudioThread.cpp
Normal file
212
Ladybird/Qt/AudioThread.cpp
Normal file
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "AudioThread.h"
|
||||
#include <LibWeb/Platform/AudioCodecPlugin.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
struct AudioDevice {
|
||||
static AudioDevice create(Audio::Loader const& loader)
|
||||
{
|
||||
auto const& device_info = QMediaDevices::defaultAudioOutput();
|
||||
|
||||
auto format = device_info.preferredFormat();
|
||||
format.setSampleRate(static_cast<int>(loader.sample_rate()));
|
||||
format.setChannelCount(2);
|
||||
|
||||
auto audio_output = make<QAudioSink>(device_info, format);
|
||||
return AudioDevice { move(audio_output) };
|
||||
}
|
||||
|
||||
AudioDevice(AudioDevice&&) = default;
|
||||
|
||||
AudioDevice& operator=(AudioDevice&& device)
|
||||
{
|
||||
if (audio_output) {
|
||||
audio_output->stop();
|
||||
io_device = nullptr;
|
||||
}
|
||||
|
||||
swap(audio_output, device.audio_output);
|
||||
swap(io_device, device.io_device);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~AudioDevice()
|
||||
{
|
||||
if (audio_output)
|
||||
audio_output->stop();
|
||||
}
|
||||
|
||||
OwnPtr<QAudioSink> audio_output;
|
||||
QIODevice* io_device { nullptr };
|
||||
|
||||
private:
|
||||
explicit AudioDevice(NonnullOwnPtr<QAudioSink> output)
|
||||
: audio_output(move(output))
|
||||
{
|
||||
io_device = audio_output->start();
|
||||
}
|
||||
};
|
||||
|
||||
ErrorOr<NonnullOwnPtr<AudioThread>> AudioThread::create(NonnullRefPtr<Audio::Loader> loader)
|
||||
{
|
||||
auto task_queue = TRY(AudioTaskQueue::create());
|
||||
return adopt_nonnull_own_or_enomem(new (nothrow) AudioThread(move(loader), move(task_queue)));
|
||||
}
|
||||
|
||||
ErrorOr<void> AudioThread::stop()
|
||||
{
|
||||
TRY(queue_task({ AudioTask::Type::Stop }));
|
||||
wait();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> AudioThread::queue_task(AudioTask task)
|
||||
{
|
||||
return m_task_queue.blocking_enqueue(move(task), []() {
|
||||
usleep(UPDATE_RATE_MS * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
AudioThread::AudioThread(NonnullRefPtr<Audio::Loader> loader, AudioTaskQueue task_queue)
|
||||
: m_loader(move(loader))
|
||||
, m_task_queue(move(task_queue))
|
||||
{
|
||||
auto duration = static_cast<double>(m_loader->total_samples()) / static_cast<double>(m_loader->sample_rate());
|
||||
m_duration = Duration::from_milliseconds(static_cast<i64>(duration * 1000.0));
|
||||
}
|
||||
|
||||
void AudioThread::run()
|
||||
{
|
||||
auto devices = make<QMediaDevices>();
|
||||
auto audio_device = AudioDevice::create(m_loader);
|
||||
|
||||
connect(devices, &QMediaDevices::audioOutputsChanged, this, [this]() {
|
||||
queue_task({ AudioTask::Type::RecreateAudioDevice }).release_value_but_fixme_should_propagate_errors();
|
||||
});
|
||||
|
||||
auto paused = Paused::Yes;
|
||||
|
||||
while (true) {
|
||||
auto& audio_output = audio_device.audio_output;
|
||||
auto* io_device = audio_device.io_device;
|
||||
|
||||
if (auto result = m_task_queue.dequeue(); result.is_error()) {
|
||||
VERIFY(result.error() == AudioTaskQueue::QueueStatus::Empty);
|
||||
} else {
|
||||
auto task = result.release_value();
|
||||
|
||||
switch (task.type) {
|
||||
case AudioTask::Type::Stop:
|
||||
return;
|
||||
|
||||
case AudioTask::Type::Play:
|
||||
audio_output->resume();
|
||||
paused = Paused::No;
|
||||
break;
|
||||
|
||||
case AudioTask::Type::Pause:
|
||||
audio_output->suspend();
|
||||
paused = Paused::Yes;
|
||||
break;
|
||||
|
||||
case AudioTask::Type::Seek:
|
||||
VERIFY(task.data.has_value());
|
||||
m_position = Web::Platform::AudioCodecPlugin::set_loader_position(m_loader, *task.data, m_duration);
|
||||
|
||||
if (paused == Paused::Yes)
|
||||
Q_EMIT playback_position_updated(m_position);
|
||||
|
||||
break;
|
||||
|
||||
case AudioTask::Type::Volume:
|
||||
VERIFY(task.data.has_value());
|
||||
audio_output->setVolume(*task.data);
|
||||
break;
|
||||
|
||||
case AudioTask::Type::RecreateAudioDevice:
|
||||
audio_device = AudioDevice::create(m_loader);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (paused == Paused::No) {
|
||||
if (auto result = play_next_samples(*audio_output, *io_device); result.is_error()) {
|
||||
// FIXME: Propagate the error to the HTMLMediaElement.
|
||||
} else {
|
||||
Q_EMIT playback_position_updated(m_position);
|
||||
paused = result.value();
|
||||
}
|
||||
}
|
||||
|
||||
usleep(UPDATE_RATE_MS * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorOr<AudioThread::Paused> AudioThread::play_next_samples(QAudioSink& audio_output, QIODevice& io_device)
|
||||
{
|
||||
bool all_samples_loaded = m_loader->loaded_samples() >= m_loader->total_samples();
|
||||
|
||||
if (all_samples_loaded) {
|
||||
audio_output.suspend();
|
||||
(void)m_loader->reset();
|
||||
|
||||
m_position = m_duration;
|
||||
return Paused::Yes;
|
||||
}
|
||||
|
||||
auto bytes_available = audio_output.bytesFree();
|
||||
auto bytes_per_sample = audio_output.format().bytesPerSample();
|
||||
auto channel_count = audio_output.format().channelCount();
|
||||
auto samples_to_load = bytes_available / bytes_per_sample / channel_count;
|
||||
|
||||
auto samples = TRY(Web::Platform::AudioCodecPlugin::read_samples_from_loader(*m_loader, samples_to_load));
|
||||
enqueue_samples(audio_output, io_device, move(samples));
|
||||
|
||||
m_position = Web::Platform::AudioCodecPlugin::current_loader_position(m_loader);
|
||||
return Paused::No;
|
||||
}
|
||||
|
||||
void AudioThread::enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray<Audio::Sample> samples)
|
||||
{
|
||||
auto buffer_size = samples.size() * audio_output.format().bytesPerSample() * audio_output.format().channelCount();
|
||||
|
||||
if (buffer_size > static_cast<size_t>(m_sample_buffer.size()))
|
||||
m_sample_buffer.resize(buffer_size);
|
||||
|
||||
FixedMemoryStream stream { Bytes { m_sample_buffer.data(), buffer_size } };
|
||||
|
||||
for (auto const& sample : samples) {
|
||||
switch (audio_output.format().sampleFormat()) {
|
||||
case QAudioFormat::UInt8:
|
||||
write_sample<u8>(stream, sample.left);
|
||||
write_sample<u8>(stream, sample.right);
|
||||
break;
|
||||
case QAudioFormat::Int16:
|
||||
write_sample<i16>(stream, sample.left);
|
||||
write_sample<i16>(stream, sample.right);
|
||||
break;
|
||||
case QAudioFormat::Int32:
|
||||
write_sample<i32>(stream, sample.left);
|
||||
write_sample<i32>(stream, sample.right);
|
||||
break;
|
||||
case QAudioFormat::Float:
|
||||
write_sample<float>(stream, sample.left);
|
||||
write_sample<float>(stream, sample.right);
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
io_device.write(m_sample_buffer.data(), buffer_size);
|
||||
}
|
||||
|
||||
}
|
103
Ladybird/Qt/AudioThread.h
Normal file
103
Ladybird/Qt/AudioThread.h
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Endian.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibAudio/Loader.h>
|
||||
#include <LibAudio/Sample.h>
|
||||
#include <LibCore/SharedCircularQueue.h>
|
||||
#include <QAudioFormat>
|
||||
#include <QAudioSink>
|
||||
#include <QByteArray>
|
||||
#include <QMediaDevices>
|
||||
#include <QThread>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
static constexpr u32 UPDATE_RATE_MS = 10;
|
||||
|
||||
struct AudioTask {
|
||||
enum class Type {
|
||||
Stop,
|
||||
Play,
|
||||
Pause,
|
||||
Seek,
|
||||
Volume,
|
||||
RecreateAudioDevice,
|
||||
};
|
||||
|
||||
Type type;
|
||||
Optional<double> data {};
|
||||
};
|
||||
|
||||
using AudioTaskQueue = Core::SharedSingleProducerCircularQueue<AudioTask>;
|
||||
|
||||
class AudioThread final : public QThread { // We have to use QThread, otherwise internal Qt media QTimer objects do not work.
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ErrorOr<NonnullOwnPtr<AudioThread>> create(NonnullRefPtr<Audio::Loader> loader);
|
||||
|
||||
ErrorOr<void> stop();
|
||||
|
||||
Duration duration() const { return m_duration; }
|
||||
|
||||
ErrorOr<void> queue_task(AudioTask task);
|
||||
|
||||
Q_SIGNALS:
|
||||
void playback_position_updated(Duration);
|
||||
|
||||
private:
|
||||
AudioThread(NonnullRefPtr<Audio::Loader> loader, AudioTaskQueue task_queue);
|
||||
|
||||
enum class Paused {
|
||||
Yes,
|
||||
No,
|
||||
};
|
||||
|
||||
void run() override;
|
||||
|
||||
ErrorOr<Paused> play_next_samples(QAudioSink& audio_output, QIODevice& io_device);
|
||||
|
||||
void enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray<Audio::Sample> samples);
|
||||
|
||||
template<typename T>
|
||||
void write_sample(FixedMemoryStream& stream, float sample)
|
||||
{
|
||||
// The values that need to be written to the stream vary depending on the output channel format, and isn't
|
||||
// particularly well documented. The value derivations performed below were adapted from a Qt example:
|
||||
// https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/audiooutput/audiooutput.cpp?h=6.4.2#n46
|
||||
LittleEndian<T> pcm;
|
||||
|
||||
if constexpr (IsSame<T, u8>)
|
||||
pcm = static_cast<u8>((sample + 1.0f) / 2 * NumericLimits<u8>::max());
|
||||
else if constexpr (IsSame<T, i16>)
|
||||
pcm = static_cast<i16>(sample * NumericLimits<i16>::max());
|
||||
else if constexpr (IsSame<T, i32>)
|
||||
pcm = static_cast<i32>(sample * NumericLimits<i32>::max());
|
||||
else if constexpr (IsSame<T, float>)
|
||||
pcm = sample;
|
||||
else
|
||||
static_assert(DependentFalse<T>);
|
||||
|
||||
MUST(stream.write_value(pcm));
|
||||
}
|
||||
|
||||
NonnullRefPtr<Audio::Loader> m_loader;
|
||||
AudioTaskQueue m_task_queue;
|
||||
|
||||
QByteArray m_sample_buffer;
|
||||
|
||||
Duration m_duration;
|
||||
Duration m_position;
|
||||
};
|
||||
|
||||
}
|
699
Ladybird/Qt/BrowserWindow.cpp
Normal file
699
Ladybird/Qt/BrowserWindow.cpp
Normal file
|
@ -0,0 +1,699 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
|
||||
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "BrowserWindow.h"
|
||||
#include "ConsoleWidget.h"
|
||||
#include "Settings.h"
|
||||
#include "SettingsDialog.h"
|
||||
#include "StringUtils.h"
|
||||
#include "WebContentView.h"
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <Browser/CookieJar.h>
|
||||
#include <Ladybird/Utilities.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/Loader/ResourceLoader.h>
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QInputDialog>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QTabBar>
|
||||
|
||||
namespace Ladybird {
|
||||
extern Settings* s_settings;
|
||||
|
||||
static QIcon const& app_icon()
|
||||
{
|
||||
static QIcon icon;
|
||||
if (icon.isNull()) {
|
||||
QPixmap pixmap;
|
||||
pixmap.load(":/Icons/ladybird.png");
|
||||
icon = QIcon(pixmap);
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
BrowserWindow::BrowserWindow(Browser::CookieJar& cookie_jar, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling enable_callgrind_profiling, WebView::UseJavaScriptBytecode use_javascript_bytecode, UseLagomNetworking use_lagom_networking)
|
||||
: m_cookie_jar(cookie_jar)
|
||||
, m_webdriver_content_ipc_path(webdriver_content_ipc_path)
|
||||
, m_enable_callgrind_profiling(enable_callgrind_profiling)
|
||||
, m_use_javascript_bytecode(use_javascript_bytecode)
|
||||
, m_use_lagom_networking(use_lagom_networking)
|
||||
{
|
||||
setWindowIcon(app_icon());
|
||||
m_tabs_container = new QTabWidget(this);
|
||||
m_tabs_container->installEventFilter(this);
|
||||
m_tabs_container->setElideMode(Qt::TextElideMode::ElideRight);
|
||||
m_tabs_container->setMovable(true);
|
||||
m_tabs_container->setTabsClosable(true);
|
||||
m_tabs_container->setDocumentMode(true);
|
||||
m_tabs_container->setTabBarAutoHide(true);
|
||||
|
||||
auto* menu = menuBar()->addMenu("&File");
|
||||
|
||||
auto* new_tab_action = new QAction("New &Tab", this);
|
||||
new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters())));
|
||||
new_tab_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::AddTab));
|
||||
menu->addAction(new_tab_action);
|
||||
|
||||
auto* close_current_tab_action = new QAction("&Close Current Tab", this);
|
||||
close_current_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/close-tab.png").arg(s_serenity_resource_root.characters())));
|
||||
close_current_tab_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Close));
|
||||
menu->addAction(close_current_tab_action);
|
||||
|
||||
auto* open_file_action = new QAction("&Open File...", this);
|
||||
open_file_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-folder-open.png").arg(s_serenity_resource_root.characters())));
|
||||
open_file_action->setShortcut(QKeySequence(QKeySequence::StandardKey::Open));
|
||||
menu->addAction(open_file_action);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
auto* quit_action = new QAction("&Quit", this);
|
||||
quit_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Quit));
|
||||
menu->addAction(quit_action);
|
||||
|
||||
auto* edit_menu = menuBar()->addMenu("&Edit");
|
||||
|
||||
m_copy_selection_action = make<QAction>("&Copy", this);
|
||||
m_copy_selection_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters())));
|
||||
m_copy_selection_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Copy));
|
||||
edit_menu->addAction(m_copy_selection_action);
|
||||
QObject::connect(m_copy_selection_action, &QAction::triggered, this, &BrowserWindow::copy_selected_text);
|
||||
|
||||
m_select_all_action = make<QAction>("Select &All", this);
|
||||
m_select_all_action->setIcon(QIcon(QString("%1/res/icons/16x16/select-all.png").arg(s_serenity_resource_root.characters())));
|
||||
m_select_all_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::SelectAll));
|
||||
edit_menu->addAction(m_select_all_action);
|
||||
QObject::connect(m_select_all_action, &QAction::triggered, this, &BrowserWindow::select_all);
|
||||
|
||||
edit_menu->addSeparator();
|
||||
|
||||
auto* settings_action = new QAction("&Settings", this);
|
||||
settings_action->setIcon(QIcon(QString("%1/res/icons/16x16/settings.png").arg(s_serenity_resource_root.characters())));
|
||||
settings_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Preferences));
|
||||
edit_menu->addAction(settings_action);
|
||||
|
||||
auto* view_menu = menuBar()->addMenu("&View");
|
||||
|
||||
auto* open_next_tab_action = new QAction("Open &Next Tab", this);
|
||||
open_next_tab_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_PageDown));
|
||||
view_menu->addAction(open_next_tab_action);
|
||||
QObject::connect(open_next_tab_action, &QAction::triggered, this, &BrowserWindow::open_next_tab);
|
||||
|
||||
auto* open_previous_tab_action = new QAction("Open &Previous Tab", this);
|
||||
open_previous_tab_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_PageUp));
|
||||
view_menu->addAction(open_previous_tab_action);
|
||||
QObject::connect(open_previous_tab_action, &QAction::triggered, this, &BrowserWindow::open_previous_tab);
|
||||
|
||||
view_menu->addSeparator();
|
||||
|
||||
m_zoom_menu = view_menu->addMenu("&Zoom");
|
||||
|
||||
auto* zoom_in_action = new QAction("Zoom &In", this);
|
||||
zoom_in_action->setIcon(QIcon(QString("%1/res/icons/16x16/zoom-in.png").arg(s_serenity_resource_root.characters())));
|
||||
auto zoom_in_shortcuts = QKeySequence::keyBindings(QKeySequence::StandardKey::ZoomIn);
|
||||
zoom_in_shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_Equal));
|
||||
zoom_in_action->setShortcuts(zoom_in_shortcuts);
|
||||
m_zoom_menu->addAction(zoom_in_action);
|
||||
QObject::connect(zoom_in_action, &QAction::triggered, this, &BrowserWindow::zoom_in);
|
||||
|
||||
auto* zoom_out_action = new QAction("Zoom &Out", this);
|
||||
zoom_out_action->setIcon(QIcon(QString("%1/res/icons/16x16/zoom-out.png").arg(s_serenity_resource_root.characters())));
|
||||
zoom_out_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::ZoomOut));
|
||||
m_zoom_menu->addAction(zoom_out_action);
|
||||
QObject::connect(zoom_out_action, &QAction::triggered, this, &BrowserWindow::zoom_out);
|
||||
|
||||
auto* reset_zoom_action = new QAction("&Reset Zoom", this);
|
||||
reset_zoom_action->setIcon(QIcon(QString("%1/res/icons/16x16/zoom-reset.png").arg(s_serenity_resource_root.characters())));
|
||||
reset_zoom_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
|
||||
m_zoom_menu->addAction(reset_zoom_action);
|
||||
QObject::connect(reset_zoom_action, &QAction::triggered, this, &BrowserWindow::reset_zoom);
|
||||
|
||||
view_menu->addSeparator();
|
||||
|
||||
auto* color_scheme_menu = view_menu->addMenu("&Color Scheme");
|
||||
|
||||
auto* color_scheme_group = new QActionGroup(this);
|
||||
|
||||
auto* auto_color_scheme = new QAction("&Auto", this);
|
||||
auto_color_scheme->setCheckable(true);
|
||||
color_scheme_group->addAction(auto_color_scheme);
|
||||
color_scheme_menu->addAction(auto_color_scheme);
|
||||
QObject::connect(auto_color_scheme, &QAction::triggered, this, &BrowserWindow::enable_auto_color_scheme);
|
||||
|
||||
auto* light_color_scheme = new QAction("&Light", this);
|
||||
light_color_scheme->setCheckable(true);
|
||||
color_scheme_group->addAction(light_color_scheme);
|
||||
color_scheme_menu->addAction(light_color_scheme);
|
||||
QObject::connect(light_color_scheme, &QAction::triggered, this, &BrowserWindow::enable_light_color_scheme);
|
||||
|
||||
auto* dark_color_scheme = new QAction("&Dark", this);
|
||||
dark_color_scheme->setCheckable(true);
|
||||
color_scheme_group->addAction(dark_color_scheme);
|
||||
color_scheme_menu->addAction(dark_color_scheme);
|
||||
QObject::connect(dark_color_scheme, &QAction::triggered, this, &BrowserWindow::enable_dark_color_scheme);
|
||||
|
||||
auto_color_scheme->setChecked(true);
|
||||
|
||||
auto* inspect_menu = menuBar()->addMenu("&Inspect");
|
||||
|
||||
m_view_source_action = make<QAction>("View &Source", this);
|
||||
m_view_source_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-html.png").arg(s_serenity_resource_root.characters())));
|
||||
m_view_source_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_U));
|
||||
inspect_menu->addAction(m_view_source_action);
|
||||
QObject::connect(m_view_source_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab) {
|
||||
m_current_tab->view().get_source();
|
||||
}
|
||||
});
|
||||
|
||||
auto* js_console_action = new QAction("Show &JS Console", this);
|
||||
js_console_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-javascript.png").arg(s_serenity_resource_root.characters())));
|
||||
js_console_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_J));
|
||||
inspect_menu->addAction(js_console_action);
|
||||
QObject::connect(js_console_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab) {
|
||||
m_current_tab->show_console_window();
|
||||
}
|
||||
});
|
||||
|
||||
auto* inspector_action = new QAction("Open &Inspector", this);
|
||||
inspector_action->setIcon(QIcon(QString("%1/res/icons/browser/dom-tree.png").arg(s_serenity_resource_root.characters())));
|
||||
inspector_action->setShortcut(QKeySequence("Ctrl+Shift+I"));
|
||||
inspect_menu->addAction(inspector_action);
|
||||
QObject::connect(inspector_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab) {
|
||||
m_current_tab->show_inspector_window();
|
||||
}
|
||||
});
|
||||
|
||||
auto* debug_menu = menuBar()->addMenu("&Debug");
|
||||
|
||||
auto* dump_dom_tree_action = new QAction("Dump &DOM Tree", this);
|
||||
dump_dom_tree_action->setIcon(QIcon(QString("%1/res/icons/browser/dom-tree.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_dom_tree_action);
|
||||
QObject::connect(dump_dom_tree_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-dom-tree");
|
||||
});
|
||||
|
||||
auto* dump_layout_tree_action = new QAction("Dump &Layout Tree", this);
|
||||
dump_layout_tree_action->setIcon(QIcon(QString("%1/res/icons/16x16/layout.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_layout_tree_action);
|
||||
QObject::connect(dump_layout_tree_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-layout-tree");
|
||||
});
|
||||
|
||||
auto* dump_paint_tree_action = new QAction("Dump &Paint Tree", this);
|
||||
dump_paint_tree_action->setIcon(QIcon(QString("%1/res/icons/16x16/layout.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_paint_tree_action);
|
||||
QObject::connect(dump_paint_tree_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-paint-tree");
|
||||
});
|
||||
|
||||
auto* dump_stacking_context_tree_action = new QAction("Dump S&tacking Context Tree", this);
|
||||
dump_stacking_context_tree_action->setIcon(QIcon(QString("%1/res/icons/16x16/layers.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_stacking_context_tree_action);
|
||||
QObject::connect(dump_stacking_context_tree_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-stacking-context-tree");
|
||||
});
|
||||
|
||||
auto* dump_style_sheets_action = new QAction("Dump &Style Sheets", this);
|
||||
dump_style_sheets_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-css.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_style_sheets_action);
|
||||
QObject::connect(dump_style_sheets_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-style-sheets");
|
||||
});
|
||||
|
||||
auto* dump_styles_action = new QAction("Dump &All Resolved Styles", this);
|
||||
dump_styles_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-css.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_styles_action);
|
||||
QObject::connect(dump_styles_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-all-resolved-styles");
|
||||
});
|
||||
|
||||
auto* dump_history_action = new QAction("Dump &History", this);
|
||||
dump_history_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_H));
|
||||
dump_history_action->setIcon(QIcon(QString("%1/res/icons/16x16/history.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_history_action);
|
||||
QObject::connect(dump_history_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-history");
|
||||
});
|
||||
|
||||
auto* dump_cookies_action = new QAction("Dump C&ookies", this);
|
||||
dump_cookies_action->setIcon(QIcon(QString("%1/res/icons/browser/cookie.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_cookies_action);
|
||||
QObject::connect(dump_cookies_action, &QAction::triggered, this, [this] {
|
||||
m_cookie_jar.dump_cookies();
|
||||
});
|
||||
|
||||
auto* dump_local_storage_action = new QAction("Dump Loc&al Storage", this);
|
||||
dump_local_storage_action->setIcon(QIcon(QString("%1/res/icons/browser/local-storage.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(dump_local_storage_action);
|
||||
QObject::connect(dump_local_storage_action, &QAction::triggered, this, [this] {
|
||||
debug_request("dump-local-storage");
|
||||
});
|
||||
|
||||
debug_menu->addSeparator();
|
||||
|
||||
auto* show_line_box_borders_action = new QAction("Show Line Box Borders", this);
|
||||
show_line_box_borders_action->setCheckable(true);
|
||||
debug_menu->addAction(show_line_box_borders_action);
|
||||
QObject::connect(show_line_box_borders_action, &QAction::triggered, this, [this, show_line_box_borders_action] {
|
||||
bool state = show_line_box_borders_action->isChecked();
|
||||
debug_request("set-line-box-borders", state ? "on" : "off");
|
||||
});
|
||||
|
||||
debug_menu->addSeparator();
|
||||
|
||||
auto* collect_garbage_action = new QAction("Collect &Garbage", this);
|
||||
collect_garbage_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_G));
|
||||
collect_garbage_action->setIcon(QIcon(QString("%1/res/icons/16x16/trash-can.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(collect_garbage_action);
|
||||
QObject::connect(collect_garbage_action, &QAction::triggered, this, [this] {
|
||||
debug_request("collect-garbage");
|
||||
});
|
||||
|
||||
auto* clear_cache_action = new QAction("Clear &Cache", this);
|
||||
clear_cache_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_C));
|
||||
clear_cache_action->setIcon(QIcon(QString("%1/res/icons/browser/clear-cache.png").arg(s_serenity_resource_root.characters())));
|
||||
debug_menu->addAction(clear_cache_action);
|
||||
QObject::connect(clear_cache_action, &QAction::triggered, this, [this] {
|
||||
debug_request("clear-cache");
|
||||
});
|
||||
|
||||
auto* spoof_user_agent_menu = debug_menu->addMenu("Spoof &User Agent");
|
||||
spoof_user_agent_menu->setIcon(QIcon(QString("%1/res/icons/16x16/spoof.png").arg(s_serenity_resource_root.characters())));
|
||||
|
||||
auto* user_agent_group = new QActionGroup(this);
|
||||
|
||||
auto add_user_agent = [this, &user_agent_group, &spoof_user_agent_menu](auto& name, auto& user_agent) {
|
||||
auto* action = new QAction(name, this);
|
||||
action->setCheckable(true);
|
||||
user_agent_group->addAction(action);
|
||||
spoof_user_agent_menu->addAction(action);
|
||||
QObject::connect(action, &QAction::triggered, this, [this, user_agent] {
|
||||
debug_request("spoof-user-agent", user_agent);
|
||||
debug_request("clear-cache"); // clear the cache to ensure requests are re-done with the new user agent
|
||||
});
|
||||
return action;
|
||||
};
|
||||
|
||||
auto* disable_spoofing = add_user_agent("Disabled", Web::default_user_agent);
|
||||
disable_spoofing->setChecked(true);
|
||||
add_user_agent("Chrome Linux Desktop", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36");
|
||||
add_user_agent("Firefox Linux Desktop", "Mozilla/5.0 (X11; Linux i686; rv:87.0) Gecko/20100101 Firefox/87.0");
|
||||
add_user_agent("Safari macOS Desktop", "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15");
|
||||
add_user_agent("Chrome Android Mobile", "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.66 Mobile Safari/537.36");
|
||||
add_user_agent("Firefox Android Mobile", "Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/86.0");
|
||||
add_user_agent("Safari iOS Mobile", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1");
|
||||
|
||||
auto* custom_user_agent_action = new QAction("Custom...", this);
|
||||
custom_user_agent_action->setCheckable(true);
|
||||
user_agent_group->addAction(custom_user_agent_action);
|
||||
spoof_user_agent_menu->addAction(custom_user_agent_action);
|
||||
QObject::connect(custom_user_agent_action, &QAction::triggered, this, [this, disable_spoofing] {
|
||||
auto user_agent = QInputDialog::getText(this, "Custom User Agent", "Enter User Agent:");
|
||||
if (!user_agent.isEmpty()) {
|
||||
debug_request("spoof-user-agent", ak_deprecated_string_from_qstring(user_agent));
|
||||
debug_request("clear-cache"); // clear the cache to ensure requests are re-done with the new user agent
|
||||
} else {
|
||||
disable_spoofing->activate(QAction::Trigger);
|
||||
}
|
||||
});
|
||||
|
||||
debug_menu->addSeparator();
|
||||
|
||||
auto* enable_scripting_action = new QAction("Enable Scripting", this);
|
||||
enable_scripting_action->setCheckable(true);
|
||||
enable_scripting_action->setChecked(true);
|
||||
debug_menu->addAction(enable_scripting_action);
|
||||
QObject::connect(enable_scripting_action, &QAction::triggered, this, [this, enable_scripting_action] {
|
||||
bool state = enable_scripting_action->isChecked();
|
||||
debug_request("scripting", state ? "on" : "off");
|
||||
});
|
||||
|
||||
auto* block_pop_ups_action = new QAction("Block Pop-ups", this);
|
||||
block_pop_ups_action->setCheckable(true);
|
||||
block_pop_ups_action->setChecked(true);
|
||||
debug_menu->addAction(block_pop_ups_action);
|
||||
QObject::connect(block_pop_ups_action, &QAction::triggered, this, [this, block_pop_ups_action] {
|
||||
bool state = block_pop_ups_action->isChecked();
|
||||
debug_request("block-pop-ups", state ? "on" : "off");
|
||||
});
|
||||
|
||||
auto* enable_same_origin_policy_action = new QAction("Enable Same-Origin Policy", this);
|
||||
enable_same_origin_policy_action->setCheckable(true);
|
||||
debug_menu->addAction(enable_same_origin_policy_action);
|
||||
QObject::connect(enable_same_origin_policy_action, &QAction::triggered, this, [this, enable_same_origin_policy_action] {
|
||||
bool state = enable_same_origin_policy_action->isChecked();
|
||||
debug_request("same-origin-policy", state ? "on" : "off");
|
||||
});
|
||||
|
||||
QObject::connect(new_tab_action, &QAction::triggered, this, [this] {
|
||||
new_tab(s_settings->new_tab_page(), Web::HTML::ActivateTab::Yes);
|
||||
});
|
||||
QObject::connect(open_file_action, &QAction::triggered, this, &BrowserWindow::open_file);
|
||||
QObject::connect(settings_action, &QAction::triggered, this, [this] {
|
||||
new SettingsDialog(this);
|
||||
});
|
||||
QObject::connect(quit_action, &QAction::triggered, this, &QMainWindow::close);
|
||||
QObject::connect(m_tabs_container, &QTabWidget::currentChanged, [this](int index) {
|
||||
setWindowTitle(QString("%1 - Ladybird").arg(m_tabs_container->tabText(index)));
|
||||
set_current_tab(verify_cast<Tab>(m_tabs_container->widget(index)));
|
||||
});
|
||||
QObject::connect(m_tabs_container, &QTabWidget::tabCloseRequested, this, &BrowserWindow::close_tab);
|
||||
QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::close_current_tab);
|
||||
|
||||
m_inspect_dom_node_action = make<QAction>("&Inspect Element", this);
|
||||
connect(m_inspect_dom_node_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab)
|
||||
m_current_tab->show_inspector_window(Tab::InspectorTarget::HoveredElement);
|
||||
});
|
||||
m_go_back_action = make<QAction>("Go Back");
|
||||
connect(m_go_back_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab)
|
||||
m_current_tab->back();
|
||||
});
|
||||
m_go_forward_action = make<QAction>("Go Forward");
|
||||
connect(m_go_forward_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab)
|
||||
m_current_tab->forward();
|
||||
});
|
||||
m_reload_action = make<QAction>("&Reload");
|
||||
connect(m_reload_action, &QAction::triggered, this, [this] {
|
||||
if (m_current_tab)
|
||||
m_current_tab->reload();
|
||||
});
|
||||
|
||||
m_go_back_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Back));
|
||||
m_go_forward_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Forward));
|
||||
m_reload_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Refresh));
|
||||
m_go_back_action->setEnabled(false);
|
||||
m_go_forward_action->setEnabled(false);
|
||||
|
||||
new_tab(s_settings->new_tab_page(), Web::HTML::ActivateTab::Yes);
|
||||
|
||||
setCentralWidget(m_tabs_container);
|
||||
setContextMenuPolicy(Qt::PreventContextMenu);
|
||||
}
|
||||
|
||||
void BrowserWindow::set_current_tab(Tab* tab)
|
||||
{
|
||||
m_current_tab = tab;
|
||||
if (tab)
|
||||
update_displayed_zoom_level();
|
||||
}
|
||||
|
||||
void BrowserWindow::debug_request(DeprecatedString const& request, DeprecatedString const& argument)
|
||||
{
|
||||
if (!m_current_tab)
|
||||
return;
|
||||
m_current_tab->debug_request(request, argument);
|
||||
}
|
||||
|
||||
Tab& BrowserWindow::new_tab(QString const& url, Web::HTML::ActivateTab activate_tab)
|
||||
{
|
||||
auto tab = make<Tab>(this, m_webdriver_content_ipc_path, m_enable_callgrind_profiling, m_use_javascript_bytecode, m_use_lagom_networking);
|
||||
auto tab_ptr = tab.ptr();
|
||||
m_tabs.append(std::move(tab));
|
||||
|
||||
if (m_current_tab == nullptr) {
|
||||
set_current_tab(tab_ptr);
|
||||
}
|
||||
|
||||
m_tabs_container->addTab(tab_ptr, "New Tab");
|
||||
if (activate_tab == Web::HTML::ActivateTab::Yes)
|
||||
m_tabs_container->setCurrentWidget(tab_ptr);
|
||||
|
||||
QObject::connect(tab_ptr, &Tab::title_changed, this, &BrowserWindow::tab_title_changed);
|
||||
QObject::connect(tab_ptr, &Tab::favicon_changed, this, &BrowserWindow::tab_favicon_changed);
|
||||
|
||||
QObject::connect(&tab_ptr->view(), &WebContentView::urls_dropped, this, [this](auto& urls) {
|
||||
VERIFY(urls.size());
|
||||
m_current_tab->navigate(urls[0].toString());
|
||||
|
||||
for (qsizetype i = 1; i < urls.size(); ++i)
|
||||
new_tab(urls[i].toString(), Web::HTML::ActivateTab::No);
|
||||
});
|
||||
|
||||
tab_ptr->view().on_new_tab = [this](auto activate_tab) {
|
||||
auto& tab = new_tab("about:blank", activate_tab);
|
||||
return tab.view().handle();
|
||||
};
|
||||
|
||||
tab_ptr->view().on_tab_open_request = [this](auto url, auto activate_tab) {
|
||||
auto& tab = new_tab(qstring_from_ak_deprecated_string(url.to_deprecated_string()), activate_tab);
|
||||
return tab.view().handle();
|
||||
};
|
||||
|
||||
tab_ptr->view().on_link_click = [this](auto url, auto target, unsigned modifiers) {
|
||||
// TODO: maybe activate tabs according to some configuration, this is just normal current browser behavior
|
||||
if (modifiers == Mod_Ctrl) {
|
||||
m_current_tab->view().on_tab_open_request(url, Web::HTML::ActivateTab::No);
|
||||
} else if (target == "_blank") {
|
||||
m_current_tab->view().on_tab_open_request(url, Web::HTML::ActivateTab::Yes);
|
||||
} else {
|
||||
m_current_tab->view().load(url);
|
||||
}
|
||||
};
|
||||
|
||||
tab_ptr->view().on_link_middle_click = [this](auto url, auto target, unsigned modifiers) {
|
||||
m_current_tab->view().on_link_click(url, target, Mod_Ctrl);
|
||||
(void)modifiers;
|
||||
};
|
||||
|
||||
tab_ptr->view().on_get_all_cookies = [this](auto const& url) {
|
||||
return m_cookie_jar.get_all_cookies(url);
|
||||
};
|
||||
|
||||
tab_ptr->view().on_get_named_cookie = [this](auto const& url, auto const& name) {
|
||||
return m_cookie_jar.get_named_cookie(url, name);
|
||||
};
|
||||
|
||||
tab_ptr->view().on_get_cookie = [this](auto& url, auto source) -> DeprecatedString {
|
||||
return m_cookie_jar.get_cookie(url, source);
|
||||
};
|
||||
|
||||
tab_ptr->view().on_set_cookie = [this](auto& url, auto& cookie, auto source) {
|
||||
m_cookie_jar.set_cookie(url, cookie, source);
|
||||
};
|
||||
|
||||
tab_ptr->view().on_update_cookie = [this](auto const& cookie) {
|
||||
m_cookie_jar.update_cookie(cookie);
|
||||
};
|
||||
|
||||
tab_ptr->focus_location_editor();
|
||||
|
||||
// We *don't* load the initial page if we are connected to a WebDriver, as the Set URL command may come in very
|
||||
// quickly, and become replaced by this load.
|
||||
if (m_webdriver_content_ipc_path.is_empty()) {
|
||||
// We make it HistoryNavigation so that the initial page doesn't get added to the history.
|
||||
tab_ptr->navigate(url, Tab::LoadType::HistoryNavigation);
|
||||
}
|
||||
|
||||
return *tab_ptr;
|
||||
}
|
||||
|
||||
void BrowserWindow::activate_tab(int index)
|
||||
{
|
||||
m_tabs_container->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
void BrowserWindow::close_tab(int index)
|
||||
{
|
||||
auto* tab = m_tabs_container->widget(index);
|
||||
m_tabs_container->removeTab(index);
|
||||
m_tabs.remove_first_matching([&](auto& entry) {
|
||||
return entry == tab;
|
||||
});
|
||||
}
|
||||
|
||||
void BrowserWindow::open_file()
|
||||
{
|
||||
m_current_tab->open_file();
|
||||
}
|
||||
|
||||
void BrowserWindow::close_current_tab()
|
||||
{
|
||||
auto count = m_tabs_container->count() - 1;
|
||||
if (!count)
|
||||
close();
|
||||
else
|
||||
close_tab(m_tabs_container->currentIndex());
|
||||
}
|
||||
|
||||
int BrowserWindow::tab_index(Tab* tab)
|
||||
{
|
||||
return m_tabs_container->indexOf(tab);
|
||||
}
|
||||
|
||||
void BrowserWindow::tab_title_changed(int index, QString const& title)
|
||||
{
|
||||
m_tabs_container->setTabText(index, title);
|
||||
if (m_tabs_container->currentIndex() == index)
|
||||
setWindowTitle(QString("%1 - Ladybird").arg(title));
|
||||
}
|
||||
|
||||
void BrowserWindow::tab_favicon_changed(int index, QIcon icon)
|
||||
{
|
||||
m_tabs_container->setTabIcon(index, icon);
|
||||
}
|
||||
|
||||
void BrowserWindow::open_next_tab()
|
||||
{
|
||||
if (m_tabs_container->count() <= 1)
|
||||
return;
|
||||
|
||||
auto next_index = m_tabs_container->currentIndex() + 1;
|
||||
if (next_index >= m_tabs_container->count())
|
||||
next_index = 0;
|
||||
m_tabs_container->setCurrentIndex(next_index);
|
||||
}
|
||||
|
||||
void BrowserWindow::open_previous_tab()
|
||||
{
|
||||
if (m_tabs_container->count() <= 1)
|
||||
return;
|
||||
|
||||
auto next_index = m_tabs_container->currentIndex() - 1;
|
||||
if (next_index < 0)
|
||||
next_index = m_tabs_container->count() - 1;
|
||||
m_tabs_container->setCurrentIndex(next_index);
|
||||
}
|
||||
|
||||
void BrowserWindow::enable_auto_color_scheme()
|
||||
{
|
||||
for (auto& tab : m_tabs) {
|
||||
tab->view().set_preferred_color_scheme(Web::CSS::PreferredColorScheme::Auto);
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserWindow::enable_light_color_scheme()
|
||||
{
|
||||
for (auto& tab : m_tabs) {
|
||||
tab->view().set_preferred_color_scheme(Web::CSS::PreferredColorScheme::Light);
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserWindow::enable_dark_color_scheme()
|
||||
{
|
||||
for (auto& tab : m_tabs) {
|
||||
tab->view().set_preferred_color_scheme(Web::CSS::PreferredColorScheme::Dark);
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserWindow::zoom_in()
|
||||
{
|
||||
if (!m_current_tab)
|
||||
return;
|
||||
m_current_tab->view().zoom_in();
|
||||
update_displayed_zoom_level();
|
||||
}
|
||||
|
||||
void BrowserWindow::zoom_out()
|
||||
{
|
||||
if (!m_current_tab)
|
||||
return;
|
||||
m_current_tab->view().zoom_out();
|
||||
update_displayed_zoom_level();
|
||||
}
|
||||
|
||||
void BrowserWindow::reset_zoom()
|
||||
{
|
||||
if (!m_current_tab)
|
||||
return;
|
||||
m_current_tab->view().reset_zoom();
|
||||
update_displayed_zoom_level();
|
||||
}
|
||||
|
||||
void BrowserWindow::update_zoom_menu()
|
||||
{
|
||||
VERIFY(m_zoom_menu);
|
||||
auto zoom_level_text = MUST(String::formatted("&Zoom ({}%)", round_to<int>(m_current_tab->view().zoom_level() * 100)));
|
||||
m_zoom_menu->setTitle(qstring_from_ak_string(zoom_level_text));
|
||||
}
|
||||
|
||||
void BrowserWindow::select_all()
|
||||
{
|
||||
if (!m_current_tab)
|
||||
return;
|
||||
|
||||
if (auto* console = m_current_tab->console(); console && console->isActiveWindow())
|
||||
console->view().select_all();
|
||||
else
|
||||
m_current_tab->view().select_all();
|
||||
}
|
||||
|
||||
void BrowserWindow::update_displayed_zoom_level()
|
||||
{
|
||||
VERIFY(m_current_tab);
|
||||
update_zoom_menu();
|
||||
m_current_tab->update_reset_zoom_button();
|
||||
}
|
||||
|
||||
void BrowserWindow::copy_selected_text()
|
||||
{
|
||||
if (!m_current_tab)
|
||||
return;
|
||||
|
||||
DeprecatedString text;
|
||||
|
||||
if (auto* console = m_current_tab->console(); console && console->isActiveWindow())
|
||||
text = console->view().selected_text();
|
||||
else
|
||||
text = m_current_tab->view().selected_text();
|
||||
|
||||
auto* clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(qstring_from_ak_deprecated_string(text));
|
||||
}
|
||||
|
||||
void BrowserWindow::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
for (auto& tab : m_tabs) {
|
||||
tab->view().set_window_size({ frameSize().width(), frameSize().height() });
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserWindow::moveEvent(QMoveEvent* event)
|
||||
{
|
||||
QWidget::moveEvent(event);
|
||||
for (auto& tab : m_tabs) {
|
||||
tab->view().set_window_position({ event->pos().x(), event->pos().y() });
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserWindow::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if ((event->modifiers() & Qt::ControlModifier) != 0) {
|
||||
if (event->angleDelta().y() > 0)
|
||||
zoom_in();
|
||||
else if (event->angleDelta().y() < 0)
|
||||
zoom_out();
|
||||
}
|
||||
}
|
||||
|
||||
bool BrowserWindow::eventFilter(QObject* obj, QEvent* event)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonRelease) {
|
||||
auto const* const mouse_event = static_cast<QMouseEvent*>(event);
|
||||
if (mouse_event->button() == Qt::MouseButton::MiddleButton) {
|
||||
if (obj == m_tabs_container) {
|
||||
auto const tab_index = m_tabs_container->tabBar()->tabAt(mouse_event->pos());
|
||||
close_tab(tab_index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QMainWindow::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
}
|
126
Ladybird/Qt/BrowserWindow.h
Normal file
126
Ladybird/Qt/BrowserWindow.h
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Tab.h"
|
||||
#include <LibCore/Forward.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <QIcon>
|
||||
#include <QLineEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QMenuBar>
|
||||
#include <QTabWidget>
|
||||
#include <QToolBar>
|
||||
|
||||
namespace Browser {
|
||||
class CookieJar;
|
||||
}
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebContentView;
|
||||
|
||||
class BrowserWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BrowserWindow(Browser::CookieJar&, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode, UseLagomNetworking);
|
||||
|
||||
WebContentView& view() const { return m_current_tab->view(); }
|
||||
|
||||
int tab_index(Tab*);
|
||||
|
||||
QAction& go_back_action()
|
||||
{
|
||||
return *m_go_back_action;
|
||||
}
|
||||
|
||||
QAction& go_forward_action()
|
||||
{
|
||||
return *m_go_forward_action;
|
||||
}
|
||||
|
||||
QAction& reload_action()
|
||||
{
|
||||
return *m_reload_action;
|
||||
}
|
||||
|
||||
QAction& copy_selection_action()
|
||||
{
|
||||
return *m_copy_selection_action;
|
||||
}
|
||||
|
||||
QAction& select_all_action()
|
||||
{
|
||||
return *m_select_all_action;
|
||||
}
|
||||
|
||||
QAction& view_source_action()
|
||||
{
|
||||
return *m_view_source_action;
|
||||
}
|
||||
|
||||
QAction& inspect_dom_node_action()
|
||||
{
|
||||
return *m_inspect_dom_node_action;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void tab_title_changed(int index, QString const&);
|
||||
void tab_favicon_changed(int index, QIcon icon);
|
||||
Tab& new_tab(QString const&, Web::HTML::ActivateTab);
|
||||
void activate_tab(int index);
|
||||
void close_tab(int index);
|
||||
void close_current_tab();
|
||||
void open_next_tab();
|
||||
void open_previous_tab();
|
||||
void open_file();
|
||||
void enable_auto_color_scheme();
|
||||
void enable_light_color_scheme();
|
||||
void enable_dark_color_scheme();
|
||||
void zoom_in();
|
||||
void zoom_out();
|
||||
void reset_zoom();
|
||||
void update_zoom_menu();
|
||||
void select_all();
|
||||
void copy_selected_text();
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
private:
|
||||
virtual void resizeEvent(QResizeEvent*) override;
|
||||
virtual void moveEvent(QMoveEvent*) override;
|
||||
virtual void wheelEvent(QWheelEvent*) override;
|
||||
|
||||
void debug_request(DeprecatedString const& request, DeprecatedString const& argument = "");
|
||||
|
||||
void set_current_tab(Tab* tab);
|
||||
void update_displayed_zoom_level();
|
||||
|
||||
QTabWidget* m_tabs_container { nullptr };
|
||||
Vector<NonnullOwnPtr<Tab>> m_tabs;
|
||||
Tab* m_current_tab { nullptr };
|
||||
QMenu* m_zoom_menu { nullptr };
|
||||
|
||||
OwnPtr<QAction> m_go_back_action {};
|
||||
OwnPtr<QAction> m_go_forward_action {};
|
||||
OwnPtr<QAction> m_reload_action {};
|
||||
OwnPtr<QAction> m_copy_selection_action {};
|
||||
OwnPtr<QAction> m_select_all_action {};
|
||||
OwnPtr<QAction> m_view_source_action {};
|
||||
OwnPtr<QAction> m_inspect_dom_node_action {};
|
||||
|
||||
Browser::CookieJar& m_cookie_jar;
|
||||
|
||||
StringView m_webdriver_content_ipc_path;
|
||||
WebView::EnableCallgrindProfiling m_enable_callgrind_profiling;
|
||||
WebView::UseJavaScriptBytecode m_use_javascript_bytecode;
|
||||
UseLagomNetworking m_use_lagom_networking;
|
||||
};
|
||||
|
||||
}
|
220
Ladybird/Qt/ConsoleWidget.cpp
Normal file
220
Ladybird/Qt/ConsoleWidget.cpp
Normal file
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
|
||||
* Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
|
||||
* Copyright (c) 2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ConsoleWidget.h"
|
||||
#include "StringUtils.h"
|
||||
#include "WebContentView.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibJS/MarkupGenerator.h>
|
||||
#include <QFontDatabase>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QPalette>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
bool is_using_dark_system_theme(QWidget&);
|
||||
|
||||
ConsoleWidget::ConsoleWidget()
|
||||
{
|
||||
setLayout(new QVBoxLayout);
|
||||
|
||||
m_output_view = new WebContentView({}, WebView::EnableCallgrindProfiling::No, WebView::UseJavaScriptBytecode::No, UseLagomNetworking::No);
|
||||
if (is_using_dark_system_theme(*this))
|
||||
m_output_view->update_palette(WebContentView::PaletteMode::Dark);
|
||||
|
||||
m_output_view->load("data:text/html,<html style=\"font: 10pt monospace;\"></html>"sv);
|
||||
// Wait until our output WebView is loaded, and then request any messages that occurred before we existed
|
||||
m_output_view->on_load_finish = [this](auto&) {
|
||||
if (on_request_messages)
|
||||
on_request_messages(0);
|
||||
};
|
||||
|
||||
layout()->addWidget(m_output_view);
|
||||
|
||||
auto* bottom_container = new QWidget(this);
|
||||
bottom_container->setLayout(new QHBoxLayout);
|
||||
|
||||
layout()->addWidget(bottom_container);
|
||||
|
||||
m_input = new ConsoleInputEdit(bottom_container, *this);
|
||||
m_input->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
bottom_container->layout()->addWidget(m_input);
|
||||
|
||||
setFocusProxy(m_input);
|
||||
|
||||
auto* clear_button = new QPushButton(bottom_container);
|
||||
bottom_container->layout()->addWidget(clear_button);
|
||||
clear_button->setFixedSize(22, 22);
|
||||
clear_button->setText("X");
|
||||
clear_button->setToolTip("Clear the console output");
|
||||
QObject::connect(clear_button, &QPushButton::pressed, [this] {
|
||||
clear_output();
|
||||
});
|
||||
|
||||
m_input->setFocus();
|
||||
}
|
||||
|
||||
void ConsoleWidget::request_console_messages()
|
||||
{
|
||||
VERIFY(!m_waiting_for_messages);
|
||||
VERIFY(on_request_messages);
|
||||
on_request_messages(m_highest_received_message_index + 1);
|
||||
m_waiting_for_messages = true;
|
||||
}
|
||||
|
||||
void ConsoleWidget::notify_about_new_console_message(i32 message_index)
|
||||
{
|
||||
if (message_index <= m_highest_received_message_index) {
|
||||
dbgln("Notified about console message we already have");
|
||||
return;
|
||||
}
|
||||
if (message_index <= m_highest_notified_message_index) {
|
||||
dbgln("Notified about console message we're already aware of");
|
||||
return;
|
||||
}
|
||||
|
||||
m_highest_notified_message_index = message_index;
|
||||
if (!m_waiting_for_messages)
|
||||
request_console_messages();
|
||||
}
|
||||
|
||||
void ConsoleWidget::handle_console_messages(i32 start_index, Vector<DeprecatedString> const& message_types, Vector<DeprecatedString> const& messages)
|
||||
{
|
||||
i32 end_index = start_index + message_types.size() - 1;
|
||||
if (end_index <= m_highest_received_message_index) {
|
||||
dbgln("Received old console messages");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < message_types.size(); i++) {
|
||||
auto& type = message_types[i];
|
||||
auto& message = messages[i];
|
||||
|
||||
if (type == "html") {
|
||||
print_html(message);
|
||||
} else if (type == "clear") {
|
||||
clear_output();
|
||||
} else if (type == "group") {
|
||||
// FIXME: Implement.
|
||||
} else if (type == "groupCollapsed") {
|
||||
// FIXME: Implement.
|
||||
} else if (type == "groupEnd") {
|
||||
// FIXME: Implement.
|
||||
} else {
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
m_highest_received_message_index = end_index;
|
||||
m_waiting_for_messages = false;
|
||||
|
||||
if (m_highest_received_message_index < m_highest_notified_message_index)
|
||||
request_console_messages();
|
||||
}
|
||||
|
||||
void ConsoleWidget::print_source_line(StringView source)
|
||||
{
|
||||
StringBuilder html;
|
||||
html.append("<span class=\"repl-indicator\">"sv);
|
||||
html.append("> "sv);
|
||||
html.append("</span>"sv);
|
||||
|
||||
html.append(JS::MarkupGenerator::html_from_source(source).release_value_but_fixme_should_propagate_errors());
|
||||
|
||||
print_html(html.string_view());
|
||||
}
|
||||
|
||||
void ConsoleWidget::print_html(StringView line)
|
||||
{
|
||||
StringBuilder builder;
|
||||
|
||||
builder.append(R"~~~(
|
||||
var p = document.createElement("p");
|
||||
p.innerHTML = ")~~~"sv);
|
||||
builder.append_escaped_for_json(line);
|
||||
builder.append(R"~~~("
|
||||
document.body.appendChild(p);
|
||||
)~~~"sv);
|
||||
|
||||
// FIXME: It should be sufficient to scrollTo a y value of document.documentElement.offsetHeight,
|
||||
// but due to an unknown bug offsetHeight seems to not be properly updated after spamming
|
||||
// a lot of document changes.
|
||||
//
|
||||
// The setTimeout makes the scrollTo async and allows the DOM to be updated.
|
||||
builder.append("setTimeout(function() { window.scrollTo(0, 1_000_000_000); }, 0);"sv);
|
||||
|
||||
m_output_view->run_javascript(builder.string_view());
|
||||
}
|
||||
|
||||
void ConsoleWidget::clear_output()
|
||||
{
|
||||
m_output_view->run_javascript(R"~~~(
|
||||
document.body.innerHTML = "";
|
||||
)~~~"sv);
|
||||
}
|
||||
|
||||
void ConsoleWidget::reset()
|
||||
{
|
||||
clear_output();
|
||||
m_highest_notified_message_index = -1;
|
||||
m_highest_received_message_index = -1;
|
||||
m_waiting_for_messages = false;
|
||||
}
|
||||
|
||||
void ConsoleInputEdit::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Down: {
|
||||
if (m_history.is_empty())
|
||||
break;
|
||||
auto last_index = m_history.size() - 1;
|
||||
if (m_history_index < last_index) {
|
||||
m_history_index++;
|
||||
setText(qstring_from_ak_deprecated_string(m_history.at(m_history_index)));
|
||||
} else if (m_history_index == last_index) {
|
||||
m_history_index++;
|
||||
clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Qt::Key_Up:
|
||||
if (m_history_index > 0) {
|
||||
m_history_index--;
|
||||
setText(qstring_from_ak_deprecated_string(m_history.at(m_history_index)));
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Return: {
|
||||
auto js_source = ak_deprecated_string_from_qstring(text());
|
||||
if (js_source.is_whitespace())
|
||||
return;
|
||||
|
||||
if (m_history.is_empty() || m_history.last() != js_source) {
|
||||
m_history.append(js_source);
|
||||
m_history_index = m_history.size();
|
||||
}
|
||||
|
||||
clear();
|
||||
|
||||
m_console_widget.print_source_line(js_source);
|
||||
|
||||
if (m_console_widget.on_js_input)
|
||||
m_console_widget.on_js_input(js_source);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
QLineEdit::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
69
Ladybird/Qt/ConsoleWidget.h
Normal file
69
Ladybird/Qt/ConsoleWidget.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
|
||||
* Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
|
||||
* Copyright (c) 2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <QLineEdit>
|
||||
#include <QWidget>
|
||||
|
||||
class QLineEdit;
|
||||
namespace Ladybird {
|
||||
|
||||
class WebContentView;
|
||||
|
||||
class ConsoleWidget final : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ConsoleWidget();
|
||||
virtual ~ConsoleWidget() = default;
|
||||
|
||||
void notify_about_new_console_message(i32 message_index);
|
||||
void handle_console_messages(i32 start_index, Vector<DeprecatedString> const& message_types, Vector<DeprecatedString> const& messages);
|
||||
void print_source_line(StringView);
|
||||
void print_html(StringView);
|
||||
void reset();
|
||||
|
||||
WebContentView& view() { return *m_output_view; }
|
||||
|
||||
Function<void(DeprecatedString const&)> on_js_input;
|
||||
Function<void(i32)> on_request_messages;
|
||||
|
||||
private:
|
||||
void request_console_messages();
|
||||
void clear_output();
|
||||
|
||||
WebContentView* m_output_view { nullptr };
|
||||
QLineEdit* m_input { nullptr };
|
||||
|
||||
i32 m_highest_notified_message_index { -1 };
|
||||
i32 m_highest_received_message_index { -1 };
|
||||
bool m_waiting_for_messages { false };
|
||||
};
|
||||
|
||||
class ConsoleInputEdit final : public QLineEdit {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ConsoleInputEdit(QWidget* q_widget, ConsoleWidget& console_widget)
|
||||
: QLineEdit(q_widget)
|
||||
, m_console_widget(console_widget)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
virtual void keyPressEvent(QKeyEvent* event) override;
|
||||
|
||||
ConsoleWidget& m_console_widget;
|
||||
Vector<DeprecatedString> m_history;
|
||||
size_t m_history_index { 0 };
|
||||
};
|
||||
|
||||
}
|
177
Ladybird/Qt/EventLoopImplementationQt.cpp
Normal file
177
Ladybird/Qt/EventLoopImplementationQt.cpp
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "EventLoopImplementationQt.h"
|
||||
#include "EventLoopImplementationQtEventTarget.h"
|
||||
#include <AK/IDAllocator.h>
|
||||
#include <LibCore/Event.h>
|
||||
#include <LibCore/EventReceiver.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
struct ThreadData;
|
||||
static thread_local ThreadData* s_thread_data;
|
||||
|
||||
struct ThreadData {
|
||||
static ThreadData& the()
|
||||
{
|
||||
if (!s_thread_data) {
|
||||
// FIXME: Don't leak this.
|
||||
s_thread_data = new ThreadData;
|
||||
}
|
||||
return *s_thread_data;
|
||||
}
|
||||
|
||||
IDAllocator timer_id_allocator;
|
||||
HashMap<int, NonnullOwnPtr<QTimer>> timers;
|
||||
HashMap<Core::Notifier*, NonnullOwnPtr<QSocketNotifier>> notifiers;
|
||||
};
|
||||
|
||||
EventLoopImplementationQt::EventLoopImplementationQt()
|
||||
{
|
||||
}
|
||||
|
||||
EventLoopImplementationQt::~EventLoopImplementationQt() = default;
|
||||
|
||||
int EventLoopImplementationQt::exec()
|
||||
{
|
||||
if (is_main_loop())
|
||||
return QCoreApplication::exec();
|
||||
return m_event_loop.exec();
|
||||
}
|
||||
|
||||
size_t EventLoopImplementationQt::pump(PumpMode mode)
|
||||
{
|
||||
auto result = Core::ThreadEventQueue::current().process();
|
||||
auto qt_mode = mode == PumpMode::WaitForEvents ? QEventLoop::WaitForMoreEvents : QEventLoop::AllEvents;
|
||||
if (is_main_loop())
|
||||
QCoreApplication::processEvents(qt_mode);
|
||||
else
|
||||
m_event_loop.processEvents(qt_mode);
|
||||
result += Core::ThreadEventQueue::current().process();
|
||||
return result;
|
||||
}
|
||||
|
||||
void EventLoopImplementationQt::quit(int code)
|
||||
{
|
||||
if (is_main_loop())
|
||||
QCoreApplication::exit(code);
|
||||
else
|
||||
m_event_loop.exit(code);
|
||||
}
|
||||
|
||||
void EventLoopImplementationQt::wake()
|
||||
{
|
||||
if (!is_main_loop())
|
||||
m_event_loop.wakeUp();
|
||||
}
|
||||
|
||||
void EventLoopImplementationQt::post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&& event)
|
||||
{
|
||||
m_thread_event_queue.post_event(receiver, move(event));
|
||||
if (&m_thread_event_queue != &Core::ThreadEventQueue::current())
|
||||
wake();
|
||||
}
|
||||
|
||||
static void qt_timer_fired(int timer_id, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible, Core::EventReceiver& object)
|
||||
{
|
||||
if (should_fire_when_not_visible == Core::TimerShouldFireWhenNotVisible::No) {
|
||||
if (!object.is_visible_for_timer_purposes())
|
||||
return;
|
||||
}
|
||||
Core::TimerEvent event(timer_id);
|
||||
object.dispatch_event(event);
|
||||
}
|
||||
|
||||
int EventLoopManagerQt::register_timer(Core::EventReceiver& object, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible)
|
||||
{
|
||||
auto& thread_data = ThreadData::the();
|
||||
auto timer = make<QTimer>();
|
||||
timer->setInterval(milliseconds);
|
||||
timer->setSingleShot(!should_reload);
|
||||
auto timer_id = thread_data.timer_id_allocator.allocate();
|
||||
auto weak_object = object.make_weak_ptr();
|
||||
QObject::connect(timer, &QTimer::timeout, [timer_id, should_fire_when_not_visible, weak_object = move(weak_object)] {
|
||||
auto object = weak_object.strong_ref();
|
||||
if (!object)
|
||||
return;
|
||||
qt_timer_fired(timer_id, should_fire_when_not_visible, *object);
|
||||
});
|
||||
timer->start();
|
||||
thread_data.timers.set(timer_id, move(timer));
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
bool EventLoopManagerQt::unregister_timer(int timer_id)
|
||||
{
|
||||
auto& thread_data = ThreadData::the();
|
||||
thread_data.timer_id_allocator.deallocate(timer_id);
|
||||
return thread_data.timers.remove(timer_id);
|
||||
}
|
||||
|
||||
static void qt_notifier_activated(Core::Notifier& notifier)
|
||||
{
|
||||
Core::NotifierActivationEvent event(notifier.fd());
|
||||
notifier.dispatch_event(event);
|
||||
}
|
||||
|
||||
void EventLoopManagerQt::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
QSocketNotifier::Type type;
|
||||
switch (notifier.type()) {
|
||||
case Core::Notifier::Type::Read:
|
||||
type = QSocketNotifier::Read;
|
||||
break;
|
||||
case Core::Notifier::Type::Write:
|
||||
type = QSocketNotifier::Write;
|
||||
break;
|
||||
default:
|
||||
TODO();
|
||||
}
|
||||
auto socket_notifier = make<QSocketNotifier>(notifier.fd(), type);
|
||||
QObject::connect(socket_notifier, &QSocketNotifier::activated, [¬ifier] {
|
||||
qt_notifier_activated(notifier);
|
||||
});
|
||||
|
||||
ThreadData::the().notifiers.set(¬ifier, move(socket_notifier));
|
||||
}
|
||||
|
||||
void EventLoopManagerQt::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
ThreadData::the().notifiers.remove(¬ifier);
|
||||
}
|
||||
|
||||
void EventLoopManagerQt::did_post_event()
|
||||
{
|
||||
QCoreApplication::postEvent(m_main_thread_event_target.ptr(), new QtEventLoopManagerEvent(QtEventLoopManagerEvent::process_event_queue_event_type()));
|
||||
}
|
||||
|
||||
bool EventLoopManagerQt::event_target_received_event(Badge<EventLoopImplementationQtEventTarget>, QEvent* event)
|
||||
{
|
||||
if (event->type() == QtEventLoopManagerEvent::process_event_queue_event_type()) {
|
||||
Core::ThreadEventQueue::current().process();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
EventLoopManagerQt::EventLoopManagerQt()
|
||||
: m_main_thread_event_target(make<EventLoopImplementationQtEventTarget>())
|
||||
{
|
||||
}
|
||||
|
||||
EventLoopManagerQt::~EventLoopManagerQt() = default;
|
||||
|
||||
NonnullOwnPtr<Core::EventLoopImplementation> EventLoopManagerQt::make_implementation()
|
||||
{
|
||||
return adopt_own(*new EventLoopImplementationQt);
|
||||
}
|
||||
|
||||
}
|
89
Ladybird/Qt/EventLoopImplementationQt.h
Normal file
89
Ladybird/Qt/EventLoopImplementationQt.h
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibCore/EventLoopImplementation.h>
|
||||
#include <QEvent>
|
||||
#include <QEventLoop>
|
||||
#include <QSocketNotifier>
|
||||
#include <QTimer>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class EventLoopImplementationQtEventTarget;
|
||||
|
||||
class EventLoopManagerQt final : public Core::EventLoopManager {
|
||||
public:
|
||||
EventLoopManagerQt();
|
||||
virtual ~EventLoopManagerQt() override;
|
||||
virtual NonnullOwnPtr<Core::EventLoopImplementation> make_implementation() override;
|
||||
|
||||
virtual int register_timer(Core::EventReceiver&, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override;
|
||||
virtual bool unregister_timer(int timer_id) override;
|
||||
|
||||
virtual void register_notifier(Core::Notifier&) override;
|
||||
virtual void unregister_notifier(Core::Notifier&) override;
|
||||
|
||||
virtual void did_post_event() override;
|
||||
static bool event_target_received_event(Badge<EventLoopImplementationQtEventTarget>, QEvent* event);
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual int register_signal(int, Function<void(int)>) override { return 0; }
|
||||
virtual void unregister_signal(int) override { }
|
||||
|
||||
private:
|
||||
NonnullOwnPtr<EventLoopImplementationQtEventTarget> m_main_thread_event_target;
|
||||
};
|
||||
|
||||
class QtEventLoopManagerEvent final : public QEvent {
|
||||
public:
|
||||
static QEvent::Type process_event_queue_event_type()
|
||||
{
|
||||
static auto const type = static_cast<QEvent::Type>(QEvent::registerEventType());
|
||||
return type;
|
||||
}
|
||||
|
||||
QtEventLoopManagerEvent(QEvent::Type type)
|
||||
: QEvent(type)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class EventLoopImplementationQt final : public Core::EventLoopImplementation {
|
||||
public:
|
||||
static NonnullOwnPtr<EventLoopImplementationQt> create() { return adopt_own(*new EventLoopImplementationQt); }
|
||||
|
||||
virtual ~EventLoopImplementationQt() override;
|
||||
|
||||
virtual int exec() override;
|
||||
virtual size_t pump(PumpMode) override;
|
||||
virtual void quit(int) override;
|
||||
virtual void wake() override;
|
||||
virtual void post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&&) override;
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual void unquit() override { }
|
||||
virtual bool was_exit_requested() const override { return false; }
|
||||
virtual void notify_forked_and_in_child() override { }
|
||||
|
||||
void set_main_loop() { m_main_loop = true; }
|
||||
|
||||
private:
|
||||
friend class EventLoopManagerQt;
|
||||
|
||||
EventLoopImplementationQt();
|
||||
bool is_main_loop() const { return m_main_loop; }
|
||||
|
||||
QEventLoop m_event_loop;
|
||||
bool m_main_loop { false };
|
||||
};
|
||||
|
||||
}
|
16
Ladybird/Qt/EventLoopImplementationQtEventTarget.cpp
Normal file
16
Ladybird/Qt/EventLoopImplementationQtEventTarget.cpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Gregory Bertilson <zaggy1024@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "EventLoopImplementationQtEventTarget.h"
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
bool EventLoopImplementationQtEventTarget::event(QEvent* event)
|
||||
{
|
||||
return EventLoopManagerQt::event_target_received_event({}, event);
|
||||
}
|
||||
|
||||
}
|
22
Ladybird/Qt/EventLoopImplementationQtEventTarget.h
Normal file
22
Ladybird/Qt/EventLoopImplementationQtEventTarget.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Gregory Bertilson <zaggy1024@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QEvent>
|
||||
|
||||
#include "EventLoopImplementationQt.h"
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class EventLoopImplementationQtEventTarget final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
virtual bool event(QEvent* event) override;
|
||||
};
|
||||
|
||||
}
|
185
Ladybird/Qt/InspectorWidget.cpp
Normal file
185
Ladybird/Qt/InspectorWidget.cpp
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWebView/AccessibilityTreeModel.h>
|
||||
#include <LibWebView/DOMTreeModel.h>
|
||||
#include <LibWebView/StylePropertiesModel.h>
|
||||
|
||||
#include "InspectorWidget.h"
|
||||
#include <QCloseEvent>
|
||||
#include <QHeaderView>
|
||||
#include <QSplitter>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QTableView>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
InspectorWidget::InspectorWidget()
|
||||
{
|
||||
setLayout(new QVBoxLayout);
|
||||
auto splitter = new QSplitter(this);
|
||||
layout()->addWidget(splitter);
|
||||
splitter->setOrientation(Qt::Vertical);
|
||||
|
||||
auto add_tab = [&](auto* tab_widget, auto* widget, auto name) {
|
||||
auto container = new QWidget;
|
||||
container->setLayout(new QVBoxLayout);
|
||||
container->layout()->addWidget(widget);
|
||||
tab_widget->addTab(container, name);
|
||||
};
|
||||
|
||||
auto* top_tab_widget = new QTabWidget;
|
||||
splitter->addWidget(top_tab_widget);
|
||||
|
||||
m_dom_tree_view = new QTreeView;
|
||||
m_dom_tree_view->setHeaderHidden(true);
|
||||
m_dom_tree_view->setModel(&m_dom_model);
|
||||
QObject::connect(m_dom_tree_view->selectionModel(), &QItemSelectionModel::selectionChanged,
|
||||
[this](QItemSelection const& selected, QItemSelection const&) {
|
||||
auto indexes = selected.indexes();
|
||||
if (indexes.size()) {
|
||||
auto index = m_dom_model.to_gui(indexes.first());
|
||||
set_selection(index);
|
||||
}
|
||||
});
|
||||
add_tab(top_tab_widget, m_dom_tree_view, "DOM");
|
||||
|
||||
auto accessibility_tree_view = new QTreeView;
|
||||
accessibility_tree_view->setHeaderHidden(true);
|
||||
accessibility_tree_view->setModel(&m_accessibility_model);
|
||||
add_tab(top_tab_widget, accessibility_tree_view, "Accessibility");
|
||||
|
||||
auto add_table_tab = [&](auto* tab_widget, auto& model, auto name) {
|
||||
auto table_view = new QTableView;
|
||||
table_view->setModel(&model);
|
||||
table_view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
table_view->verticalHeader()->setVisible(false);
|
||||
table_view->horizontalHeader()->setVisible(false);
|
||||
add_tab(tab_widget, table_view, name);
|
||||
};
|
||||
|
||||
auto node_tabs = new QTabWidget;
|
||||
add_table_tab(node_tabs, m_computed_style_model, "Computed");
|
||||
add_table_tab(node_tabs, m_resolved_style_model, "Resolved");
|
||||
add_table_tab(node_tabs, m_custom_properties_model, "Variables");
|
||||
splitter->addWidget(node_tabs);
|
||||
}
|
||||
|
||||
void InspectorWidget::set_dom_json(StringView dom_json)
|
||||
{
|
||||
m_dom_model.set_underlying_model(WebView::DOMTreeModel::create(dom_json));
|
||||
m_dom_loaded = true;
|
||||
if (m_pending_selection.has_value())
|
||||
set_selection(m_pending_selection.release_value());
|
||||
else
|
||||
select_default_node();
|
||||
}
|
||||
|
||||
void InspectorWidget::set_accessibility_json(StringView accessibility_json)
|
||||
{
|
||||
m_accessibility_model.set_underlying_model(WebView::AccessibilityTreeModel::create(accessibility_json));
|
||||
}
|
||||
|
||||
void InspectorWidget::clear_dom_json()
|
||||
{
|
||||
m_dom_model.set_underlying_model(nullptr);
|
||||
// The accessibility tree is pretty much another form of the DOM tree, so should be cleared at the time time.
|
||||
m_accessibility_model.set_underlying_model(nullptr);
|
||||
clear_style_json();
|
||||
clear_selection();
|
||||
m_dom_loaded = false;
|
||||
}
|
||||
|
||||
void InspectorWidget::load_style_json(StringView computed_style_json, StringView resolved_style_json, StringView custom_properties_json)
|
||||
{
|
||||
m_computed_style_model.set_underlying_model(WebView::StylePropertiesModel::create(computed_style_json));
|
||||
m_resolved_style_model.set_underlying_model(WebView::StylePropertiesModel::create(resolved_style_json));
|
||||
m_custom_properties_model.set_underlying_model(WebView::StylePropertiesModel::create(custom_properties_json));
|
||||
}
|
||||
|
||||
void InspectorWidget::clear_style_json()
|
||||
{
|
||||
m_computed_style_model.set_underlying_model(nullptr);
|
||||
m_resolved_style_model.set_underlying_model(nullptr);
|
||||
m_custom_properties_model.set_underlying_model(nullptr);
|
||||
clear_selection();
|
||||
}
|
||||
|
||||
void InspectorWidget::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
event->accept();
|
||||
if (on_close)
|
||||
on_close();
|
||||
clear_selection();
|
||||
}
|
||||
|
||||
void InspectorWidget::clear_selection()
|
||||
{
|
||||
m_selection = {};
|
||||
m_dom_tree_view->clearSelection();
|
||||
}
|
||||
|
||||
void InspectorWidget::set_selection(Selection selection)
|
||||
{
|
||||
if (!m_dom_loaded) {
|
||||
m_pending_selection = selection;
|
||||
return;
|
||||
}
|
||||
|
||||
auto* model = verify_cast<WebView::DOMTreeModel>(m_dom_model.underlying_model().ptr());
|
||||
auto index = model->index_for_node(selection.dom_node_id, selection.pseudo_element);
|
||||
auto qt_index = m_dom_model.to_qt(index);
|
||||
|
||||
if (!qt_index.isValid()) {
|
||||
dbgln("Failed to set DOM inspector selection! Could not find valid model index for node: {}", selection.dom_node_id);
|
||||
return;
|
||||
}
|
||||
|
||||
m_dom_tree_view->scrollTo(qt_index);
|
||||
m_dom_tree_view->setCurrentIndex(qt_index);
|
||||
}
|
||||
|
||||
void InspectorWidget::select_default_node()
|
||||
{
|
||||
clear_style_json();
|
||||
m_dom_tree_view->collapseAll();
|
||||
m_dom_tree_view->setCurrentIndex({});
|
||||
}
|
||||
|
||||
void InspectorWidget::set_selection(GUI::ModelIndex index)
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return;
|
||||
|
||||
auto* json = static_cast<JsonObject const*>(index.internal_data());
|
||||
VERIFY(json);
|
||||
|
||||
Selection selection {};
|
||||
if (json->has_u32("pseudo-element"sv)) {
|
||||
selection.dom_node_id = json->get_i32("parent-id"sv).value();
|
||||
selection.pseudo_element = static_cast<Web::CSS::Selector::PseudoElement>(json->get_u32("pseudo-element"sv).value());
|
||||
} else {
|
||||
selection.dom_node_id = json->get_i32("id"sv).value();
|
||||
}
|
||||
|
||||
if (selection == m_selection)
|
||||
return;
|
||||
m_selection = selection;
|
||||
|
||||
VERIFY(on_dom_node_inspected);
|
||||
auto maybe_inspected_node_properties = on_dom_node_inspected(m_selection.dom_node_id, m_selection.pseudo_element);
|
||||
if (!maybe_inspected_node_properties.is_error()) {
|
||||
auto properties = maybe_inspected_node_properties.release_value();
|
||||
load_style_json(properties.computed_style_json, properties.resolved_style_json, properties.custom_properties_json);
|
||||
} else {
|
||||
clear_style_json();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
69
Ladybird/Qt/InspectorWidget.h
Normal file
69
Ladybird/Qt/InspectorWidget.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ModelTranslator.h"
|
||||
#include "WebContentView.h"
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibWeb/CSS/Selector.h>
|
||||
#include <QWidget>
|
||||
|
||||
class QTreeView;
|
||||
class QTableView;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class InspectorWidget final : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
InspectorWidget();
|
||||
virtual ~InspectorWidget() = default;
|
||||
|
||||
struct Selection {
|
||||
i32 dom_node_id { 0 };
|
||||
Optional<Web::CSS::Selector::PseudoElement> pseudo_element {};
|
||||
bool operator==(Selection const& other) const = default;
|
||||
};
|
||||
|
||||
bool dom_loaded() const { return m_dom_loaded; }
|
||||
|
||||
void set_selection(Selection);
|
||||
void clear_selection();
|
||||
|
||||
void select_default_node();
|
||||
|
||||
void clear_dom_json();
|
||||
void set_dom_json(StringView dom_json);
|
||||
|
||||
void set_accessibility_json(StringView accessibility_json);
|
||||
|
||||
void load_style_json(StringView computed_style_json, StringView resolved_style_json, StringView custom_properties_json);
|
||||
void clear_style_json();
|
||||
|
||||
Function<ErrorOr<WebContentView::DOMNodeProperties>(i32, Optional<Web::CSS::Selector::PseudoElement>)> on_dom_node_inspected;
|
||||
Function<void()> on_close;
|
||||
|
||||
private:
|
||||
void set_selection(GUI::ModelIndex);
|
||||
void closeEvent(QCloseEvent*) override;
|
||||
|
||||
Selection m_selection;
|
||||
|
||||
ModelTranslator m_dom_model {};
|
||||
ModelTranslator m_accessibility_model {};
|
||||
ModelTranslator m_computed_style_model {};
|
||||
ModelTranslator m_resolved_style_model {};
|
||||
ModelTranslator m_custom_properties_model {};
|
||||
|
||||
QTreeView* m_dom_tree_view { nullptr };
|
||||
|
||||
bool m_dom_loaded { false };
|
||||
Optional<Selection> m_pending_selection {};
|
||||
};
|
||||
|
||||
}
|
92
Ladybird/Qt/LocationEdit.cpp
Normal file
92
Ladybird/Qt/LocationEdit.cpp
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LocationEdit.h"
|
||||
#include "StringUtils.h"
|
||||
#include <AK/URL.h>
|
||||
#include <QCoreApplication>
|
||||
#include <QPalette>
|
||||
#include <QTextLayout>
|
||||
#include <QTimer>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
LocationEdit::LocationEdit(QWidget* parent)
|
||||
: QLineEdit(parent)
|
||||
{
|
||||
setPlaceholderText("Enter web address");
|
||||
connect(this, &QLineEdit::returnPressed, this, [&] {
|
||||
clearFocus();
|
||||
});
|
||||
|
||||
connect(this, &QLineEdit::textChanged, this, [&] {
|
||||
highlight_location();
|
||||
});
|
||||
}
|
||||
|
||||
void LocationEdit::focusInEvent(QFocusEvent* event)
|
||||
{
|
||||
QLineEdit::focusInEvent(event);
|
||||
highlight_location();
|
||||
QTimer::singleShot(0, this, &QLineEdit::selectAll);
|
||||
}
|
||||
|
||||
void LocationEdit::focusOutEvent(QFocusEvent* event)
|
||||
{
|
||||
QLineEdit::focusOutEvent(event);
|
||||
highlight_location();
|
||||
}
|
||||
|
||||
void LocationEdit::highlight_location()
|
||||
{
|
||||
auto url = AK::URL::create_with_url_or_path(ak_deprecated_string_from_qstring(text()));
|
||||
|
||||
auto darkened_text_color = QPalette().color(QPalette::Text);
|
||||
darkened_text_color.setAlpha(127);
|
||||
|
||||
QList<QInputMethodEvent::Attribute> attributes;
|
||||
if (url.is_valid() && !hasFocus()) {
|
||||
if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "gemini") {
|
||||
int host_start = (url.scheme().length() + 3) - cursorPosition();
|
||||
auto host_length = url.serialized_host().release_value_but_fixme_should_propagate_errors().bytes().size();
|
||||
|
||||
// FIXME: Maybe add a generator to use https://publicsuffix.org/list/public_suffix_list.dat
|
||||
// for now just highlight the whole host
|
||||
|
||||
QTextCharFormat defaultFormat;
|
||||
defaultFormat.setForeground(darkened_text_color);
|
||||
attributes.append({
|
||||
QInputMethodEvent::TextFormat,
|
||||
-cursorPosition(),
|
||||
static_cast<int>(text().length()),
|
||||
defaultFormat,
|
||||
});
|
||||
|
||||
QTextCharFormat hostFormat;
|
||||
hostFormat.setForeground(QPalette().color(QPalette::Text));
|
||||
attributes.append({
|
||||
QInputMethodEvent::TextFormat,
|
||||
host_start,
|
||||
static_cast<int>(host_length),
|
||||
hostFormat,
|
||||
});
|
||||
} else if (url.scheme() == "file") {
|
||||
QTextCharFormat schemeFormat;
|
||||
schemeFormat.setForeground(darkened_text_color);
|
||||
attributes.append({
|
||||
QInputMethodEvent::TextFormat,
|
||||
-cursorPosition(),
|
||||
static_cast<int>(url.scheme().length() + 3),
|
||||
schemeFormat,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QInputMethodEvent event(QString(), attributes);
|
||||
QCoreApplication::sendEvent(this, &event);
|
||||
}
|
||||
|
||||
}
|
25
Ladybird/Qt/LocationEdit.h
Normal file
25
Ladybird/Qt/LocationEdit.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QLineEdit>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class LocationEdit final : public QLineEdit {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LocationEdit(QWidget*);
|
||||
|
||||
private:
|
||||
virtual void focusInEvent(QFocusEvent* event) override;
|
||||
virtual void focusOutEvent(QFocusEvent* event) override;
|
||||
|
||||
void highlight_location();
|
||||
};
|
||||
|
||||
}
|
85
Ladybird/Qt/ModelTranslator.cpp
Normal file
85
Ladybird/Qt/ModelTranslator.cpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ModelTranslator.h"
|
||||
#include "StringUtils.h"
|
||||
#include <QIcon>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
ModelTranslator::~ModelTranslator() = default;
|
||||
|
||||
int ModelTranslator::columnCount(QModelIndex const& parent) const
|
||||
{
|
||||
if (!m_model)
|
||||
return 0;
|
||||
return m_model->column_count(to_gui(parent));
|
||||
}
|
||||
|
||||
int ModelTranslator::rowCount(QModelIndex const& parent) const
|
||||
{
|
||||
if (!m_model)
|
||||
return 0;
|
||||
return m_model->row_count(to_gui(parent));
|
||||
}
|
||||
|
||||
static QVariant convert_variant(GUI::Variant const& value)
|
||||
{
|
||||
if (value.is_string())
|
||||
return qstring_from_ak_deprecated_string(value.as_string());
|
||||
if (value.is_icon()) {
|
||||
auto const& gui_icon = value.as_icon();
|
||||
auto bitmap = gui_icon.bitmap_for_size(16);
|
||||
VERIFY(bitmap);
|
||||
auto qt_image = QImage(bitmap->scanline_u8(0), 16, 16, QImage::Format_ARGB32);
|
||||
QIcon qt_icon;
|
||||
qt_icon.addPixmap(QPixmap::fromImage(qt_image.convertToFormat(QImage::Format::Format_ARGB32_Premultiplied)));
|
||||
return qt_icon;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariant ModelTranslator::data(QModelIndex const& index, int role) const
|
||||
{
|
||||
VERIFY(m_model);
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return convert_variant(m_model->data(to_gui(index), GUI::ModelRole::Display));
|
||||
case Qt::DecorationRole:
|
||||
return convert_variant(m_model->data(to_gui(index), GUI::ModelRole::Icon));
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex ModelTranslator::index(int row, int column, QModelIndex const& parent) const
|
||||
{
|
||||
VERIFY(m_model);
|
||||
return to_qt(m_model->index(row, column, to_gui(parent)));
|
||||
}
|
||||
|
||||
QModelIndex ModelTranslator::parent(QModelIndex const& index) const
|
||||
{
|
||||
VERIFY(m_model);
|
||||
return to_qt(m_model->parent_index(to_gui(index)));
|
||||
}
|
||||
|
||||
QModelIndex ModelTranslator::to_qt(GUI::ModelIndex const& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
return createIndex(index.row(), index.column(), index.internal_data());
|
||||
}
|
||||
|
||||
GUI::ModelIndex ModelTranslator::to_gui(QModelIndex const& index) const
|
||||
{
|
||||
VERIFY(m_model);
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
return m_model->unsafe_create_index(index.row(), index.column(), index.internalPointer());
|
||||
}
|
||||
|
||||
}
|
44
Ladybird/Qt/ModelTranslator.h
Normal file
44
Ladybird/Qt/ModelTranslator.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/Model.h>
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class ModelTranslator final : public QAbstractItemModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
virtual ~ModelTranslator() override;
|
||||
|
||||
void set_underlying_model(RefPtr<GUI::Model> model)
|
||||
{
|
||||
beginResetModel();
|
||||
m_model = model;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
RefPtr<GUI::Model> underlying_model()
|
||||
{
|
||||
return m_model;
|
||||
}
|
||||
|
||||
virtual int columnCount(QModelIndex const& parent) const override;
|
||||
virtual int rowCount(QModelIndex const& parent) const override;
|
||||
virtual QVariant data(QModelIndex const&, int role) const override;
|
||||
virtual QModelIndex index(int row, int column, QModelIndex const& parent) const override;
|
||||
virtual QModelIndex parent(QModelIndex const& index) const override;
|
||||
|
||||
QModelIndex to_qt(GUI::ModelIndex const&) const;
|
||||
GUI::ModelIndex to_gui(QModelIndex const&) const;
|
||||
|
||||
private:
|
||||
RefPtr<GUI::Model> m_model;
|
||||
};
|
||||
|
||||
}
|
113
Ladybird/Qt/RequestManagerQt.cpp
Normal file
113
Ladybird/Qt/RequestManagerQt.cpp
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "RequestManagerQt.h"
|
||||
#include <AK/JsonObject.h>
|
||||
#include <QNetworkCookie>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
RequestManagerQt::RequestManagerQt()
|
||||
{
|
||||
m_qnam = new QNetworkAccessManager(this);
|
||||
|
||||
QObject::connect(m_qnam, &QNetworkAccessManager::finished, this, &RequestManagerQt::reply_finished);
|
||||
}
|
||||
|
||||
void RequestManagerQt::reply_finished(QNetworkReply* reply)
|
||||
{
|
||||
auto request = m_pending.get(reply).value();
|
||||
m_pending.remove(reply);
|
||||
request->did_finish();
|
||||
}
|
||||
|
||||
RefPtr<Web::ResourceLoaderConnectorRequest> RequestManagerQt::start_request(DeprecatedString const& method, AK::URL const& url, HashMap<DeprecatedString, DeprecatedString> const& request_headers, ReadonlyBytes request_body, Core::ProxyData const& proxy)
|
||||
{
|
||||
if (!url.scheme().is_one_of_ignoring_ascii_case("http"sv, "https"sv)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto request_or_error = Request::create(*m_qnam, method, url, request_headers, request_body, proxy);
|
||||
if (request_or_error.is_error()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto request = request_or_error.release_value();
|
||||
m_pending.set(&request->reply(), *request);
|
||||
return request;
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<RequestManagerQt::Request>> RequestManagerQt::Request::create(QNetworkAccessManager& qnam, DeprecatedString const& method, AK::URL const& url, HashMap<DeprecatedString, DeprecatedString> const& request_headers, ReadonlyBytes request_body, Core::ProxyData const&)
|
||||
{
|
||||
QNetworkRequest request { QString(url.to_deprecated_string().characters()) };
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
|
||||
request.setAttribute(QNetworkRequest::CookieLoadControlAttribute, QNetworkRequest::Manual);
|
||||
request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual);
|
||||
|
||||
// NOTE: We disable HTTP2 as it's significantly slower (up to 5x, possibly more)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
|
||||
QNetworkReply* reply = nullptr;
|
||||
|
||||
for (auto& it : request_headers) {
|
||||
// FIXME: We currently strip the Accept-Encoding header on outgoing requests from LibWeb
|
||||
// since otherwise it'll ask for compression without Qt being aware of it.
|
||||
// This is very hackish and I'm sure we can do it in concert with Qt somehow.
|
||||
if (it.key == "Accept-Encoding")
|
||||
continue;
|
||||
request.setRawHeader(QByteArray(it.key.characters()), QByteArray(it.value.characters()));
|
||||
}
|
||||
|
||||
if (method.equals_ignoring_ascii_case("head"sv)) {
|
||||
reply = qnam.head(request);
|
||||
} else if (method.equals_ignoring_ascii_case("get"sv)) {
|
||||
reply = qnam.get(request);
|
||||
} else if (method.equals_ignoring_ascii_case("post"sv)) {
|
||||
reply = qnam.post(request, QByteArray((char const*)request_body.data(), request_body.size()));
|
||||
} else if (method.equals_ignoring_ascii_case("put"sv)) {
|
||||
reply = qnam.put(request, QByteArray((char const*)request_body.data(), request_body.size()));
|
||||
} else if (method.equals_ignoring_ascii_case("delete"sv)) {
|
||||
reply = qnam.deleteResource(request);
|
||||
} else {
|
||||
reply = qnam.sendCustomRequest(request, QByteArray(method.characters()), QByteArray((char const*)request_body.data(), request_body.size()));
|
||||
}
|
||||
|
||||
return adopt_ref(*new Request(*reply));
|
||||
}
|
||||
|
||||
RequestManagerQt::Request::Request(QNetworkReply& reply)
|
||||
: m_reply(reply)
|
||||
{
|
||||
}
|
||||
|
||||
RequestManagerQt::Request::~Request() = default;
|
||||
|
||||
void RequestManagerQt::Request::did_finish()
|
||||
{
|
||||
auto buffer = m_reply.readAll();
|
||||
auto http_status_code = m_reply.attribute(QNetworkRequest::Attribute::HttpStatusCodeAttribute).toInt();
|
||||
HashMap<DeprecatedString, DeprecatedString, CaseInsensitiveStringTraits> response_headers;
|
||||
Vector<DeprecatedString> set_cookie_headers;
|
||||
for (auto& it : m_reply.rawHeaderPairs()) {
|
||||
auto name = DeprecatedString(it.first.data(), it.first.length());
|
||||
auto value = DeprecatedString(it.second.data(), it.second.length());
|
||||
if (name.equals_ignoring_ascii_case("set-cookie"sv)) {
|
||||
// NOTE: Qt may have bundled multiple Set-Cookie headers into a single one.
|
||||
// We have to extract the full list of cookies via QNetworkReply::header().
|
||||
auto set_cookie_list = m_reply.header(QNetworkRequest::SetCookieHeader).value<QList<QNetworkCookie>>();
|
||||
for (auto const& cookie : set_cookie_list) {
|
||||
set_cookie_headers.append(cookie.toRawForm().data());
|
||||
}
|
||||
} else {
|
||||
response_headers.set(name, value);
|
||||
}
|
||||
}
|
||||
if (!set_cookie_headers.is_empty()) {
|
||||
response_headers.set("set-cookie"sv, JsonArray { set_cookie_headers }.to_deprecated_string());
|
||||
}
|
||||
bool success = http_status_code != 0;
|
||||
on_buffered_request_finish(success, buffer.length(), response_headers, http_status_code, ReadonlyBytes { buffer.data(), (size_t)buffer.size() });
|
||||
}
|
||||
|
||||
}
|
63
Ladybird/Qt/RequestManagerQt.h
Normal file
63
Ladybird/Qt/RequestManagerQt.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Loader/ResourceLoader.h>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class RequestManagerQt
|
||||
: public QObject
|
||||
, public Web::ResourceLoaderConnector {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static NonnullRefPtr<RequestManagerQt> create()
|
||||
{
|
||||
return adopt_ref(*new RequestManagerQt());
|
||||
}
|
||||
|
||||
virtual ~RequestManagerQt() override { }
|
||||
|
||||
virtual void prefetch_dns(AK::URL const&) override { }
|
||||
virtual void preconnect(AK::URL const&) override { }
|
||||
|
||||
virtual RefPtr<Web::ResourceLoaderConnectorRequest> start_request(DeprecatedString const& method, AK::URL const&, HashMap<DeprecatedString, DeprecatedString> const& request_headers, ReadonlyBytes request_body, Core::ProxyData const&) override;
|
||||
|
||||
private slots:
|
||||
void reply_finished(QNetworkReply*);
|
||||
|
||||
private:
|
||||
RequestManagerQt();
|
||||
|
||||
class Request
|
||||
: public Web::ResourceLoaderConnectorRequest {
|
||||
public:
|
||||
static ErrorOr<NonnullRefPtr<Request>> create(QNetworkAccessManager& qnam, DeprecatedString const& method, AK::URL const& url, HashMap<DeprecatedString, DeprecatedString> const& request_headers, ReadonlyBytes request_body, Core::ProxyData const&);
|
||||
|
||||
virtual ~Request() override;
|
||||
|
||||
virtual void set_should_buffer_all_input(bool) override { }
|
||||
virtual bool stop() override { return false; }
|
||||
virtual void stream_into(Stream&) override { }
|
||||
|
||||
void did_finish();
|
||||
|
||||
QNetworkReply& reply() { return m_reply; }
|
||||
|
||||
private:
|
||||
Request(QNetworkReply&);
|
||||
|
||||
QNetworkReply& m_reply;
|
||||
};
|
||||
|
||||
HashMap<QNetworkReply*, NonnullRefPtr<Request>> m_pending;
|
||||
QNetworkAccessManager* m_qnam { nullptr };
|
||||
};
|
||||
|
||||
}
|
47
Ladybird/Qt/Settings.cpp
Normal file
47
Ladybird/Qt/Settings.cpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Settings.h"
|
||||
#include "StringUtils.h"
|
||||
#include <AK/URL.h>
|
||||
#include <BrowserSettings/Defaults.h>
|
||||
#include <Ladybird/Utilities.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
static QString rebase_default_url_on_serenity_resource_root(StringView default_url)
|
||||
{
|
||||
URL url { default_url };
|
||||
Vector<DeprecatedString> paths;
|
||||
|
||||
for (auto segment : s_serenity_resource_root.split('/'))
|
||||
paths.append(move(segment));
|
||||
|
||||
for (size_t i = 0; i < url.path_segment_count(); ++i)
|
||||
paths.append(url.path_segment_at_index(i));
|
||||
|
||||
url.set_paths(move(paths));
|
||||
|
||||
return qstring_from_ak_deprecated_string(url.to_deprecated_string());
|
||||
}
|
||||
|
||||
Settings::Settings()
|
||||
{
|
||||
m_qsettings = new QSettings("Serenity", "Ladybird", this);
|
||||
}
|
||||
|
||||
QString Settings::new_tab_page()
|
||||
{
|
||||
static auto const default_new_tab_url = rebase_default_url_on_serenity_resource_root(Browser::default_new_tab_url);
|
||||
return m_qsettings->value("new_tab_page", default_new_tab_url).toString();
|
||||
}
|
||||
|
||||
void Settings::set_new_tab_page(QString const& page)
|
||||
{
|
||||
m_qsettings->setValue("new_tab_page", page);
|
||||
}
|
||||
|
||||
}
|
25
Ladybird/Qt/Settings.h
Normal file
25
Ladybird/Qt/Settings.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <QSettings>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class Settings : public QObject {
|
||||
public:
|
||||
Settings();
|
||||
|
||||
QString new_tab_page();
|
||||
void set_new_tab_page(QString const& page);
|
||||
|
||||
private:
|
||||
QSettings* m_qsettings;
|
||||
};
|
||||
|
||||
}
|
50
Ladybird/Qt/SettingsDialog.cpp
Normal file
50
Ladybird/Qt/SettingsDialog.cpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "SettingsDialog.h"
|
||||
#include "Settings.h"
|
||||
#include <QCloseEvent>
|
||||
#include <QLabel>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
extern Settings* s_settings;
|
||||
|
||||
SettingsDialog::SettingsDialog(QMainWindow* window)
|
||||
: m_window(window)
|
||||
{
|
||||
m_layout = new QFormLayout(this);
|
||||
m_new_tab_page = new QLineEdit(this);
|
||||
m_ok_button = new QPushButton("&Save", this);
|
||||
|
||||
m_layout->addRow(new QLabel("Page on New Tab", this), m_new_tab_page);
|
||||
m_layout->addWidget(m_ok_button);
|
||||
|
||||
QObject::connect(m_ok_button, &QPushButton::released, this, [this] {
|
||||
close();
|
||||
});
|
||||
|
||||
setWindowTitle("Settings");
|
||||
setFixedWidth(300);
|
||||
setFixedHeight(150);
|
||||
setLayout(m_layout);
|
||||
show();
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void SettingsDialog::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
save();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void SettingsDialog::save()
|
||||
{
|
||||
// FIXME: Validate data.
|
||||
s_settings->set_new_tab_page(m_new_tab_page->text());
|
||||
}
|
||||
|
||||
}
|
33
Ladybird/Qt/SettingsDialog.h
Normal file
33
Ladybird/Qt/SettingsDialog.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QPushButton>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class SettingsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(QMainWindow* window);
|
||||
|
||||
void save();
|
||||
|
||||
virtual void closeEvent(QCloseEvent*) override;
|
||||
|
||||
private:
|
||||
QFormLayout* m_layout;
|
||||
QPushButton* m_ok_button { nullptr };
|
||||
QLineEdit* m_new_tab_page { nullptr };
|
||||
QMainWindow* m_window { nullptr };
|
||||
};
|
||||
|
||||
}
|
28
Ladybird/Qt/StringUtils.cpp
Normal file
28
Ladybird/Qt/StringUtils.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "StringUtils.h"
|
||||
|
||||
AK::DeprecatedString ak_deprecated_string_from_qstring(QString const& qstring)
|
||||
{
|
||||
return AK::DeprecatedString(qstring.toUtf8().data());
|
||||
}
|
||||
|
||||
ErrorOr<String> ak_string_from_qstring(QString const& qstring)
|
||||
{
|
||||
return String::from_utf8(StringView(qstring.toUtf8().data(), qstring.size()));
|
||||
}
|
||||
|
||||
QString qstring_from_ak_deprecated_string(AK::DeprecatedString const& ak_deprecated_string)
|
||||
{
|
||||
return QString::fromUtf8(ak_deprecated_string.characters(), ak_deprecated_string.length());
|
||||
}
|
||||
|
||||
QString qstring_from_ak_string(String const& ak_string)
|
||||
{
|
||||
auto view = ak_string.bytes_as_string_view();
|
||||
return QString::fromUtf8(view.characters_without_null_termination(), view.length());
|
||||
}
|
17
Ladybird/Qt/StringUtils.h
Normal file
17
Ladybird/Qt/StringUtils.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <AK/Error.h>
|
||||
#include <AK/String.h>
|
||||
#include <QString>
|
||||
|
||||
AK::DeprecatedString ak_deprecated_string_from_qstring(QString const&);
|
||||
ErrorOr<String> ak_string_from_qstring(QString const&);
|
||||
QString qstring_from_ak_deprecated_string(AK::DeprecatedString const&);
|
||||
QString qstring_from_ak_string(String const&);
|
75
Ladybird/Qt/TVGIconEngine.cpp
Normal file
75
Ladybird/Qt/TVGIconEngine.cpp
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "TVGIconEngine.h"
|
||||
#include "StringUtils.h"
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibGfx/Painter.h>
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
void TVGIconEngine::paint(QPainter* qpainter, QRect const& rect, QIcon::Mode mode, QIcon::State state)
|
||||
{
|
||||
qpainter->drawPixmap(rect, pixmap(rect.size(), mode, state));
|
||||
}
|
||||
|
||||
QIconEngine* TVGIconEngine::clone() const
|
||||
{
|
||||
return new TVGIconEngine(*this);
|
||||
}
|
||||
|
||||
QPixmap TVGIconEngine::pixmap(QSize const& size, QIcon::Mode mode, QIcon::State state)
|
||||
{
|
||||
QPixmap pixmap;
|
||||
auto key = pixmap_cache_key(size, mode, state);
|
||||
if (QPixmapCache::find(key, &pixmap))
|
||||
return pixmap;
|
||||
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { size.width(), size.height() }));
|
||||
Gfx::Painter painter { *bitmap };
|
||||
m_image_data->draw_into(painter, bitmap->rect());
|
||||
for (auto const& filter : m_filters) {
|
||||
if (filter->mode() == mode) {
|
||||
painter.blit_filtered({}, *bitmap, bitmap->rect(), filter->function(), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QImage qimage { bitmap->scanline_u8(0), bitmap->width(), bitmap->height(), QImage::Format::Format_ARGB32 };
|
||||
pixmap = QPixmap::fromImage(qimage);
|
||||
if (!pixmap.isNull())
|
||||
QPixmapCache::insert(key, pixmap);
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QString TVGIconEngine::pixmap_cache_key(QSize const& size, QIcon::Mode mode, QIcon::State state)
|
||||
{
|
||||
return qstring_from_ak_string(
|
||||
MUST(String::formatted("$sernity_tvgicon_{}_{}x{}_{}_{}", m_cache_id, size.width(), size.height(), to_underlying(mode), to_underlying(state))));
|
||||
}
|
||||
|
||||
void TVGIconEngine::add_filter(QIcon::Mode mode, Function<Color(Color)> filter)
|
||||
{
|
||||
m_filters.empend(adopt_ref(*new Filter(mode, move(filter))));
|
||||
invalidate_cache();
|
||||
}
|
||||
|
||||
TVGIconEngine* TVGIconEngine::from_file(QString const& path)
|
||||
{
|
||||
QFile icon_resource(path);
|
||||
if (!icon_resource.open(QIODeviceBase::ReadOnly))
|
||||
return nullptr;
|
||||
auto icon_data = icon_resource.readAll();
|
||||
FixedMemoryStream icon_bytes { ReadonlyBytes { icon_data.data(), static_cast<size_t>(icon_data.size()) } };
|
||||
if (auto tvg = Gfx::TinyVGDecodedImageData::decode(icon_bytes); !tvg.is_error())
|
||||
return new TVGIconEngine(tvg.release_value());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
68
Ladybird/Qt/TVGIconEngine.h
Normal file
68
Ladybird/Qt/TVGIconEngine.h
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/ImageFormats/TinyVGLoader.h>
|
||||
#include <QIconEngine>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class TVGIconEngine : public QIconEngine {
|
||||
public:
|
||||
TVGIconEngine(Gfx::TinyVGDecodedImageData const& image_data)
|
||||
: m_image_data(image_data)
|
||||
{
|
||||
}
|
||||
|
||||
static TVGIconEngine* from_file(QString const& path);
|
||||
|
||||
void paint(QPainter* painter, QRect const& rect, QIcon::Mode mode,
|
||||
QIcon::State state) override;
|
||||
QIconEngine* clone() const override;
|
||||
QPixmap pixmap(QSize const& size, QIcon::Mode mode,
|
||||
QIcon::State state) override;
|
||||
|
||||
void add_filter(QIcon::Mode mode, Function<Color(Color)> filter);
|
||||
|
||||
private:
|
||||
static unsigned next_cache_id()
|
||||
{
|
||||
static unsigned cache_id = 0;
|
||||
return cache_id++;
|
||||
}
|
||||
|
||||
void invalidate_cache()
|
||||
{
|
||||
m_cache_id = next_cache_id();
|
||||
}
|
||||
|
||||
class Filter : public RefCounted<Filter> {
|
||||
public:
|
||||
Filter(QIcon::Mode mode, Function<Color(Color)> function)
|
||||
: m_mode(mode)
|
||||
, m_function(move(function))
|
||||
{
|
||||
}
|
||||
QIcon::Mode mode() const { return m_mode; }
|
||||
Function<Color(Color)> const& function() const { return m_function; }
|
||||
|
||||
private:
|
||||
QIcon::Mode m_mode;
|
||||
Function<Color(Color)> m_function;
|
||||
};
|
||||
|
||||
QString pixmap_cache_key(QSize const& size, QIcon::Mode mode, QIcon::State state);
|
||||
|
||||
Vector<NonnullRefPtr<Filter>> m_filters;
|
||||
NonnullRefPtr<Gfx::TinyVGDecodedImageData> m_image_data;
|
||||
unsigned m_cache_id { next_cache_id() };
|
||||
};
|
||||
|
||||
}
|
700
Ladybird/Qt/Tab.cpp
Normal file
700
Ladybird/Qt/Tab.cpp
Normal file
|
@ -0,0 +1,700 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "BrowserWindow.h"
|
||||
#include "ConsoleWidget.h"
|
||||
#include "InspectorWidget.h"
|
||||
#include "Settings.h"
|
||||
#include "StringUtils.h"
|
||||
#include "TVGIconEngine.h"
|
||||
#include <Browser/History.h>
|
||||
#include <LibGfx/ImageFormats/BMPWriter.h>
|
||||
#include <LibGfx/Painter.h>
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QCursor>
|
||||
#include <QFileDialog>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QGuiApplication>
|
||||
#include <QImage>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPoint>
|
||||
#include <QResizeEvent>
|
||||
|
||||
extern DeprecatedString s_serenity_resource_root;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
extern Settings* s_settings;
|
||||
|
||||
static QIcon create_tvg_icon_with_theme_colors(QString name, QPalette const& palette)
|
||||
{
|
||||
auto path = QString(":/Icons/%1.tvg").arg(name);
|
||||
auto icon_engine = TVGIconEngine::from_file(path);
|
||||
VERIFY(icon_engine);
|
||||
auto icon_filter = [](QColor color) {
|
||||
return [color = Color::from_argb(color.rgba64().toArgb32())](Gfx::Color icon_color) {
|
||||
return color.with_alpha((icon_color.alpha() * color.alpha()) / 255);
|
||||
};
|
||||
};
|
||||
icon_engine->add_filter(QIcon::Mode::Normal, icon_filter(palette.color(QPalette::ColorGroup::Normal, QPalette::ColorRole::ButtonText)));
|
||||
icon_engine->add_filter(QIcon::Mode::Disabled, icon_filter(palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::ButtonText)));
|
||||
return QIcon(icon_engine);
|
||||
}
|
||||
|
||||
Tab::Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling enable_callgrind_profiling, WebView::UseJavaScriptBytecode use_javascript_bytecode, UseLagomNetworking use_lagom_networking)
|
||||
: QWidget(window)
|
||||
, m_window(window)
|
||||
{
|
||||
m_layout = new QBoxLayout(QBoxLayout::Direction::TopToBottom, this);
|
||||
m_layout->setSpacing(0);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
m_view = new WebContentView(webdriver_content_ipc_path, enable_callgrind_profiling, use_javascript_bytecode, use_lagom_networking);
|
||||
m_toolbar = new QToolBar(this);
|
||||
m_location_edit = new LocationEdit(this);
|
||||
|
||||
m_hover_label = new QLabel(this);
|
||||
m_hover_label->hide();
|
||||
m_hover_label->setFrameShape(QFrame::Shape::Box);
|
||||
m_hover_label->setAutoFillBackground(true);
|
||||
|
||||
auto* focus_location_editor_action = new QAction("Edit Location", this);
|
||||
focus_location_editor_action->setShortcut(QKeySequence("Ctrl+L"));
|
||||
addAction(focus_location_editor_action);
|
||||
|
||||
m_layout->addWidget(m_toolbar);
|
||||
m_layout->addWidget(m_view);
|
||||
|
||||
recreate_toolbar_icons();
|
||||
|
||||
m_toolbar->addAction(&m_window->go_back_action());
|
||||
m_toolbar->addAction(&m_window->go_forward_action());
|
||||
m_toolbar->addAction(&m_window->reload_action());
|
||||
m_toolbar->addWidget(m_location_edit);
|
||||
m_toolbar->setIconSize({ 16, 16 });
|
||||
// This is a little awkward, but without this Qt shrinks the button to the size of the icon.
|
||||
// Note: toolButtonStyle="0" -> ToolButtonIconOnly.
|
||||
m_toolbar->setStyleSheet("QToolButton[toolButtonStyle=\"0\"]{width:24px;height:24px}");
|
||||
|
||||
m_reset_zoom_button = new QToolButton(m_toolbar);
|
||||
m_reset_zoom_button->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||
m_reset_zoom_button->setToolTip("Reset zoom level");
|
||||
m_reset_zoom_button_action = m_toolbar->addWidget(m_reset_zoom_button);
|
||||
m_reset_zoom_button_action->setVisible(false);
|
||||
|
||||
QObject::connect(m_reset_zoom_button, &QAbstractButton::clicked, [this] {
|
||||
view().reset_zoom();
|
||||
update_reset_zoom_button();
|
||||
m_window->update_zoom_menu();
|
||||
});
|
||||
|
||||
view().on_activate_tab = [this] {
|
||||
m_window->activate_tab(tab_index());
|
||||
};
|
||||
|
||||
view().on_close = [this] {
|
||||
m_window->close_tab(tab_index());
|
||||
};
|
||||
|
||||
view().on_link_hover = [this](auto const& url) {
|
||||
m_hover_label->setText(qstring_from_ak_deprecated_string(url.to_deprecated_string()));
|
||||
update_hover_label();
|
||||
m_hover_label->show();
|
||||
};
|
||||
|
||||
view().on_link_unhover = [this]() {
|
||||
m_hover_label->hide();
|
||||
};
|
||||
|
||||
view().on_back_button = [this] {
|
||||
back();
|
||||
};
|
||||
|
||||
view().on_forward_button = [this] {
|
||||
forward();
|
||||
};
|
||||
|
||||
view().on_load_start = [this](const URL& url, bool is_redirect) {
|
||||
// If we are loading due to a redirect, we replace the current history entry
|
||||
// with the loaded URL
|
||||
if (is_redirect) {
|
||||
m_history.replace_current(url, m_title.toUtf8().data());
|
||||
}
|
||||
|
||||
m_location_edit->setText(url.to_deprecated_string().characters());
|
||||
m_location_edit->setCursorPosition(0);
|
||||
|
||||
// Don't add to history if back or forward is pressed
|
||||
if (!m_is_history_navigation) {
|
||||
m_history.push(url, m_title.toUtf8().data());
|
||||
}
|
||||
m_is_history_navigation = false;
|
||||
|
||||
m_window->go_back_action().setEnabled(m_history.can_go_back());
|
||||
m_window->go_forward_action().setEnabled(m_history.can_go_forward());
|
||||
|
||||
if (m_inspector_widget)
|
||||
m_inspector_widget->clear_dom_json();
|
||||
|
||||
if (m_console_widget)
|
||||
m_console_widget->reset();
|
||||
};
|
||||
|
||||
view().on_load_finish = [this](auto&) {
|
||||
if (m_inspector_widget != nullptr && m_inspector_widget->isVisible()) {
|
||||
view().inspect_dom_tree();
|
||||
view().inspect_accessibility_tree();
|
||||
}
|
||||
};
|
||||
|
||||
QObject::connect(m_location_edit, &QLineEdit::returnPressed, this, &Tab::location_edit_return_pressed);
|
||||
|
||||
view().on_title_change = [this](auto const& title) {
|
||||
m_title = qstring_from_ak_deprecated_string(title);
|
||||
m_history.update_title(title);
|
||||
|
||||
emit title_changed(tab_index(), m_title);
|
||||
};
|
||||
|
||||
view().on_favicon_change = [this](auto const& bitmap) {
|
||||
auto qimage = QImage(bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32);
|
||||
if (qimage.isNull())
|
||||
return;
|
||||
auto qpixmap = QPixmap::fromImage(qimage);
|
||||
if (qpixmap.isNull())
|
||||
return;
|
||||
emit favicon_changed(tab_index(), QIcon(qpixmap));
|
||||
};
|
||||
|
||||
QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
|
||||
|
||||
view().on_get_source = [this](auto const& url, auto const& source) {
|
||||
auto* text_edit = new QPlainTextEdit(this);
|
||||
text_edit->setWindowFlags(Qt::Window);
|
||||
text_edit->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont));
|
||||
text_edit->resize(800, 600);
|
||||
text_edit->setWindowTitle(qstring_from_ak_deprecated_string(url.to_deprecated_string()));
|
||||
text_edit->setPlainText(qstring_from_ak_deprecated_string(source));
|
||||
text_edit->show();
|
||||
};
|
||||
|
||||
view().on_navigate_back = [this]() {
|
||||
back();
|
||||
};
|
||||
|
||||
view().on_navigate_forward = [this]() {
|
||||
forward();
|
||||
};
|
||||
|
||||
view().on_refresh = [this]() {
|
||||
reload();
|
||||
};
|
||||
|
||||
view().on_restore_window = [this]() {
|
||||
m_window->showNormal();
|
||||
};
|
||||
|
||||
view().on_reposition_window = [this](auto const& position) {
|
||||
m_window->move(position.x(), position.y());
|
||||
return Gfx::IntPoint { m_window->x(), m_window->y() };
|
||||
};
|
||||
|
||||
view().on_resize_window = [this](auto const& size) {
|
||||
m_window->resize(size.width(), size.height());
|
||||
return Gfx::IntSize { m_window->width(), m_window->height() };
|
||||
};
|
||||
|
||||
view().on_maximize_window = [this]() {
|
||||
m_window->showMaximized();
|
||||
return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
|
||||
};
|
||||
|
||||
view().on_minimize_window = [this]() {
|
||||
m_window->showMinimized();
|
||||
return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
|
||||
};
|
||||
|
||||
view().on_fullscreen_window = [this]() {
|
||||
m_window->showFullScreen();
|
||||
return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
|
||||
};
|
||||
|
||||
view().on_get_dom_tree = [this](auto& dom_tree) {
|
||||
if (m_inspector_widget)
|
||||
m_inspector_widget->set_dom_json(dom_tree);
|
||||
};
|
||||
|
||||
view().on_get_accessibility_tree = [this](auto& accessibility_tree) {
|
||||
if (m_inspector_widget)
|
||||
m_inspector_widget->set_accessibility_json(accessibility_tree);
|
||||
};
|
||||
|
||||
view().on_js_console_new_message = [this](auto message_index) {
|
||||
if (m_console_widget)
|
||||
m_console_widget->notify_about_new_console_message(message_index);
|
||||
};
|
||||
|
||||
view().on_get_js_console_messages = [this](auto start_index, auto& message_types, auto& messages) {
|
||||
if (m_console_widget)
|
||||
m_console_widget->handle_console_messages(start_index, message_types, messages);
|
||||
};
|
||||
|
||||
auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this);
|
||||
take_visible_screenshot_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [this]() {
|
||||
if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible); result.is_error()) {
|
||||
auto error = String::formatted("{}", result.error()).release_value_but_fixme_should_propagate_errors();
|
||||
QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error));
|
||||
}
|
||||
});
|
||||
|
||||
auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this);
|
||||
take_full_screenshot_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [this]() {
|
||||
if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Full); result.is_error()) {
|
||||
auto error = String::formatted("{}", result.error()).release_value_but_fixme_should_propagate_errors();
|
||||
QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error));
|
||||
}
|
||||
});
|
||||
|
||||
m_page_context_menu = make<QMenu>("Context menu", this);
|
||||
m_page_context_menu->addAction(&m_window->go_back_action());
|
||||
m_page_context_menu->addAction(&m_window->go_forward_action());
|
||||
m_page_context_menu->addAction(&m_window->reload_action());
|
||||
m_page_context_menu->addSeparator();
|
||||
m_page_context_menu->addAction(&m_window->copy_selection_action());
|
||||
m_page_context_menu->addAction(&m_window->select_all_action());
|
||||
m_page_context_menu->addSeparator();
|
||||
m_page_context_menu->addAction(take_visible_screenshot_action);
|
||||
m_page_context_menu->addAction(take_full_screenshot_action);
|
||||
m_page_context_menu->addSeparator();
|
||||
m_page_context_menu->addAction(&m_window->view_source_action());
|
||||
m_page_context_menu->addAction(&m_window->inspect_dom_node_action());
|
||||
|
||||
view().on_context_menu_request = [this](Gfx::IntPoint) {
|
||||
auto screen_position = QCursor::pos();
|
||||
m_page_context_menu->exec(screen_position);
|
||||
};
|
||||
|
||||
auto* open_link_action = new QAction("&Open", this);
|
||||
open_link_action->setIcon(QIcon(QString("%1/res/icons/16x16/go-forward.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_link_action, &QAction::triggered, this, [this]() {
|
||||
open_link(m_link_context_menu_url);
|
||||
});
|
||||
|
||||
auto* open_link_in_new_tab_action = new QAction("&Open in New &Tab", this);
|
||||
open_link_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_link_in_new_tab_action, &QAction::triggered, this, [this]() {
|
||||
open_link_in_new_tab(m_link_context_menu_url);
|
||||
});
|
||||
|
||||
auto* copy_url_action = new QAction("Copy &URL", this);
|
||||
copy_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(copy_url_action, &QAction::triggered, this, [this]() {
|
||||
copy_link_url(m_link_context_menu_url);
|
||||
});
|
||||
|
||||
m_link_context_menu = make<QMenu>("Link context menu", this);
|
||||
m_link_context_menu->addAction(open_link_action);
|
||||
m_link_context_menu->addAction(open_link_in_new_tab_action);
|
||||
m_link_context_menu->addSeparator();
|
||||
m_link_context_menu->addAction(copy_url_action);
|
||||
m_link_context_menu->addSeparator();
|
||||
m_link_context_menu->addAction(&m_window->inspect_dom_node_action());
|
||||
|
||||
view().on_link_context_menu_request = [this](auto const& url, Gfx::IntPoint) {
|
||||
m_link_context_menu_url = url;
|
||||
|
||||
auto screen_position = QCursor::pos();
|
||||
m_link_context_menu->exec(screen_position);
|
||||
};
|
||||
|
||||
auto* open_image_action = new QAction("&Open Image", this);
|
||||
open_image_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_image_action, &QAction::triggered, this, [this]() {
|
||||
open_link(m_image_context_menu_url);
|
||||
});
|
||||
|
||||
auto* open_image_in_new_tab_action = new QAction("&Open Image in New &Tab", this);
|
||||
open_image_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_image_in_new_tab_action, &QAction::triggered, this, [this]() {
|
||||
open_link_in_new_tab(m_image_context_menu_url);
|
||||
});
|
||||
|
||||
auto* copy_image_action = new QAction("&Copy Image", this);
|
||||
copy_image_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(copy_image_action, &QAction::triggered, this, [this]() {
|
||||
auto* bitmap = m_image_context_menu_bitmap.bitmap();
|
||||
if (bitmap == nullptr)
|
||||
return;
|
||||
|
||||
auto data = Gfx::BMPWriter::encode(*bitmap);
|
||||
if (data.is_error())
|
||||
return;
|
||||
|
||||
auto image = QImage::fromData(data.value().data(), data.value().size(), "BMP");
|
||||
if (image.isNull())
|
||||
return;
|
||||
|
||||
auto* clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setImage(image);
|
||||
});
|
||||
|
||||
auto* copy_image_url_action = new QAction("Copy Image &URL", this);
|
||||
copy_image_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(copy_image_url_action, &QAction::triggered, this, [this]() {
|
||||
copy_link_url(m_image_context_menu_url);
|
||||
});
|
||||
|
||||
m_image_context_menu = make<QMenu>("Image context menu", this);
|
||||
m_image_context_menu->addAction(open_image_action);
|
||||
m_image_context_menu->addAction(open_image_in_new_tab_action);
|
||||
m_image_context_menu->addSeparator();
|
||||
m_image_context_menu->addAction(copy_image_action);
|
||||
m_image_context_menu->addAction(copy_image_url_action);
|
||||
m_image_context_menu->addSeparator();
|
||||
m_image_context_menu->addAction(&m_window->inspect_dom_node_action());
|
||||
|
||||
view().on_image_context_menu_request = [this](auto& image_url, Gfx::IntPoint, Gfx::ShareableBitmap const& shareable_bitmap) {
|
||||
m_image_context_menu_url = image_url;
|
||||
m_image_context_menu_bitmap = shareable_bitmap;
|
||||
|
||||
auto screen_position = QCursor::pos();
|
||||
m_image_context_menu->exec(screen_position);
|
||||
};
|
||||
|
||||
m_media_context_menu_play_icon = make<QIcon>(QString("%1/res/icons/16x16/play.png").arg(s_serenity_resource_root.characters()));
|
||||
m_media_context_menu_pause_icon = make<QIcon>(QString("%1/res/icons/16x16/pause.png").arg(s_serenity_resource_root.characters()));
|
||||
m_media_context_menu_mute_icon = make<QIcon>(QString("%1/res/icons/16x16/audio-volume-muted.png").arg(s_serenity_resource_root.characters()));
|
||||
m_media_context_menu_unmute_icon = make<QIcon>(QString("%1/res/icons/16x16/audio-volume-high.png").arg(s_serenity_resource_root.characters()));
|
||||
|
||||
m_media_context_menu_play_pause_action = make<QAction>("&Play", this);
|
||||
m_media_context_menu_play_pause_action->setIcon(*m_media_context_menu_play_icon);
|
||||
QObject::connect(m_media_context_menu_play_pause_action, &QAction::triggered, this, [this]() {
|
||||
view().toggle_media_play_state();
|
||||
});
|
||||
|
||||
m_media_context_menu_mute_unmute_action = make<QAction>("&Mute", this);
|
||||
m_media_context_menu_mute_unmute_action->setIcon(*m_media_context_menu_mute_icon);
|
||||
QObject::connect(m_media_context_menu_mute_unmute_action, &QAction::triggered, this, [this]() {
|
||||
view().toggle_media_mute_state();
|
||||
});
|
||||
|
||||
m_media_context_menu_controls_action = make<QAction>("Show &Controls", this);
|
||||
m_media_context_menu_controls_action->setCheckable(true);
|
||||
QObject::connect(m_media_context_menu_controls_action, &QAction::triggered, this, [this]() {
|
||||
view().toggle_media_controls_state();
|
||||
});
|
||||
|
||||
m_media_context_menu_loop_action = make<QAction>("&Loop", this);
|
||||
m_media_context_menu_loop_action->setCheckable(true);
|
||||
QObject::connect(m_media_context_menu_loop_action, &QAction::triggered, this, [this]() {
|
||||
view().toggle_media_loop_state();
|
||||
});
|
||||
|
||||
auto* open_audio_action = new QAction("&Open Audio", this);
|
||||
open_audio_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-sound.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_audio_action, &QAction::triggered, this, [this]() {
|
||||
open_link(m_media_context_menu_url);
|
||||
});
|
||||
|
||||
auto* open_audio_in_new_tab_action = new QAction("Open Audio in New &Tab", this);
|
||||
open_audio_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_audio_in_new_tab_action, &QAction::triggered, this, [this]() {
|
||||
open_link_in_new_tab(m_media_context_menu_url);
|
||||
});
|
||||
|
||||
auto* copy_audio_url_action = new QAction("Copy Audio &URL", this);
|
||||
copy_audio_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(copy_audio_url_action, &QAction::triggered, this, [this]() {
|
||||
copy_link_url(m_media_context_menu_url);
|
||||
});
|
||||
|
||||
m_audio_context_menu = make<QMenu>("Audio context menu", this);
|
||||
m_audio_context_menu->addAction(m_media_context_menu_play_pause_action);
|
||||
m_audio_context_menu->addAction(m_media_context_menu_mute_unmute_action);
|
||||
m_audio_context_menu->addAction(m_media_context_menu_controls_action);
|
||||
m_audio_context_menu->addAction(m_media_context_menu_loop_action);
|
||||
m_audio_context_menu->addSeparator();
|
||||
m_audio_context_menu->addAction(open_audio_action);
|
||||
m_audio_context_menu->addAction(open_audio_in_new_tab_action);
|
||||
m_audio_context_menu->addSeparator();
|
||||
m_audio_context_menu->addAction(copy_audio_url_action);
|
||||
m_audio_context_menu->addSeparator();
|
||||
m_audio_context_menu->addAction(&m_window->inspect_dom_node_action());
|
||||
|
||||
auto* open_video_action = new QAction("&Open Video", this);
|
||||
open_video_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-video.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_video_action, &QAction::triggered, this, [this]() {
|
||||
open_link(m_media_context_menu_url);
|
||||
});
|
||||
|
||||
auto* open_video_in_new_tab_action = new QAction("Open Video in New &Tab", this);
|
||||
open_video_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(open_video_in_new_tab_action, &QAction::triggered, this, [this]() {
|
||||
open_link_in_new_tab(m_media_context_menu_url);
|
||||
});
|
||||
|
||||
auto* copy_video_url_action = new QAction("Copy Video &URL", this);
|
||||
copy_video_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters())));
|
||||
QObject::connect(copy_video_url_action, &QAction::triggered, this, [this]() {
|
||||
copy_link_url(m_media_context_menu_url);
|
||||
});
|
||||
|
||||
m_video_context_menu = make<QMenu>("Video context menu", this);
|
||||
m_video_context_menu->addAction(m_media_context_menu_play_pause_action);
|
||||
m_video_context_menu->addAction(m_media_context_menu_mute_unmute_action);
|
||||
m_video_context_menu->addAction(m_media_context_menu_controls_action);
|
||||
m_video_context_menu->addAction(m_media_context_menu_loop_action);
|
||||
m_video_context_menu->addSeparator();
|
||||
m_video_context_menu->addAction(open_video_action);
|
||||
m_video_context_menu->addAction(open_video_in_new_tab_action);
|
||||
m_video_context_menu->addSeparator();
|
||||
m_video_context_menu->addAction(copy_video_url_action);
|
||||
m_video_context_menu->addSeparator();
|
||||
m_video_context_menu->addAction(&m_window->inspect_dom_node_action());
|
||||
|
||||
view().on_media_context_menu_request = [this](Gfx::IntPoint, Web::Page::MediaContextMenu const& menu) {
|
||||
m_media_context_menu_url = menu.media_url;
|
||||
|
||||
if (menu.is_playing) {
|
||||
m_media_context_menu_play_pause_action->setIcon(*m_media_context_menu_pause_icon);
|
||||
m_media_context_menu_play_pause_action->setText("&Pause");
|
||||
} else {
|
||||
m_media_context_menu_play_pause_action->setIcon(*m_media_context_menu_play_icon);
|
||||
m_media_context_menu_play_pause_action->setText("&Play");
|
||||
}
|
||||
|
||||
if (menu.is_muted) {
|
||||
m_media_context_menu_mute_unmute_action->setIcon(*m_media_context_menu_unmute_icon);
|
||||
m_media_context_menu_mute_unmute_action->setText("Un&mute");
|
||||
} else {
|
||||
m_media_context_menu_mute_unmute_action->setIcon(*m_media_context_menu_mute_icon);
|
||||
m_media_context_menu_mute_unmute_action->setText("&Mute");
|
||||
}
|
||||
|
||||
m_media_context_menu_controls_action->setChecked(menu.has_user_agent_controls);
|
||||
m_media_context_menu_loop_action->setChecked(menu.is_looping);
|
||||
|
||||
auto screen_position = QCursor::pos();
|
||||
|
||||
if (menu.is_video)
|
||||
m_video_context_menu->exec(screen_position);
|
||||
else
|
||||
m_audio_context_menu->exec(screen_position);
|
||||
};
|
||||
}
|
||||
|
||||
Tab::~Tab()
|
||||
{
|
||||
close_sub_widgets();
|
||||
}
|
||||
|
||||
void Tab::update_reset_zoom_button()
|
||||
{
|
||||
auto zoom_level = view().zoom_level();
|
||||
if (zoom_level != 1.0f) {
|
||||
auto zoom_level_text = MUST(String::formatted("{}%", round_to<int>(zoom_level * 100)));
|
||||
m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text));
|
||||
m_reset_zoom_button_action->setVisible(true);
|
||||
} else {
|
||||
m_reset_zoom_button_action->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Tab::focus_location_editor()
|
||||
{
|
||||
m_location_edit->setFocus();
|
||||
m_location_edit->selectAll();
|
||||
}
|
||||
|
||||
void Tab::navigate(QString url_qstring, LoadType load_type)
|
||||
{
|
||||
auto url_string = ak_deprecated_string_from_qstring(url_qstring);
|
||||
if (url_string.starts_with('/'))
|
||||
url_string = DeprecatedString::formatted("file://{}", url_string);
|
||||
else if (URL url = url_string; !url.is_valid())
|
||||
url_string = DeprecatedString::formatted("https://{}", url_string);
|
||||
m_is_history_navigation = (load_type == LoadType::HistoryNavigation);
|
||||
view().load(url_string);
|
||||
}
|
||||
|
||||
void Tab::back()
|
||||
{
|
||||
if (!m_history.can_go_back())
|
||||
return;
|
||||
|
||||
m_is_history_navigation = true;
|
||||
m_history.go_back();
|
||||
view().load(m_history.current().url.to_deprecated_string());
|
||||
}
|
||||
|
||||
void Tab::forward()
|
||||
{
|
||||
if (!m_history.can_go_forward())
|
||||
return;
|
||||
|
||||
m_is_history_navigation = true;
|
||||
m_history.go_forward();
|
||||
view().load(m_history.current().url.to_deprecated_string());
|
||||
}
|
||||
|
||||
void Tab::reload()
|
||||
{
|
||||
m_is_history_navigation = true;
|
||||
view().load(m_history.current().url.to_deprecated_string());
|
||||
}
|
||||
|
||||
void Tab::open_link(URL const& url)
|
||||
{
|
||||
view().on_link_click(url, "", 0);
|
||||
}
|
||||
|
||||
void Tab::open_link_in_new_tab(URL const& url)
|
||||
{
|
||||
view().on_link_click(url, "_blank", 0);
|
||||
}
|
||||
|
||||
void Tab::copy_link_url(URL const& url)
|
||||
{
|
||||
auto* clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(qstring_from_ak_deprecated_string(url.to_deprecated_string()));
|
||||
}
|
||||
|
||||
void Tab::location_edit_return_pressed()
|
||||
{
|
||||
navigate(m_location_edit->text());
|
||||
}
|
||||
|
||||
void Tab::open_file()
|
||||
{
|
||||
auto filename = QFileDialog::getOpenFileName(this, "Open file", QDir::homePath(), "All Files (*.*)");
|
||||
if (!filename.isNull())
|
||||
navigate("file://" + filename);
|
||||
}
|
||||
|
||||
int Tab::tab_index()
|
||||
{
|
||||
return m_window->tab_index(this);
|
||||
}
|
||||
|
||||
void Tab::debug_request(DeprecatedString const& request, DeprecatedString const& argument)
|
||||
{
|
||||
if (request == "dump-history")
|
||||
m_history.dump();
|
||||
else
|
||||
m_view->debug_request(request, argument);
|
||||
}
|
||||
|
||||
void Tab::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
if (m_hover_label->isVisible())
|
||||
update_hover_label();
|
||||
}
|
||||
|
||||
void Tab::update_hover_label()
|
||||
{
|
||||
m_hover_label->resize(QFontMetrics(m_hover_label->font()).boundingRect(m_hover_label->text()).adjusted(-4, -2, 4, 2).size());
|
||||
m_hover_label->move(6, height() - m_hover_label->height() - 8);
|
||||
m_hover_label->raise();
|
||||
}
|
||||
|
||||
bool Tab::event(QEvent* event)
|
||||
{
|
||||
if (event->type() == QEvent::PaletteChange) {
|
||||
recreate_toolbar_icons();
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void Tab::recreate_toolbar_icons()
|
||||
{
|
||||
m_window->go_back_action().setIcon(create_tvg_icon_with_theme_colors("back", palette()));
|
||||
m_window->go_forward_action().setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
|
||||
m_window->reload_action().setIcon(create_tvg_icon_with_theme_colors("reload", palette()));
|
||||
}
|
||||
|
||||
void Tab::show_inspector_window(InspectorTarget inspector_target)
|
||||
{
|
||||
bool inspector_previously_loaded = m_inspector_widget != nullptr;
|
||||
|
||||
if (!m_inspector_widget) {
|
||||
m_inspector_widget = new Ladybird::InspectorWidget;
|
||||
m_inspector_widget->setWindowTitle("Inspector");
|
||||
m_inspector_widget->resize(640, 480);
|
||||
m_inspector_widget->on_close = [this] {
|
||||
view().clear_inspected_dom_node();
|
||||
};
|
||||
|
||||
m_inspector_widget->on_dom_node_inspected = [&](auto id, auto pseudo_element) {
|
||||
return view().inspect_dom_node(id, pseudo_element);
|
||||
};
|
||||
}
|
||||
|
||||
if (!inspector_previously_loaded || !m_inspector_widget->dom_loaded()) {
|
||||
view().inspect_dom_tree();
|
||||
view().inspect_accessibility_tree();
|
||||
}
|
||||
|
||||
m_inspector_widget->show();
|
||||
|
||||
if (inspector_target == InspectorTarget::HoveredElement) {
|
||||
auto hovered_node = view().get_hovered_node_id();
|
||||
m_inspector_widget->set_selection({ hovered_node });
|
||||
} else {
|
||||
m_inspector_widget->select_default_node();
|
||||
}
|
||||
}
|
||||
|
||||
void Tab::show_console_window()
|
||||
{
|
||||
if (!m_console_widget) {
|
||||
m_console_widget = new Ladybird::ConsoleWidget;
|
||||
m_console_widget->setWindowTitle("JS Console");
|
||||
m_console_widget->resize(640, 480);
|
||||
|
||||
// Make the copy action available in the window via the bound copy key shortcut.
|
||||
// Simply adding it to the context menu is not enough.
|
||||
m_console_widget->addAction(&m_window->copy_selection_action());
|
||||
|
||||
m_console_context_menu = make<QMenu>("Context menu", m_console_widget);
|
||||
m_console_context_menu->addAction(&m_window->copy_selection_action());
|
||||
m_console_widget->view().on_context_menu_request = [this](Gfx::IntPoint) {
|
||||
auto screen_position = QCursor::pos();
|
||||
m_console_context_menu->exec(screen_position);
|
||||
};
|
||||
m_console_widget->on_js_input = [this](auto js_source) {
|
||||
view().js_console_input(js_source);
|
||||
};
|
||||
m_console_widget->on_request_messages = [this](i32 start_index) {
|
||||
view().js_console_request_messages(start_index);
|
||||
};
|
||||
}
|
||||
|
||||
m_console_widget->show();
|
||||
}
|
||||
|
||||
void Tab::close_sub_widgets()
|
||||
{
|
||||
auto close_widget_window = [](auto* widget) {
|
||||
if (widget)
|
||||
widget->close();
|
||||
};
|
||||
|
||||
close_widget_window(m_console_widget);
|
||||
close_widget_window(m_inspector_widget);
|
||||
}
|
||||
|
||||
}
|
120
Ladybird/Qt/Tab.h
Normal file
120
Ladybird/Qt/Tab.h
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "LocationEdit.h"
|
||||
#include "WebContentView.h"
|
||||
#include <Browser/History.h>
|
||||
#include <QBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class BrowserWindow;
|
||||
class ConsoleWidget;
|
||||
class InspectorWidget;
|
||||
|
||||
class Tab final : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode, UseLagomNetworking);
|
||||
virtual ~Tab() override;
|
||||
|
||||
WebContentView& view() { return *m_view; }
|
||||
|
||||
enum class LoadType {
|
||||
Normal,
|
||||
HistoryNavigation,
|
||||
};
|
||||
void navigate(QString, LoadType = LoadType::Normal);
|
||||
void back();
|
||||
void forward();
|
||||
void reload();
|
||||
|
||||
void debug_request(DeprecatedString const& request, DeprecatedString const& argument);
|
||||
|
||||
void open_file();
|
||||
void update_reset_zoom_button();
|
||||
|
||||
enum class InspectorTarget {
|
||||
Document,
|
||||
HoveredElement
|
||||
};
|
||||
void show_inspector_window(InspectorTarget = InspectorTarget::Document);
|
||||
void show_console_window();
|
||||
|
||||
Ladybird::ConsoleWidget* console() { return m_console_widget; }
|
||||
|
||||
public slots:
|
||||
void focus_location_editor();
|
||||
void location_edit_return_pressed();
|
||||
|
||||
signals:
|
||||
void title_changed(int id, QString);
|
||||
void favicon_changed(int id, QIcon);
|
||||
|
||||
private:
|
||||
virtual void resizeEvent(QResizeEvent*) override;
|
||||
virtual bool event(QEvent*) override;
|
||||
|
||||
void recreate_toolbar_icons();
|
||||
void update_hover_label();
|
||||
|
||||
void open_link(URL const&);
|
||||
void open_link_in_new_tab(URL const&);
|
||||
void copy_link_url(URL const&);
|
||||
|
||||
void close_sub_widgets();
|
||||
|
||||
QBoxLayout* m_layout;
|
||||
QToolBar* m_toolbar { nullptr };
|
||||
QToolButton* m_reset_zoom_button { nullptr };
|
||||
QAction* m_reset_zoom_button_action { nullptr };
|
||||
LocationEdit* m_location_edit { nullptr };
|
||||
WebContentView* m_view { nullptr };
|
||||
BrowserWindow* m_window { nullptr };
|
||||
Browser::History m_history;
|
||||
QString m_title;
|
||||
QLabel* m_hover_label { nullptr };
|
||||
|
||||
OwnPtr<QMenu> m_page_context_menu;
|
||||
|
||||
OwnPtr<QMenu> m_link_context_menu;
|
||||
URL m_link_context_menu_url;
|
||||
|
||||
OwnPtr<QMenu> m_image_context_menu;
|
||||
Gfx::ShareableBitmap m_image_context_menu_bitmap;
|
||||
URL m_image_context_menu_url;
|
||||
|
||||
OwnPtr<QMenu> m_audio_context_menu;
|
||||
OwnPtr<QMenu> m_video_context_menu;
|
||||
OwnPtr<QIcon> m_media_context_menu_play_icon;
|
||||
OwnPtr<QIcon> m_media_context_menu_pause_icon;
|
||||
OwnPtr<QIcon> m_media_context_menu_mute_icon;
|
||||
OwnPtr<QIcon> m_media_context_menu_unmute_icon;
|
||||
OwnPtr<QAction> m_media_context_menu_play_pause_action;
|
||||
OwnPtr<QAction> m_media_context_menu_mute_unmute_action;
|
||||
OwnPtr<QAction> m_media_context_menu_controls_action;
|
||||
OwnPtr<QAction> m_media_context_menu_loop_action;
|
||||
URL m_media_context_menu_url;
|
||||
|
||||
int tab_index();
|
||||
|
||||
bool m_is_history_navigation { false };
|
||||
|
||||
Ladybird::ConsoleWidget* m_console_widget { nullptr };
|
||||
OwnPtr<QMenu> m_console_context_menu;
|
||||
Ladybird::InspectorWidget* m_inspector_widget { nullptr };
|
||||
};
|
||||
|
||||
}
|
837
Ladybird/Qt/WebContentView.cpp
Normal file
837
Ladybird/Qt/WebContentView.cpp
Normal file
|
@ -0,0 +1,837 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebContentView.h"
|
||||
#include "StringUtils.h"
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/Format.h>
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/Types.h>
|
||||
#include <Kernel/API/KeyCode.h>
|
||||
#include <Ladybird/HelperProcess.h>
|
||||
#include <Ladybird/Utilities.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/Font/FontDatabase.h>
|
||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
||||
#include <LibGfx/Painter.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/SystemTheme.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWeb/Loader/ContentFilter.h>
|
||||
#include <LibWebView/WebContentClient.h>
|
||||
#include <QApplication>
|
||||
#include <QCursor>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QMimeData>
|
||||
#include <QMouseEvent>
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QScrollBar>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
#include <QToolTip>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
bool is_using_dark_system_theme(QWidget&);
|
||||
|
||||
WebContentView::WebContentView(StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling enable_callgrind_profiling, WebView::UseJavaScriptBytecode use_javascript_bytecode, UseLagomNetworking use_lagom_networking)
|
||||
: WebView::ViewImplementation(use_javascript_bytecode)
|
||||
, m_use_lagom_networking(use_lagom_networking)
|
||||
, m_webdriver_content_ipc_path(webdriver_content_ipc_path)
|
||||
{
|
||||
setMouseTracking(true);
|
||||
setAcceptDrops(true);
|
||||
|
||||
setFocusPolicy(Qt::FocusPolicy::StrongFocus);
|
||||
|
||||
m_device_pixel_ratio = devicePixelRatio();
|
||||
m_inverse_pixel_scaling_ratio = 1.0 / m_device_pixel_ratio;
|
||||
|
||||
verticalScrollBar()->setSingleStep(24);
|
||||
horizontalScrollBar()->setSingleStep(24);
|
||||
|
||||
QObject::connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
|
||||
update_viewport_rect();
|
||||
});
|
||||
QObject::connect(horizontalScrollBar(), &QScrollBar::valueChanged, [this](int) {
|
||||
update_viewport_rect();
|
||||
});
|
||||
|
||||
create_client(enable_callgrind_profiling);
|
||||
}
|
||||
|
||||
WebContentView::~WebContentView() = default;
|
||||
|
||||
unsigned get_button_from_qt_event(QSinglePointEvent const& event)
|
||||
{
|
||||
if (event.button() == Qt::MouseButton::LeftButton)
|
||||
return 1;
|
||||
if (event.button() == Qt::MouseButton::RightButton)
|
||||
return 2;
|
||||
if (event.button() == Qt::MouseButton::MiddleButton)
|
||||
return 4;
|
||||
if (event.button() == Qt::MouseButton::BackButton)
|
||||
return 8;
|
||||
if (event.buttons() == Qt::MouseButton::ForwardButton)
|
||||
return 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsigned get_buttons_from_qt_event(QSinglePointEvent const& event)
|
||||
{
|
||||
unsigned buttons = 0;
|
||||
if (event.buttons() & Qt::MouseButton::LeftButton)
|
||||
buttons |= 1;
|
||||
if (event.buttons() & Qt::MouseButton::RightButton)
|
||||
buttons |= 2;
|
||||
if (event.buttons() & Qt::MouseButton::MiddleButton)
|
||||
buttons |= 4;
|
||||
if (event.buttons() & Qt::MouseButton::BackButton)
|
||||
buttons |= 8;
|
||||
if (event.buttons() & Qt::MouseButton::ForwardButton)
|
||||
buttons |= 16;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
unsigned get_modifiers_from_qt_mouse_event(QSinglePointEvent const& event)
|
||||
{
|
||||
unsigned modifiers = 0;
|
||||
if (event.modifiers() & Qt::Modifier::ALT)
|
||||
modifiers |= 1;
|
||||
if (event.modifiers() & Qt::Modifier::CTRL)
|
||||
modifiers |= 2;
|
||||
if (event.modifiers() & Qt::Modifier::SHIFT)
|
||||
modifiers |= 4;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
unsigned get_modifiers_from_qt_keyboard_event(QKeyEvent const& event)
|
||||
{
|
||||
auto modifiers = 0;
|
||||
if (event.modifiers().testFlag(Qt::AltModifier))
|
||||
modifiers |= KeyModifier::Mod_Alt;
|
||||
if (event.modifiers().testFlag(Qt::ControlModifier))
|
||||
modifiers |= KeyModifier::Mod_Ctrl;
|
||||
if (event.modifiers().testFlag(Qt::MetaModifier))
|
||||
modifiers |= KeyModifier::Mod_Super;
|
||||
if (event.modifiers().testFlag(Qt::ShiftModifier))
|
||||
modifiers |= KeyModifier::Mod_Shift;
|
||||
if (event.modifiers().testFlag(Qt::AltModifier))
|
||||
modifiers |= KeyModifier::Mod_AltGr;
|
||||
if (event.modifiers().testFlag(Qt::KeypadModifier))
|
||||
modifiers |= KeyModifier::Mod_Keypad;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
KeyCode get_keycode_from_qt_keyboard_event(QKeyEvent const& event)
|
||||
{
|
||||
struct Mapping {
|
||||
constexpr Mapping(Qt::Key q, KeyCode s)
|
||||
: qt_key(q)
|
||||
, serenity_key(s)
|
||||
{
|
||||
}
|
||||
|
||||
Qt::Key qt_key;
|
||||
KeyCode serenity_key;
|
||||
};
|
||||
|
||||
// https://doc.qt.io/qt-6/qt.html#Key-enum
|
||||
constexpr Mapping mappings[] = {
|
||||
{ Qt::Key_0, Key_0 },
|
||||
{ Qt::Key_1, Key_1 },
|
||||
{ Qt::Key_2, Key_2 },
|
||||
{ Qt::Key_3, Key_3 },
|
||||
{ Qt::Key_4, Key_4 },
|
||||
{ Qt::Key_5, Key_5 },
|
||||
{ Qt::Key_6, Key_6 },
|
||||
{ Qt::Key_7, Key_7 },
|
||||
{ Qt::Key_8, Key_8 },
|
||||
{ Qt::Key_9, Key_9 },
|
||||
{ Qt::Key_A, Key_A },
|
||||
{ Qt::Key_Alt, Key_Alt },
|
||||
{ Qt::Key_Ampersand, Key_Ampersand },
|
||||
{ Qt::Key_Apostrophe, Key_Apostrophe },
|
||||
{ Qt::Key_AsciiCircum, Key_Circumflex },
|
||||
{ Qt::Key_AsciiTilde, Key_Tilde },
|
||||
{ Qt::Key_Asterisk, Key_Asterisk },
|
||||
{ Qt::Key_At, Key_AtSign },
|
||||
{ Qt::Key_B, Key_B },
|
||||
{ Qt::Key_Backslash, Key_Backslash },
|
||||
{ Qt::Key_Backspace, Key_Backspace },
|
||||
{ Qt::Key_Bar, Key_Pipe },
|
||||
{ Qt::Key_BraceLeft, Key_LeftBrace },
|
||||
{ Qt::Key_BraceRight, Key_RightBrace },
|
||||
{ Qt::Key_BracketLeft, Key_LeftBracket },
|
||||
{ Qt::Key_BracketRight, Key_RightBracket },
|
||||
{ Qt::Key_C, Key_C },
|
||||
{ Qt::Key_CapsLock, Key_CapsLock },
|
||||
{ Qt::Key_Colon, Key_Colon },
|
||||
{ Qt::Key_Comma, Key_Comma },
|
||||
{ Qt::Key_Control, Key_Control },
|
||||
{ Qt::Key_D, Key_D },
|
||||
{ Qt::Key_Delete, Key_Delete },
|
||||
{ Qt::Key_Dollar, Key_Dollar },
|
||||
{ Qt::Key_Down, Key_Down },
|
||||
{ Qt::Key_E, Key_E },
|
||||
{ Qt::Key_End, Key_End },
|
||||
{ Qt::Key_Equal, Key_Equal },
|
||||
{ Qt::Key_Enter, Key_Return },
|
||||
{ Qt::Key_Escape, Key_Escape },
|
||||
{ Qt::Key_Exclam, Key_ExclamationPoint },
|
||||
{ Qt::Key_exclamdown, Key_ExclamationPoint },
|
||||
{ Qt::Key_F, Key_F },
|
||||
{ Qt::Key_F1, Key_F1 },
|
||||
{ Qt::Key_F10, Key_F10 },
|
||||
{ Qt::Key_F11, Key_F11 },
|
||||
{ Qt::Key_F12, Key_F12 },
|
||||
{ Qt::Key_F2, Key_F2 },
|
||||
{ Qt::Key_F3, Key_F3 },
|
||||
{ Qt::Key_F4, Key_F4 },
|
||||
{ Qt::Key_F5, Key_F5 },
|
||||
{ Qt::Key_F6, Key_F6 },
|
||||
{ Qt::Key_F7, Key_F7 },
|
||||
{ Qt::Key_F8, Key_F8 },
|
||||
{ Qt::Key_F9, Key_F9 },
|
||||
{ Qt::Key_G, Key_G },
|
||||
{ Qt::Key_Greater, Key_GreaterThan },
|
||||
{ Qt::Key_H, Key_H },
|
||||
{ Qt::Key_Home, Key_Home },
|
||||
{ Qt::Key_I, Key_I },
|
||||
{ Qt::Key_Insert, Key_Insert },
|
||||
{ Qt::Key_J, Key_J },
|
||||
{ Qt::Key_K, Key_K },
|
||||
{ Qt::Key_L, Key_L },
|
||||
{ Qt::Key_Left, Key_Left },
|
||||
{ Qt::Key_Less, Key_LessThan },
|
||||
{ Qt::Key_M, Key_M },
|
||||
{ Qt::Key_Menu, Key_Menu },
|
||||
{ Qt::Key_Meta, Key_Super },
|
||||
{ Qt::Key_Minus, Key_Minus },
|
||||
{ Qt::Key_N, Key_N },
|
||||
{ Qt::Key_NumberSign, Key_Hashtag },
|
||||
{ Qt::Key_NumLock, Key_NumLock },
|
||||
{ Qt::Key_O, Key_O },
|
||||
{ Qt::Key_P, Key_P },
|
||||
{ Qt::Key_PageDown, Key_PageDown },
|
||||
{ Qt::Key_PageUp, Key_PageUp },
|
||||
{ Qt::Key_ParenLeft, Key_LeftParen },
|
||||
{ Qt::Key_ParenRight, Key_RightParen },
|
||||
{ Qt::Key_Percent, Key_Percent },
|
||||
{ Qt::Key_Period, Key_Period },
|
||||
{ Qt::Key_Plus, Key_Plus },
|
||||
{ Qt::Key_Print, Key_PrintScreen },
|
||||
{ Qt::Key_Q, Key_Q },
|
||||
{ Qt::Key_Question, Key_QuestionMark },
|
||||
{ Qt::Key_QuoteDbl, Key_DoubleQuote },
|
||||
{ Qt::Key_QuoteLeft, Key_Backtick },
|
||||
{ Qt::Key_R, Key_R },
|
||||
{ Qt::Key_Return, Key_Return },
|
||||
{ Qt::Key_Right, Key_Right },
|
||||
{ Qt::Key_S, Key_S },
|
||||
{ Qt::Key_ScrollLock, Key_ScrollLock },
|
||||
{ Qt::Key_Semicolon, Key_Semicolon },
|
||||
{ Qt::Key_Shift, Key_LeftShift },
|
||||
{ Qt::Key_Slash, Key_Slash },
|
||||
{ Qt::Key_Space, Key_Space },
|
||||
{ Qt::Key_Super_L, Key_Super },
|
||||
{ Qt::Key_Super_R, Key_Super },
|
||||
{ Qt::Key_SysReq, Key_SysRq },
|
||||
{ Qt::Key_T, Key_T },
|
||||
{ Qt::Key_Tab, Key_Tab },
|
||||
{ Qt::Key_U, Key_U },
|
||||
{ Qt::Key_Underscore, Key_Underscore },
|
||||
{ Qt::Key_Up, Key_Up },
|
||||
{ Qt::Key_V, Key_V },
|
||||
{ Qt::Key_W, Key_W },
|
||||
{ Qt::Key_X, Key_X },
|
||||
{ Qt::Key_Y, Key_Y },
|
||||
{ Qt::Key_Z, Key_Z },
|
||||
};
|
||||
|
||||
for (auto const& mapping : mappings) {
|
||||
if (event.key() == mapping.qt_key)
|
||||
return mapping.serenity_key;
|
||||
}
|
||||
return Key_Invalid;
|
||||
}
|
||||
|
||||
void WebContentView::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (!event->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
|
||||
auto button = get_button_from_qt_event(*event);
|
||||
auto buttons = get_buttons_from_qt_event(*event);
|
||||
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
|
||||
auto num_degrees = -event->angleDelta();
|
||||
float delta_x = -num_degrees.x() / 120;
|
||||
float delta_y = num_degrees.y() / 120;
|
||||
// Note: This does not use the QScrollBar's step size as LibWeb multiples this by a step size internally.
|
||||
auto step_x = delta_x * QApplication::wheelScrollLines() * devicePixelRatio();
|
||||
auto step_y = delta_y * QApplication::wheelScrollLines() * devicePixelRatio();
|
||||
client().async_mouse_wheel(to_content_position(position), button, buttons, modifiers, step_x, step_y);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void WebContentView::mouseMoveEvent(QMouseEvent* event)
|
||||
{
|
||||
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
|
||||
auto buttons = get_buttons_from_qt_event(*event);
|
||||
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
|
||||
client().async_mouse_move(to_content_position(position), 0, buttons, modifiers);
|
||||
}
|
||||
|
||||
void WebContentView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
|
||||
auto button = get_button_from_qt_event(*event);
|
||||
if (button == 0) {
|
||||
// We could not convert Qt buttons to something that Lagom can
|
||||
// recognize - don't even bother propagating this to the web engine
|
||||
// as it will not handle it anyway, and it will (currently) assert
|
||||
return;
|
||||
}
|
||||
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
|
||||
auto buttons = get_buttons_from_qt_event(*event);
|
||||
client().async_mouse_down(to_content_position(position), button, buttons, modifiers);
|
||||
}
|
||||
|
||||
void WebContentView::mouseReleaseEvent(QMouseEvent* event)
|
||||
{
|
||||
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
|
||||
auto button = get_button_from_qt_event(*event);
|
||||
|
||||
if (event->button() & Qt::MouseButton::BackButton) {
|
||||
if (on_back_button)
|
||||
on_back_button();
|
||||
} else if (event->button() & Qt::MouseButton::ForwardButton) {
|
||||
if (on_forward_button)
|
||||
on_forward_button();
|
||||
}
|
||||
|
||||
if (button == 0) {
|
||||
// We could not convert Qt buttons to something that Lagom can
|
||||
// recognize - don't even bother propagating this to the web engine
|
||||
// as it will not handle it anyway, and it will (currently) assert
|
||||
return;
|
||||
}
|
||||
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
|
||||
auto buttons = get_buttons_from_qt_event(*event);
|
||||
client().async_mouse_up(to_content_position(position), button, buttons, modifiers);
|
||||
}
|
||||
|
||||
void WebContentView::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
Gfx::IntPoint position(event->position().x() / m_inverse_pixel_scaling_ratio, event->position().y() / m_inverse_pixel_scaling_ratio);
|
||||
auto button = get_button_from_qt_event(*event);
|
||||
if (button == 0) {
|
||||
// We could not convert Qt buttons to something that Lagom can
|
||||
// recognize - don't even bother propagating this to the web engine
|
||||
// as it will not handle it anyway, and it will (currently) assert
|
||||
return;
|
||||
}
|
||||
auto modifiers = get_modifiers_from_qt_mouse_event(*event);
|
||||
auto buttons = get_buttons_from_qt_event(*event);
|
||||
client().async_doubleclick(to_content_position(position), button, buttons, modifiers);
|
||||
}
|
||||
|
||||
void WebContentView::dragEnterEvent(QDragEnterEvent* event)
|
||||
{
|
||||
if (event->mimeData()->hasUrls())
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void WebContentView::dropEvent(QDropEvent* event)
|
||||
{
|
||||
VERIFY(event->mimeData()->hasUrls());
|
||||
emit urls_dropped(event->mimeData()->urls());
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void WebContentView::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Left:
|
||||
case Qt::Key_Right:
|
||||
case Qt::Key_Up:
|
||||
case Qt::Key_Down:
|
||||
case Qt::Key_PageUp:
|
||||
case Qt::Key_PageDown:
|
||||
QAbstractScrollArea::keyPressEvent(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (event->key() == Qt::Key_Backtab) {
|
||||
// NOTE: Qt transforms Shift+Tab into a "Backtab", so we undo that transformation here.
|
||||
client().async_key_down(KeyCode::Key_Tab, Mod_Shift, '\t');
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = event->text();
|
||||
auto point = text.isEmpty() ? 0u : event->text()[0].unicode();
|
||||
auto keycode = get_keycode_from_qt_keyboard_event(*event);
|
||||
auto modifiers = get_modifiers_from_qt_keyboard_event(*event);
|
||||
client().async_key_down(keycode, modifiers, point);
|
||||
}
|
||||
|
||||
void WebContentView::keyReleaseEvent(QKeyEvent* event)
|
||||
{
|
||||
auto text = event->text();
|
||||
auto point = text.isEmpty() ? 0u : event->text()[0].unicode();
|
||||
auto keycode = get_keycode_from_qt_keyboard_event(*event);
|
||||
auto modifiers = get_modifiers_from_qt_keyboard_event(*event);
|
||||
client().async_key_up(keycode, modifiers, point);
|
||||
}
|
||||
|
||||
void WebContentView::focusInEvent(QFocusEvent*)
|
||||
{
|
||||
client().async_set_has_focus(true);
|
||||
}
|
||||
|
||||
void WebContentView::focusOutEvent(QFocusEvent*)
|
||||
{
|
||||
client().async_set_has_focus(false);
|
||||
}
|
||||
|
||||
void WebContentView::paintEvent(QPaintEvent*)
|
||||
{
|
||||
QPainter painter(viewport());
|
||||
painter.scale(m_inverse_pixel_scaling_ratio, m_inverse_pixel_scaling_ratio);
|
||||
|
||||
Gfx::Bitmap const* bitmap = nullptr;
|
||||
Gfx::IntSize bitmap_size;
|
||||
|
||||
if (m_client_state.has_usable_bitmap) {
|
||||
bitmap = m_client_state.front_bitmap.bitmap.ptr();
|
||||
bitmap_size = m_client_state.front_bitmap.last_painted_size;
|
||||
|
||||
} else {
|
||||
bitmap = m_backup_bitmap.ptr();
|
||||
bitmap_size = m_backup_bitmap_size;
|
||||
}
|
||||
|
||||
if (bitmap) {
|
||||
QImage q_image(bitmap->scanline_u8(0), bitmap->width(), bitmap->height(), QImage::Format_RGB32);
|
||||
painter.drawImage(QPoint(0, 0), q_image, QRect(0, 0, bitmap_size.width(), bitmap_size.height()));
|
||||
|
||||
if (bitmap_size.width() < width()) {
|
||||
painter.fillRect(bitmap_size.width(), 0, width() - bitmap_size.width(), bitmap->height(), palette().base());
|
||||
}
|
||||
if (bitmap_size.height() < height()) {
|
||||
painter.fillRect(0, bitmap_size.height(), width(), height() - bitmap_size.height(), palette().base());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
painter.fillRect(rect(), palette().base());
|
||||
}
|
||||
|
||||
void WebContentView::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QAbstractScrollArea::resizeEvent(event);
|
||||
update_viewport_rect();
|
||||
handle_resize();
|
||||
}
|
||||
|
||||
void WebContentView::set_viewport_rect(Gfx::IntRect rect)
|
||||
{
|
||||
m_viewport_rect = rect;
|
||||
client().async_set_viewport_rect(rect);
|
||||
}
|
||||
|
||||
void WebContentView::set_window_size(Gfx::IntSize size)
|
||||
{
|
||||
client().async_set_window_size(size);
|
||||
}
|
||||
|
||||
void WebContentView::set_window_position(Gfx::IntPoint position)
|
||||
{
|
||||
client().async_set_window_position(position);
|
||||
}
|
||||
|
||||
void WebContentView::update_viewport_rect()
|
||||
{
|
||||
auto scaled_width = int(viewport()->width() / m_inverse_pixel_scaling_ratio);
|
||||
auto scaled_height = int(viewport()->height() / m_inverse_pixel_scaling_ratio);
|
||||
Gfx::IntRect rect(max(0, horizontalScrollBar()->value()), max(0, verticalScrollBar()->value()), scaled_width, scaled_height);
|
||||
|
||||
set_viewport_rect(rect);
|
||||
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void WebContentView::update_zoom()
|
||||
{
|
||||
client().async_set_device_pixels_per_css_pixel(m_device_pixel_ratio * m_zoom_level);
|
||||
update_viewport_rect();
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void WebContentView::showEvent(QShowEvent* event)
|
||||
{
|
||||
QAbstractScrollArea::showEvent(event);
|
||||
client().async_set_system_visibility_state(true);
|
||||
}
|
||||
|
||||
void WebContentView::hideEvent(QHideEvent* event)
|
||||
{
|
||||
QAbstractScrollArea::hideEvent(event);
|
||||
client().async_set_system_visibility_state(false);
|
||||
}
|
||||
|
||||
static Core::AnonymousBuffer make_system_theme_from_qt_palette(QWidget& widget, WebContentView::PaletteMode mode)
|
||||
{
|
||||
auto qt_palette = widget.palette();
|
||||
|
||||
auto theme_file = mode == WebContentView::PaletteMode::Default ? "Default"sv : "Dark"sv;
|
||||
auto theme = Gfx::load_system_theme(DeprecatedString::formatted("{}/res/themes/{}.ini", s_serenity_resource_root, theme_file)).release_value_but_fixme_should_propagate_errors();
|
||||
auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(theme);
|
||||
auto palette = Gfx::Palette(move(palette_impl));
|
||||
|
||||
auto translate = [&](Gfx::ColorRole gfx_color_role, QPalette::ColorRole qt_color_role) {
|
||||
auto new_color = Gfx::Color::from_argb(qt_palette.color(qt_color_role).rgba());
|
||||
palette.set_color(gfx_color_role, new_color);
|
||||
};
|
||||
|
||||
translate(Gfx::ColorRole::ThreedHighlight, QPalette::ColorRole::Light);
|
||||
translate(Gfx::ColorRole::ThreedShadow1, QPalette::ColorRole::Mid);
|
||||
translate(Gfx::ColorRole::ThreedShadow2, QPalette::ColorRole::Dark);
|
||||
translate(Gfx::ColorRole::HoverHighlight, QPalette::ColorRole::Light);
|
||||
translate(Gfx::ColorRole::Link, QPalette::ColorRole::Link);
|
||||
translate(Gfx::ColorRole::VisitedLink, QPalette::ColorRole::LinkVisited);
|
||||
translate(Gfx::ColorRole::Button, QPalette::ColorRole::Button);
|
||||
translate(Gfx::ColorRole::ButtonText, QPalette::ColorRole::ButtonText);
|
||||
translate(Gfx::ColorRole::Selection, QPalette::ColorRole::Highlight);
|
||||
translate(Gfx::ColorRole::SelectionText, QPalette::ColorRole::HighlightedText);
|
||||
|
||||
palette.set_flag(Gfx::FlagRole::IsDark, is_using_dark_system_theme(widget));
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
void WebContentView::update_palette(PaletteMode mode)
|
||||
{
|
||||
client().async_update_system_theme(make_system_theme_from_qt_palette(*this, mode));
|
||||
}
|
||||
|
||||
void WebContentView::create_client(WebView::EnableCallgrindProfiling enable_callgrind_profiling)
|
||||
{
|
||||
m_client_state = {};
|
||||
|
||||
auto candidate_web_content_paths = get_paths_for_helper_process("WebContent"sv).release_value_but_fixme_should_propagate_errors();
|
||||
auto new_client = launch_web_content_process(*this, candidate_web_content_paths, enable_callgrind_profiling, WebView::IsLayoutTestMode::No, use_javascript_bytecode(), m_use_lagom_networking).release_value_but_fixme_should_propagate_errors();
|
||||
|
||||
m_client_state.client = new_client;
|
||||
m_client_state.client->on_web_content_process_crash = [this] {
|
||||
Core::deferred_invoke([this] {
|
||||
handle_web_content_process_crash();
|
||||
});
|
||||
};
|
||||
|
||||
m_client_state.client_handle = Web::Crypto::generate_random_uuid().release_value_but_fixme_should_propagate_errors();
|
||||
client().async_set_window_handle(m_client_state.client_handle);
|
||||
|
||||
client().async_set_device_pixels_per_css_pixel(m_device_pixel_ratio);
|
||||
update_palette();
|
||||
client().async_update_system_fonts(Gfx::FontDatabase::default_font_query(), Gfx::FontDatabase::fixed_width_font_query(), Gfx::FontDatabase::window_title_font_query());
|
||||
|
||||
auto screens = QGuiApplication::screens();
|
||||
|
||||
if (!screens.empty()) {
|
||||
Vector<Gfx::IntRect> screen_rects;
|
||||
|
||||
for (auto const& screen : screens) {
|
||||
auto geometry = screen->geometry();
|
||||
|
||||
screen_rects.append(Gfx::IntRect(geometry.x(), geometry.y(), geometry.width(), geometry.height()));
|
||||
}
|
||||
|
||||
// FIXME: Update the screens again when QGuiApplication::screenAdded/Removed signals are emitted
|
||||
|
||||
// NOTE: The first item in QGuiApplication::screens is always the primary screen.
|
||||
// This is not specified in the documentation but QGuiApplication::primaryScreen
|
||||
// always returns the first item in the list if it isn't empty.
|
||||
client().async_update_screen_rects(screen_rects, 0);
|
||||
}
|
||||
|
||||
if (!m_webdriver_content_ipc_path.is_empty())
|
||||
client().async_connect_to_webdriver(m_webdriver_content_ipc_path);
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
|
||||
{
|
||||
if (m_client_state.back_bitmap.id == bitmap_id) {
|
||||
m_client_state.has_usable_bitmap = true;
|
||||
m_client_state.back_bitmap.pending_paints--;
|
||||
m_client_state.back_bitmap.last_painted_size = size;
|
||||
swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
|
||||
// We don't need the backup bitmap anymore, so drop it.
|
||||
m_backup_bitmap = nullptr;
|
||||
viewport()->update();
|
||||
|
||||
if (m_client_state.got_repaint_requests_while_painting) {
|
||||
m_client_state.got_repaint_requests_while_painting = false;
|
||||
request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_invalidate_content_rect(Badge<WebContentClient>, [[maybe_unused]] Gfx::IntRect const& content_rect)
|
||||
{
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_change_selection(Badge<WebContentClient>)
|
||||
{
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_cursor_change(Badge<WebContentClient>, Gfx::StandardCursor cursor)
|
||||
{
|
||||
switch (cursor) {
|
||||
case Gfx::StandardCursor::Hidden:
|
||||
setCursor(Qt::BlankCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Arrow:
|
||||
setCursor(Qt::ArrowCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Crosshair:
|
||||
setCursor(Qt::CrossCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::IBeam:
|
||||
setCursor(Qt::IBeamCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::ResizeHorizontal:
|
||||
setCursor(Qt::SizeHorCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::ResizeVertical:
|
||||
setCursor(Qt::SizeVerCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::ResizeDiagonalTLBR:
|
||||
setCursor(Qt::SizeFDiagCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::ResizeDiagonalBLTR:
|
||||
setCursor(Qt::SizeBDiagCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::ResizeColumn:
|
||||
setCursor(Qt::SplitHCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::ResizeRow:
|
||||
setCursor(Qt::SplitVCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Hand:
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Help:
|
||||
setCursor(Qt::WhatsThisCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Drag:
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::DragCopy:
|
||||
setCursor(Qt::DragCopyCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Move:
|
||||
setCursor(Qt::DragMoveCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Wait:
|
||||
setCursor(Qt::BusyCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Disallowed:
|
||||
setCursor(Qt::ForbiddenCursor);
|
||||
break;
|
||||
case Gfx::StandardCursor::Eyedropper:
|
||||
case Gfx::StandardCursor::Zoom:
|
||||
// FIXME: No corresponding Qt cursors, default to Arrow
|
||||
default:
|
||||
setCursor(Qt::ArrowCursor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_layout(Badge<WebContentClient>, Gfx::IntSize content_size)
|
||||
{
|
||||
verticalScrollBar()->setMinimum(0);
|
||||
verticalScrollBar()->setMaximum(content_size.height() - m_viewport_rect.height());
|
||||
verticalScrollBar()->setPageStep(m_viewport_rect.height());
|
||||
horizontalScrollBar()->setMinimum(0);
|
||||
horizontalScrollBar()->setMaximum(content_size.width() - m_viewport_rect.width());
|
||||
horizontalScrollBar()->setPageStep(m_viewport_rect.width());
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_scroll(Badge<WebContentClient>, i32 x_delta, i32 y_delta)
|
||||
{
|
||||
horizontalScrollBar()->setValue(max(0, horizontalScrollBar()->value() + x_delta));
|
||||
verticalScrollBar()->setValue(max(0, verticalScrollBar()->value() + y_delta));
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_scroll_to(Badge<WebContentClient>, Gfx::IntPoint scroll_position)
|
||||
{
|
||||
horizontalScrollBar()->setValue(scroll_position.x());
|
||||
verticalScrollBar()->setValue(scroll_position.y());
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_scroll_into_view(Badge<WebContentClient>, Gfx::IntRect const& rect)
|
||||
{
|
||||
if (m_viewport_rect.contains(rect))
|
||||
return;
|
||||
|
||||
if (rect.top() < m_viewport_rect.top())
|
||||
verticalScrollBar()->setValue(rect.top());
|
||||
else if (rect.top() > m_viewport_rect.top() && rect.bottom() > m_viewport_rect.bottom())
|
||||
verticalScrollBar()->setValue(rect.bottom() - m_viewport_rect.height());
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_enter_tooltip_area(Badge<WebContentClient>, Gfx::IntPoint content_position, DeprecatedString const& tooltip)
|
||||
{
|
||||
auto widget_position = to_widget_position(content_position);
|
||||
QToolTip::showText(
|
||||
mapToGlobal(QPoint(widget_position.x(), widget_position.y())),
|
||||
qstring_from_ak_deprecated_string(tooltip),
|
||||
this);
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_leave_tooltip_area(Badge<WebContentClient>)
|
||||
{
|
||||
QToolTip::hideText();
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_alert(Badge<WebContentClient>, String const& message)
|
||||
{
|
||||
m_dialog = new QMessageBox(QMessageBox::Icon::Warning, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok, this);
|
||||
m_dialog->exec();
|
||||
|
||||
client().async_alert_closed();
|
||||
m_dialog = nullptr;
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_confirm(Badge<WebContentClient>, String const& message)
|
||||
{
|
||||
m_dialog = new QMessageBox(QMessageBox::Icon::Question, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, this);
|
||||
auto result = m_dialog->exec();
|
||||
|
||||
client().async_confirm_closed(result == QMessageBox::StandardButton::Ok || result == QDialog::Accepted);
|
||||
m_dialog = nullptr;
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_prompt(Badge<WebContentClient>, String const& message, String const& default_)
|
||||
{
|
||||
m_dialog = new QInputDialog(this);
|
||||
auto& dialog = static_cast<QInputDialog&>(*m_dialog);
|
||||
|
||||
dialog.setWindowTitle("Ladybird");
|
||||
dialog.setLabelText(qstring_from_ak_string(message));
|
||||
dialog.setTextValue(qstring_from_ak_string(default_));
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
client().async_prompt_closed(ak_string_from_qstring(dialog.textValue()).release_value_but_fixme_should_propagate_errors());
|
||||
else
|
||||
client().async_prompt_closed({});
|
||||
|
||||
m_dialog = nullptr;
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_set_prompt_text(Badge<WebContentClient>, String const& message)
|
||||
{
|
||||
if (m_dialog && is<QInputDialog>(*m_dialog))
|
||||
static_cast<QInputDialog&>(*m_dialog).setTextValue(qstring_from_ak_string(message));
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_accept_dialog(Badge<WebContentClient>)
|
||||
{
|
||||
if (m_dialog)
|
||||
m_dialog->accept();
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_dismiss_dialog(Badge<WebContentClient>)
|
||||
{
|
||||
if (m_dialog)
|
||||
m_dialog->reject();
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_request_file(Badge<WebContentClient>, DeprecatedString const& path, i32 request_id)
|
||||
{
|
||||
auto file = Core::File::open(path, Core::File::OpenMode::Read);
|
||||
if (file.is_error())
|
||||
client().async_handle_file_return(file.error().code(), {}, request_id);
|
||||
else
|
||||
client().async_handle_file_return(0, IPC::File(*file.value()), request_id);
|
||||
}
|
||||
|
||||
Gfx::IntRect WebContentView::viewport_rect() const
|
||||
{
|
||||
return m_viewport_rect;
|
||||
}
|
||||
|
||||
Gfx::IntPoint WebContentView::to_content_position(Gfx::IntPoint widget_position) const
|
||||
{
|
||||
return widget_position.translated(max(0, horizontalScrollBar()->value()), max(0, verticalScrollBar()->value()));
|
||||
}
|
||||
|
||||
Gfx::IntPoint WebContentView::to_widget_position(Gfx::IntPoint content_position) const
|
||||
{
|
||||
return content_position.translated(-(max(0, horizontalScrollBar()->value())), -(max(0, verticalScrollBar()->value())));
|
||||
}
|
||||
|
||||
bool WebContentView::event(QEvent* event)
|
||||
{
|
||||
// NOTE: We have to implement event() manually as Qt's focus navigation mechanism
|
||||
// eats all the Tab key presses by default.
|
||||
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
keyPressEvent(static_cast<QKeyEvent*>(event));
|
||||
return true;
|
||||
}
|
||||
if (event->type() == QEvent::KeyRelease) {
|
||||
keyReleaseEvent(static_cast<QKeyEvent*>(event));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event->type() == QEvent::PaletteChange) {
|
||||
update_palette();
|
||||
request_repaint();
|
||||
return QAbstractScrollArea::event(event);
|
||||
}
|
||||
|
||||
return QAbstractScrollArea::event(event);
|
||||
}
|
||||
|
||||
void WebContentView::notify_server_did_finish_handling_input_event(bool event_was_accepted)
|
||||
{
|
||||
// FIXME: Currently Ladybird handles the keyboard shortcuts before passing the event to web content, so
|
||||
// we don't need to do anything here. But we'll need to once we start asking web content first.
|
||||
(void)event_was_accepted;
|
||||
}
|
||||
|
||||
ErrorOr<String> WebContentView::dump_layout_tree()
|
||||
{
|
||||
return String::from_deprecated_string(client().dump_layout_tree());
|
||||
}
|
||||
|
||||
}
|
123
Ladybird/Qt/WebContentView.h
Normal file
123
Ladybird/Qt/WebContentView.h
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/URL.h>
|
||||
#include <Ladybird/Types.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/StandardCursor.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/CSS/Selector.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <QAbstractScrollArea>
|
||||
#include <QPointer>
|
||||
#include <QUrl>
|
||||
|
||||
class QTextEdit;
|
||||
class QLineEdit;
|
||||
|
||||
namespace WebView {
|
||||
class WebContentClient;
|
||||
}
|
||||
|
||||
using WebView::WebContentClient;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class Tab;
|
||||
|
||||
class WebContentView final
|
||||
: public QAbstractScrollArea
|
||||
, public WebView::ViewImplementation {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WebContentView(StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode, UseLagomNetworking);
|
||||
virtual ~WebContentView() override;
|
||||
|
||||
Function<String(const AK::URL&, Web::HTML::ActivateTab)> on_tab_open_request;
|
||||
|
||||
virtual void paintEvent(QPaintEvent*) override;
|
||||
virtual void resizeEvent(QResizeEvent*) override;
|
||||
virtual void wheelEvent(QWheelEvent*) override;
|
||||
virtual void mouseMoveEvent(QMouseEvent*) override;
|
||||
virtual void mousePressEvent(QMouseEvent*) override;
|
||||
virtual void mouseReleaseEvent(QMouseEvent*) override;
|
||||
virtual void mouseDoubleClickEvent(QMouseEvent*) override;
|
||||
virtual void dragEnterEvent(QDragEnterEvent*) override;
|
||||
virtual void dropEvent(QDropEvent*) override;
|
||||
virtual void keyPressEvent(QKeyEvent* event) override;
|
||||
virtual void keyReleaseEvent(QKeyEvent* event) override;
|
||||
virtual void showEvent(QShowEvent*) override;
|
||||
virtual void hideEvent(QHideEvent*) override;
|
||||
virtual void focusInEvent(QFocusEvent*) override;
|
||||
virtual void focusOutEvent(QFocusEvent*) override;
|
||||
virtual bool event(QEvent*) override;
|
||||
|
||||
ErrorOr<String> dump_layout_tree();
|
||||
|
||||
void set_viewport_rect(Gfx::IntRect);
|
||||
void set_window_size(Gfx::IntSize);
|
||||
void set_window_position(Gfx::IntPoint);
|
||||
|
||||
enum class PaletteMode {
|
||||
Default,
|
||||
Dark,
|
||||
};
|
||||
void update_palette(PaletteMode = PaletteMode::Default);
|
||||
|
||||
virtual void notify_server_did_layout(Badge<WebContentClient>, Gfx::IntSize content_size) override;
|
||||
virtual void notify_server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize) override;
|
||||
virtual void notify_server_did_invalidate_content_rect(Badge<WebContentClient>, Gfx::IntRect const&) override;
|
||||
virtual void notify_server_did_change_selection(Badge<WebContentClient>) override;
|
||||
virtual void notify_server_did_request_cursor_change(Badge<WebContentClient>, Gfx::StandardCursor cursor) override;
|
||||
virtual void notify_server_did_request_scroll(Badge<WebContentClient>, i32, i32) override;
|
||||
virtual void notify_server_did_request_scroll_to(Badge<WebContentClient>, Gfx::IntPoint) override;
|
||||
virtual void notify_server_did_request_scroll_into_view(Badge<WebContentClient>, Gfx::IntRect const&) override;
|
||||
virtual void notify_server_did_enter_tooltip_area(Badge<WebContentClient>, Gfx::IntPoint, DeprecatedString const&) override;
|
||||
virtual void notify_server_did_leave_tooltip_area(Badge<WebContentClient>) override;
|
||||
virtual void notify_server_did_request_alert(Badge<WebContentClient>, String const& message) override;
|
||||
virtual void notify_server_did_request_confirm(Badge<WebContentClient>, String const& message) override;
|
||||
virtual void notify_server_did_request_prompt(Badge<WebContentClient>, String const& message, String const& default_) override;
|
||||
virtual void notify_server_did_request_set_prompt_text(Badge<WebContentClient>, String const& message) override;
|
||||
virtual void notify_server_did_request_accept_dialog(Badge<WebContentClient>) override;
|
||||
virtual void notify_server_did_request_dismiss_dialog(Badge<WebContentClient>) override;
|
||||
virtual void notify_server_did_request_file(Badge<WebContentClient>, DeprecatedString const& path, i32) override;
|
||||
virtual void notify_server_did_finish_handling_input_event(bool event_was_accepted) override;
|
||||
|
||||
signals:
|
||||
void urls_dropped(QList<QUrl> const&);
|
||||
|
||||
private:
|
||||
// ^WebView::ViewImplementation
|
||||
virtual void create_client(WebView::EnableCallgrindProfiling = WebView::EnableCallgrindProfiling::No) override;
|
||||
virtual void update_zoom() override;
|
||||
virtual Gfx::IntRect viewport_rect() const override;
|
||||
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override;
|
||||
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override;
|
||||
|
||||
void update_viewport_rect();
|
||||
|
||||
qreal m_inverse_pixel_scaling_ratio { 1.0 };
|
||||
bool m_should_show_line_box_borders { false };
|
||||
UseLagomNetworking m_use_lagom_networking {};
|
||||
|
||||
QPointer<QDialog> m_dialog;
|
||||
|
||||
Gfx::IntRect m_viewport_rect;
|
||||
|
||||
StringView m_webdriver_content_ipc_path;
|
||||
};
|
||||
|
||||
}
|
34
Ladybird/Qt/WebSocketClientManagerQt.cpp
Normal file
34
Ladybird/Qt/WebSocketClientManagerQt.cpp
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebSocketClientManagerQt.h"
|
||||
#include "WebSocketImplQt.h"
|
||||
#include "WebSocketQt.h"
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
NonnullRefPtr<WebSocketClientManagerQt> WebSocketClientManagerQt::create()
|
||||
{
|
||||
return adopt_ref(*new WebSocketClientManagerQt());
|
||||
}
|
||||
|
||||
WebSocketClientManagerQt::WebSocketClientManagerQt() = default;
|
||||
WebSocketClientManagerQt::~WebSocketClientManagerQt() = default;
|
||||
|
||||
RefPtr<Web::WebSockets::WebSocketClientSocket> WebSocketClientManagerQt::connect(AK::URL const& url, DeprecatedString const& origin, Vector<DeprecatedString> const& protocols)
|
||||
{
|
||||
WebSocket::ConnectionInfo connection_info(url);
|
||||
connection_info.set_origin(origin);
|
||||
connection_info.set_protocols(protocols);
|
||||
|
||||
auto impl = adopt_ref(*new WebSocketImplQt);
|
||||
auto web_socket = WebSocket::WebSocket::create(move(connection_info), move(impl));
|
||||
web_socket->start();
|
||||
return WebSocketQt::create(web_socket);
|
||||
}
|
||||
|
||||
}
|
28
Ladybird/Qt/WebSocketClientManagerQt.h
Normal file
28
Ladybird/Qt/WebSocketClientManagerQt.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/WebSockets/WebSocket.h>
|
||||
#include <LibWebSocket/ConnectionInfo.h>
|
||||
#include <LibWebSocket/Message.h>
|
||||
#include <LibWebSocket/WebSocket.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebSocketClientManagerQt : public Web::WebSockets::WebSocketClientManager {
|
||||
public:
|
||||
static NonnullRefPtr<WebSocketClientManagerQt> create();
|
||||
|
||||
virtual ~WebSocketClientManagerQt() override;
|
||||
virtual RefPtr<Web::WebSockets::WebSocketClientSocket> connect(AK::URL const&, DeprecatedString const& origin, Vector<DeprecatedString> const& protocols) override;
|
||||
|
||||
private:
|
||||
WebSocketClientManagerQt();
|
||||
};
|
||||
|
||||
}
|
97
Ladybird/Qt/WebSocketImplQt.cpp
Normal file
97
Ladybird/Qt/WebSocketImplQt.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Dex♪ <dexes.ttp@gmail.com>
|
||||
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||
* Copyright (c) 2022, the SerenityOS developers.
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebSocketImplQt.h"
|
||||
#include "StringUtils.h"
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <QSslSocket>
|
||||
#include <QTcpSocket>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
WebSocketImplQt::~WebSocketImplQt() = default;
|
||||
WebSocketImplQt::WebSocketImplQt() = default;
|
||||
|
||||
bool WebSocketImplQt::can_read_line()
|
||||
{
|
||||
return m_socket->canReadLine();
|
||||
}
|
||||
|
||||
bool WebSocketImplQt::send(ReadonlyBytes bytes)
|
||||
{
|
||||
auto bytes_written = m_socket->write(reinterpret_cast<char const*>(bytes.data()), bytes.size());
|
||||
if (bytes_written == -1)
|
||||
return false;
|
||||
VERIFY(static_cast<size_t>(bytes_written) == bytes.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketImplQt::eof()
|
||||
{
|
||||
return m_socket->state() == QTcpSocket::SocketState::UnconnectedState
|
||||
&& !m_socket->bytesAvailable();
|
||||
}
|
||||
|
||||
void WebSocketImplQt::discard_connection()
|
||||
{
|
||||
m_socket = nullptr;
|
||||
}
|
||||
|
||||
void WebSocketImplQt::connect(WebSocket::ConnectionInfo const& connection_info)
|
||||
{
|
||||
VERIFY(!m_socket);
|
||||
VERIFY(on_connected);
|
||||
VERIFY(on_connection_error);
|
||||
VERIFY(on_ready_to_read);
|
||||
|
||||
if (connection_info.is_secure()) {
|
||||
auto ssl_socket = make<QSslSocket>();
|
||||
ssl_socket->connectToHostEncrypted(
|
||||
qstring_from_ak_string(connection_info.url().serialized_host().release_value_but_fixme_should_propagate_errors()),
|
||||
connection_info.url().port_or_default());
|
||||
QObject::connect(ssl_socket.ptr(), &QSslSocket::alertReceived, [this](QSsl::AlertLevel level, QSsl::AlertType, QString const&) {
|
||||
if (level == QSsl::AlertLevel::Fatal)
|
||||
on_connection_error();
|
||||
});
|
||||
m_socket = move(ssl_socket);
|
||||
} else {
|
||||
m_socket = make<QTcpSocket>();
|
||||
m_socket->connectToHost(
|
||||
qstring_from_ak_string(connection_info.url().serialized_host().release_value_but_fixme_should_propagate_errors()),
|
||||
connection_info.url().port_or_default());
|
||||
}
|
||||
|
||||
QObject::connect(m_socket.ptr(), &QTcpSocket::readyRead, [this] {
|
||||
on_ready_to_read();
|
||||
});
|
||||
|
||||
QObject::connect(m_socket.ptr(), &QTcpSocket::connected, [this] {
|
||||
on_connected();
|
||||
});
|
||||
}
|
||||
|
||||
ErrorOr<ByteBuffer> WebSocketImplQt::read(int max_size)
|
||||
{
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(max_size));
|
||||
auto bytes_read = m_socket->read(reinterpret_cast<char*>(buffer.data()), buffer.size());
|
||||
if (bytes_read == -1)
|
||||
return Error::from_string_literal("WebSocketImplQt::read(): Error reading from socket");
|
||||
return buffer.slice(0, bytes_read);
|
||||
}
|
||||
|
||||
ErrorOr<DeprecatedString> WebSocketImplQt::read_line(size_t size)
|
||||
{
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(size));
|
||||
auto bytes_read = m_socket->readLine(reinterpret_cast<char*>(buffer.data()), buffer.size());
|
||||
if (bytes_read == -1)
|
||||
return Error::from_string_literal("WebSocketImplQt::read_line(): Error reading from socket");
|
||||
return DeprecatedString::copy(buffer.span().slice(0, bytes_read));
|
||||
}
|
||||
|
||||
}
|
35
Ladybird/Qt/WebSocketImplQt.h
Normal file
35
Ladybird/Qt/WebSocketImplQt.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Dex♪ <dexes.ttp@gmail.com>
|
||||
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||
* Copyright (c) 2022, the SerenityOS developers.
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWebSocket/Impl/WebSocketImpl.h>
|
||||
|
||||
class QTcpSocket;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebSocketImplQt final : public WebSocket::WebSocketImpl {
|
||||
public:
|
||||
explicit WebSocketImplQt();
|
||||
virtual ~WebSocketImplQt() override;
|
||||
|
||||
virtual void connect(WebSocket::ConnectionInfo const&) override;
|
||||
virtual bool can_read_line() override;
|
||||
virtual ErrorOr<DeprecatedString> read_line(size_t) override;
|
||||
virtual ErrorOr<ByteBuffer> read(int max_size) override;
|
||||
virtual bool send(ReadonlyBytes) override;
|
||||
virtual bool eof() override;
|
||||
virtual void discard_connection() override;
|
||||
|
||||
private:
|
||||
OwnPtr<QTcpSocket> m_socket;
|
||||
};
|
||||
|
||||
}
|
97
Ladybird/Qt/WebSocketQt.cpp
Normal file
97
Ladybird/Qt/WebSocketQt.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebSocketQt.h"
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
NonnullRefPtr<WebSocketQt> WebSocketQt::create(NonnullRefPtr<WebSocket::WebSocket> underlying_socket)
|
||||
{
|
||||
return adopt_ref(*new WebSocketQt(move(underlying_socket)));
|
||||
}
|
||||
|
||||
WebSocketQt::WebSocketQt(NonnullRefPtr<WebSocket::WebSocket> underlying_socket)
|
||||
: m_websocket(move(underlying_socket))
|
||||
{
|
||||
m_websocket->on_open = [weak_this = make_weak_ptr()] {
|
||||
if (auto strong_this = weak_this.strong_ref())
|
||||
if (strong_this->on_open)
|
||||
strong_this->on_open();
|
||||
};
|
||||
m_websocket->on_message = [weak_this = make_weak_ptr()](auto message) {
|
||||
if (auto strong_this = weak_this.strong_ref()) {
|
||||
if (strong_this->on_message) {
|
||||
strong_this->on_message(Web::WebSockets::WebSocketClientSocket::Message {
|
||||
.data = move(message.data()),
|
||||
.is_text = message.is_text(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
m_websocket->on_error = [weak_this = make_weak_ptr()](auto error) {
|
||||
if (auto strong_this = weak_this.strong_ref()) {
|
||||
if (strong_this->on_error) {
|
||||
switch (error) {
|
||||
case WebSocket::WebSocket::Error::CouldNotEstablishConnection:
|
||||
strong_this->on_error(Web::WebSockets::WebSocketClientSocket::Error::CouldNotEstablishConnection);
|
||||
return;
|
||||
case WebSocket::WebSocket::Error::ConnectionUpgradeFailed:
|
||||
strong_this->on_error(Web::WebSockets::WebSocketClientSocket::Error::ConnectionUpgradeFailed);
|
||||
return;
|
||||
case WebSocket::WebSocket::Error::ServerClosedSocket:
|
||||
strong_this->on_error(Web::WebSockets::WebSocketClientSocket::Error::ServerClosedSocket);
|
||||
return;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
};
|
||||
m_websocket->on_close = [weak_this = make_weak_ptr()](u16 code, DeprecatedString reason, bool was_clean) {
|
||||
if (auto strong_this = weak_this.strong_ref())
|
||||
if (strong_this->on_close)
|
||||
strong_this->on_close(code, move(reason), was_clean);
|
||||
};
|
||||
}
|
||||
|
||||
WebSocketQt::~WebSocketQt() = default;
|
||||
|
||||
Web::WebSockets::WebSocket::ReadyState WebSocketQt::ready_state()
|
||||
{
|
||||
switch (m_websocket->ready_state()) {
|
||||
case WebSocket::ReadyState::Connecting:
|
||||
return Web::WebSockets::WebSocket::ReadyState::Connecting;
|
||||
case WebSocket::ReadyState::Open:
|
||||
return Web::WebSockets::WebSocket::ReadyState::Open;
|
||||
case WebSocket::ReadyState::Closing:
|
||||
return Web::WebSockets::WebSocket::ReadyState::Closing;
|
||||
case WebSocket::ReadyState::Closed:
|
||||
return Web::WebSockets::WebSocket::ReadyState::Closed;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
DeprecatedString WebSocketQt::subprotocol_in_use()
|
||||
{
|
||||
return m_websocket->subprotocol_in_use();
|
||||
}
|
||||
|
||||
void WebSocketQt::send(ByteBuffer binary_or_text_message, bool is_text)
|
||||
{
|
||||
m_websocket->send(WebSocket::Message(binary_or_text_message, is_text));
|
||||
}
|
||||
|
||||
void WebSocketQt::send(StringView message)
|
||||
{
|
||||
m_websocket->send(WebSocket::Message(message));
|
||||
}
|
||||
|
||||
void WebSocketQt::close(u16 code, DeprecatedString reason)
|
||||
{
|
||||
m_websocket->close(code, reason);
|
||||
}
|
||||
|
||||
}
|
35
Ladybird/Qt/WebSocketQt.h
Normal file
35
Ladybird/Qt/WebSocketQt.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/WebSockets/WebSocket.h>
|
||||
#include <LibWebSocket/WebSocket.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebSocketQt
|
||||
: public Web::WebSockets::WebSocketClientSocket
|
||||
, public Weakable<WebSocketQt> {
|
||||
public:
|
||||
static NonnullRefPtr<WebSocketQt> create(NonnullRefPtr<WebSocket::WebSocket>);
|
||||
|
||||
virtual ~WebSocketQt() override;
|
||||
|
||||
virtual Web::WebSockets::WebSocket::ReadyState ready_state() override;
|
||||
virtual DeprecatedString subprotocol_in_use() override;
|
||||
virtual void send(ByteBuffer binary_or_text_message, bool is_text) override;
|
||||
virtual void send(StringView message) override;
|
||||
virtual void close(u16 code, DeprecatedString reason) override;
|
||||
|
||||
private:
|
||||
explicit WebSocketQt(NonnullRefPtr<WebSocket::WebSocket>);
|
||||
|
||||
NonnullRefPtr<WebSocket::WebSocket> m_websocket;
|
||||
};
|
||||
|
||||
}
|
8
Ladybird/Qt/ladybird.qrc
Normal file
8
Ladybird/Qt/ladybird.qrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file>../Icons/ladybird.png</file>
|
||||
<file>../Icons/back.tvg</file>
|
||||
<file>../Icons/forward.tvg</file>
|
||||
<file>../Icons/reload.tvg</file>
|
||||
</qresource>
|
||||
</RCC>
|
125
Ladybird/Qt/main.cpp
Normal file
125
Ladybird/Qt/main.cpp
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "BrowserWindow.h"
|
||||
#include "EventLoopImplementationQt.h"
|
||||
#include "Settings.h"
|
||||
#include "WebContentView.h"
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <Browser/CookieJar.h>
|
||||
#include <Browser/Database.h>
|
||||
#include <Ladybird/HelperProcess.h>
|
||||
#include <Ladybird/Utilities.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/Process.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibGfx/Font/FontDatabase.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibSQL/SQLClient.h>
|
||||
#include <QApplication>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
OwnPtr<Ladybird::Settings> s_settings;
|
||||
|
||||
bool is_using_dark_system_theme(QWidget& widget)
|
||||
{
|
||||
// FIXME: Qt does not provide any method to query if the system is using a dark theme. We will have to implement
|
||||
// platform-specific methods if we wish to have better detection. For now, this inspects if Qt is using a
|
||||
// dark color for widget backgrounds using Rec. 709 luma coefficients.
|
||||
// https://en.wikipedia.org/wiki/Rec._709#Luma_coefficients
|
||||
|
||||
auto color = widget.palette().color(widget.backgroundRole());
|
||||
auto luma = 0.2126f * color.redF() + 0.7152f * color.greenF() + 0.0722f * color.blueF();
|
||||
|
||||
return luma <= 0.5f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static ErrorOr<void> handle_attached_debugger()
|
||||
{
|
||||
#ifdef AK_OS_LINUX
|
||||
// Let's ignore SIGINT if we're being debugged because GDB
|
||||
// incorrectly forwards the signal to us even when it's set to
|
||||
// "nopass". See https://sourceware.org/bugzilla/show_bug.cgi?id=9425
|
||||
// for details.
|
||||
if (TRY(Core::Process::is_being_debugged())) {
|
||||
dbgln("Debugger is attached, ignoring SIGINT");
|
||||
TRY(Core::System::signal(SIGINT, SIG_IGN));
|
||||
}
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||
{
|
||||
QApplication app(arguments.argc, arguments.argv);
|
||||
|
||||
Core::EventLoopManager::install(*new Ladybird::EventLoopManagerQt);
|
||||
Core::EventLoop event_loop;
|
||||
static_cast<Ladybird::EventLoopImplementationQt&>(event_loop.impl()).set_main_loop();
|
||||
|
||||
TRY(handle_attached_debugger());
|
||||
|
||||
platform_init();
|
||||
|
||||
// NOTE: We only instantiate this to ensure that Gfx::FontDatabase has its default queries initialized.
|
||||
Gfx::FontDatabase::set_default_font_query("Katica 10 400 0");
|
||||
Gfx::FontDatabase::set_fixed_width_font_query("Csilla 10 400 0");
|
||||
|
||||
StringView raw_url;
|
||||
StringView webdriver_content_ipc_path;
|
||||
bool enable_callgrind_profiling = false;
|
||||
bool enable_sql_database = false;
|
||||
bool use_ast_interpreter = false;
|
||||
bool use_lagom_networking = false;
|
||||
|
||||
Core::ArgsParser args_parser;
|
||||
args_parser.set_general_help("The Ladybird web browser :^)");
|
||||
args_parser.add_positional_argument(raw_url, "URL to open", "url", Core::ArgsParser::Required::No);
|
||||
args_parser.add_option(webdriver_content_ipc_path, "Path to WebDriver IPC for WebContent", "webdriver-content-path", 0, "path");
|
||||
args_parser.add_option(enable_callgrind_profiling, "Enable Callgrind profiling", "enable-callgrind-profiling", 'P');
|
||||
args_parser.add_option(enable_sql_database, "Enable SQL database", "enable-sql-database", 0);
|
||||
args_parser.add_option(use_ast_interpreter, "Enable JavaScript AST interpreter (deprecated)", "ast", 0);
|
||||
args_parser.add_option(use_lagom_networking, "Enable Lagom servers for networking", "enable-lagom-networking", 0);
|
||||
args_parser.parse(arguments);
|
||||
|
||||
auto get_formatted_url = [&](StringView const& raw_url) -> ErrorOr<URL> {
|
||||
URL url = raw_url;
|
||||
if (FileSystem::exists(raw_url))
|
||||
url = URL::create_with_file_scheme(TRY(FileSystem::real_path(raw_url)).to_deprecated_string());
|
||||
else if (!url.is_valid())
|
||||
url = DeprecatedString::formatted("https://{}", raw_url);
|
||||
return url;
|
||||
};
|
||||
|
||||
RefPtr<Browser::Database> database;
|
||||
|
||||
if (enable_sql_database) {
|
||||
auto sql_server_paths = TRY(get_paths_for_helper_process("SQLServer"sv));
|
||||
auto sql_client = TRY(SQL::SQLClient::launch_server_and_create_client(move(sql_server_paths)));
|
||||
database = TRY(Browser::Database::create(move(sql_client)));
|
||||
}
|
||||
|
||||
auto cookie_jar = database ? TRY(Browser::CookieJar::create(*database)) : Browser::CookieJar::create();
|
||||
|
||||
Ladybird::s_settings = adopt_own_if_nonnull(new Ladybird::Settings());
|
||||
Ladybird::BrowserWindow window(cookie_jar, webdriver_content_ipc_path, enable_callgrind_profiling ? WebView::EnableCallgrindProfiling::Yes : WebView::EnableCallgrindProfiling::No, use_ast_interpreter ? WebView::UseJavaScriptBytecode::No : WebView::UseJavaScriptBytecode::Yes, use_lagom_networking ? Ladybird::UseLagomNetworking::Yes : Ladybird::UseLagomNetworking::No);
|
||||
window.setWindowTitle("Ladybird");
|
||||
window.resize(800, 600);
|
||||
window.show();
|
||||
|
||||
if (auto url = TRY(get_formatted_url(raw_url)); url.is_valid()) {
|
||||
window.view().load(url);
|
||||
} else {
|
||||
window.view().load("about:blank"sv);
|
||||
}
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue