QT Frontend: Add a Loading screen with progressbar
With shader caches on the horizon, one requirement is to provide visible feedback for the progress. The shader cache reportedly takes several minutes to load for large caches that were invalidated, and as such we should provide a loading screen with progress. Adds a loading screen widget that will be shown until the first frame of the game is swapped. This was chosen in case shader caches are not being used, several games still take more than a few seconds to launch and could benefit from a loading screen.
This commit is contained in:
parent
83f8d1aa2e
commit
08fcf41b0a
|
@ -45,5 +45,8 @@ function(copy_yuzu_Qt5_deps target_dir)
|
||||||
|
|
||||||
windows_copy_files(yuzu ${Qt5_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$<CONFIG:Debug>:d>.*)
|
windows_copy_files(yuzu ${Qt5_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$<CONFIG:Debug>:d>.*)
|
||||||
windows_copy_files(yuzu ${Qt5_STYLES_DIR} ${STYLES} qwindowsvistastyle$<$<CONFIG:Debug>:d>.*)
|
windows_copy_files(yuzu ${Qt5_STYLES_DIR} ${STYLES} qwindowsvistastyle$<$<CONFIG:Debug>:d>.*)
|
||||||
windows_copy_files(yuzu ${Qt5_IMAGEFORMATS_DIR} ${IMAGEFORMATS} qjpeg$<$<CONFIG:Debug>:d>.*)
|
windows_copy_files(yuzu ${Qt5_IMAGEFORMATS_DIR} ${IMAGEFORMATS}
|
||||||
|
qjpeg$<$<CONFIG:Debug>:d>.*
|
||||||
|
qgif$<$<CONFIG:Debug>:d>.*
|
||||||
|
)
|
||||||
endfunction(copy_yuzu_Qt5_deps)
|
endfunction(copy_yuzu_Qt5_deps)
|
||||||
|
|
|
@ -68,6 +68,8 @@ add_executable(yuzu
|
||||||
game_list_p.h
|
game_list_p.h
|
||||||
game_list_worker.cpp
|
game_list_worker.cpp
|
||||||
game_list_worker.h
|
game_list_worker.h
|
||||||
|
loading_screen.cpp
|
||||||
|
loading_screen.h
|
||||||
hotkeys.cpp
|
hotkeys.cpp
|
||||||
hotkeys.h
|
hotkeys.h
|
||||||
main.cpp
|
main.cpp
|
||||||
|
@ -102,9 +104,10 @@ set(UIS
|
||||||
configuration/configure_system.ui
|
configuration/configure_system.ui
|
||||||
configuration/configure_touchscreen_advanced.ui
|
configuration/configure_touchscreen_advanced.ui
|
||||||
configuration/configure_web.ui
|
configuration/configure_web.ui
|
||||||
hotkeys.ui
|
|
||||||
main.ui
|
|
||||||
compatdb.ui
|
compatdb.ui
|
||||||
|
hotkeys.ui
|
||||||
|
loading_screen.ui
|
||||||
|
main.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
file(GLOB COMPAT_LIST
|
file(GLOB COMPAT_LIST
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include "common/microprofile.h"
|
#include "common/microprofile.h"
|
||||||
#include "common/scm_rev.h"
|
#include "common/scm_rev.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
|
@ -17,6 +15,7 @@
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
#include "yuzu/bootmanager.h"
|
#include "yuzu/bootmanager.h"
|
||||||
|
#include "yuzu/main.h"
|
||||||
|
|
||||||
EmuThread::EmuThread(GRenderWindow* render_window) : render_window(render_window) {}
|
EmuThread::EmuThread(GRenderWindow* render_window) : render_window(render_window) {}
|
||||||
|
|
||||||
|
@ -114,6 +113,8 @@ GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread)
|
||||||
|
|
||||||
InputCommon::Init();
|
InputCommon::Init();
|
||||||
InputCommon::StartJoystickEventHandler();
|
InputCommon::StartJoystickEventHandler();
|
||||||
|
connect(this, &GRenderWindow::FirstFrameDisplayed, static_cast<GMainWindow*>(parent),
|
||||||
|
&GMainWindow::OnLoadComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
GRenderWindow::~GRenderWindow() {
|
GRenderWindow::~GRenderWindow() {
|
||||||
|
@ -141,6 +142,10 @@ void GRenderWindow::SwapBuffers() {
|
||||||
child->makeCurrent();
|
child->makeCurrent();
|
||||||
|
|
||||||
child->swapBuffers();
|
child->swapBuffers();
|
||||||
|
if (!first_frame) {
|
||||||
|
emit FirstFrameDisplayed();
|
||||||
|
first_frame = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GRenderWindow::MakeCurrent() {
|
void GRenderWindow::MakeCurrent() {
|
||||||
|
@ -309,6 +314,8 @@ void GRenderWindow::InitRenderTarget() {
|
||||||
delete layout();
|
delete layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
first_frame = false;
|
||||||
|
|
||||||
// TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground,
|
// TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground,
|
||||||
// WA_DontShowOnScreen, WA_DeleteOnClose
|
// WA_DontShowOnScreen, WA_DeleteOnClose
|
||||||
QGLFormat fmt;
|
QGLFormat fmt;
|
||||||
|
|
|
@ -152,6 +152,7 @@ public slots:
|
||||||
signals:
|
signals:
|
||||||
/// Emitted when the window is closed
|
/// Emitted when the window is closed
|
||||||
void Closed();
|
void Closed();
|
||||||
|
void FirstFrameDisplayed();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::pair<unsigned, unsigned> ScaleTouch(const QPointF pos) const;
|
std::pair<unsigned, unsigned> ScaleTouch(const QPointF pos) const;
|
||||||
|
@ -171,6 +172,8 @@ private:
|
||||||
/// Temporary storage of the screenshot taken
|
/// Temporary storage of the screenshot taken
|
||||||
QImage screenshot_image;
|
QImage screenshot_image;
|
||||||
|
|
||||||
|
bool first_frame = false;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void showEvent(QShowEvent* event) override;
|
void showEvent(QShowEvent* event) override;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2019 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMovie>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QStyleOption>
|
||||||
|
#include <QWindow>
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
#include "ui_loading_screen.h"
|
||||||
|
#include "yuzu/loading_screen.h"
|
||||||
|
|
||||||
|
LoadingScreen::LoadingScreen(QWidget* parent)
|
||||||
|
: QWidget(parent), ui(std::make_unique<Ui::LoadingScreen>()) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
// Progress bar is hidden until we have a use for it.
|
||||||
|
ui->progress_bar->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingScreen::~LoadingScreen() = default;
|
||||||
|
|
||||||
|
void LoadingScreen::Prepare(Loader::AppLoader& loader) {
|
||||||
|
std::vector<u8> buffer;
|
||||||
|
if (loader.ReadBanner(buffer) == Loader::ResultStatus::Success) {
|
||||||
|
backing_mem =
|
||||||
|
std::make_unique<QByteArray>(reinterpret_cast<char*>(buffer.data()), buffer.size());
|
||||||
|
backing_buf = std::make_unique<QBuffer>(backing_mem.get());
|
||||||
|
backing_buf->open(QIODevice::ReadOnly);
|
||||||
|
animation = std::make_unique<QMovie>(backing_buf.get(), QByteArray("GIF"));
|
||||||
|
animation->start();
|
||||||
|
ui->banner->setMovie(animation.get());
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
if (loader.ReadLogo(buffer) == Loader::ResultStatus::Success) {
|
||||||
|
QPixmap map;
|
||||||
|
map.loadFromData(buffer.data(), buffer.size());
|
||||||
|
ui->logo->setPixmap(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadingScreen::OnLoadProgress(std::size_t value, std::size_t total) {
|
||||||
|
if (total != previous_total) {
|
||||||
|
ui->progress_bar->setMaximum(total);
|
||||||
|
previous_total = total;
|
||||||
|
}
|
||||||
|
ui->progress_bar->setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadingScreen::paintEvent(QPaintEvent* event) {
|
||||||
|
QStyleOption opt;
|
||||||
|
opt.init(this);
|
||||||
|
QPainter p(this);
|
||||||
|
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
|
||||||
|
QWidget::paintEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadingScreen::Clear() {
|
||||||
|
animation.reset();
|
||||||
|
backing_buf.reset();
|
||||||
|
backing_mem.reset();
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright 2019 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
class AppLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class LoadingScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QBuffer;
|
||||||
|
class QByteArray;
|
||||||
|
class QMovie;
|
||||||
|
|
||||||
|
class LoadingScreen : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LoadingScreen(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
~LoadingScreen();
|
||||||
|
|
||||||
|
/// Call before showing the loading screen to load the widgets with the logo and banner for the
|
||||||
|
/// currently loaded application.
|
||||||
|
void Prepare(Loader::AppLoader& loader);
|
||||||
|
|
||||||
|
/// After the loading screen is hidden, the owner of this class can call this to clean up any
|
||||||
|
/// used resources such as the logo and banner.
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
// In order to use a custom widget with a stylesheet, you need to override the paintEvent
|
||||||
|
// See https://wiki.qt.io/How_to_Change_the_Background_Color_of_QWidget
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
void OnLoadProgress(std::size_t value, std::size_t total);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<QMovie> animation;
|
||||||
|
std::unique_ptr<QBuffer> backing_buf;
|
||||||
|
std::unique_ptr<QByteArray> backing_mem;
|
||||||
|
std::unique_ptr<Ui::LoadingScreen> ui;
|
||||||
|
std::size_t previous_total = 0;
|
||||||
|
};
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>LoadingScreen</class>
|
||||||
|
<widget class="QWidget" name="LoadingScreen">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>746</width>
|
||||||
|
<height>495</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">background-color: rgb(0, 0, 0);</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="logo">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
<property name="margin">
|
||||||
|
<number>30</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QProgressBar" name="progress_bar">
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">font-size: 26px;</string>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="format">
|
||||||
|
<string>Loading Shaders %v out of %m</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item alignment="Qt::AlignRight|Qt::AlignBottom">
|
||||||
|
<widget class="QLabel" name="banner">
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">background-color: black;</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="margin">
|
||||||
|
<number>30</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -92,6 +92,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
||||||
#include "yuzu/game_list.h"
|
#include "yuzu/game_list.h"
|
||||||
#include "yuzu/game_list_p.h"
|
#include "yuzu/game_list_p.h"
|
||||||
#include "yuzu/hotkeys.h"
|
#include "yuzu/hotkeys.h"
|
||||||
|
#include "yuzu/loading_screen.h"
|
||||||
#include "yuzu/main.h"
|
#include "yuzu/main.h"
|
||||||
#include "yuzu/ui_settings.h"
|
#include "yuzu/ui_settings.h"
|
||||||
|
|
||||||
|
@ -411,6 +412,10 @@ void GMainWindow::InitializeWidgets() {
|
||||||
game_list = new GameList(vfs, this);
|
game_list = new GameList(vfs, this);
|
||||||
ui.horizontalLayout->addWidget(game_list);
|
ui.horizontalLayout->addWidget(game_list);
|
||||||
|
|
||||||
|
loading_screen = new LoadingScreen(this);
|
||||||
|
loading_screen->hide();
|
||||||
|
ui.horizontalLayout->addWidget(loading_screen);
|
||||||
|
|
||||||
// Create status bar
|
// Create status bar
|
||||||
message_label = new QLabel();
|
message_label = new QLabel();
|
||||||
// Configured separately for left alignment
|
// Configured separately for left alignment
|
||||||
|
@ -897,8 +902,9 @@ void GMainWindow::BootGame(const QString& filename) {
|
||||||
.arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc,
|
.arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc,
|
||||||
QString::fromStdString(title_name)));
|
QString::fromStdString(title_name)));
|
||||||
|
|
||||||
render_window->show();
|
loading_screen->Prepare(Core::System::GetInstance().GetAppLoader());
|
||||||
render_window->setFocus();
|
loading_screen->show();
|
||||||
|
loading_screen->setFocus();
|
||||||
|
|
||||||
emulation_running = true;
|
emulation_running = true;
|
||||||
if (ui.action_Fullscreen->isChecked()) {
|
if (ui.action_Fullscreen->isChecked()) {
|
||||||
|
@ -932,6 +938,8 @@ void GMainWindow::ShutdownGame() {
|
||||||
ui.action_Load_Amiibo->setEnabled(false);
|
ui.action_Load_Amiibo->setEnabled(false);
|
||||||
ui.action_Capture_Screenshot->setEnabled(false);
|
ui.action_Capture_Screenshot->setEnabled(false);
|
||||||
render_window->hide();
|
render_window->hide();
|
||||||
|
loading_screen->hide();
|
||||||
|
loading_screen->Clear();
|
||||||
game_list->show();
|
game_list->show();
|
||||||
game_list->setFilterFocus();
|
game_list->setFilterFocus();
|
||||||
setWindowTitle(QString("yuzu %1| %2-%3")
|
setWindowTitle(QString("yuzu %1| %2-%3")
|
||||||
|
@ -1505,6 +1513,13 @@ void GMainWindow::OnStopGame() {
|
||||||
ShutdownGame();
|
ShutdownGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GMainWindow::OnLoadComplete() {
|
||||||
|
loading_screen->hide();
|
||||||
|
loading_screen->Clear();
|
||||||
|
render_window->show();
|
||||||
|
render_window->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
void GMainWindow::OnMenuReportCompatibility() {
|
void GMainWindow::OnMenuReportCompatibility() {
|
||||||
if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) {
|
if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) {
|
||||||
CompatDB compatdb{this};
|
CompatDB compatdb{this};
|
||||||
|
@ -1771,9 +1786,8 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
|
||||||
this, tr("Confirm Key Rederivation"),
|
this, tr("Confirm Key Rederivation"),
|
||||||
tr("You are about to force rederive all of your keys. \nIf you do not know what this "
|
tr("You are about to force rederive all of your keys. \nIf you do not know what this "
|
||||||
"means or what you are doing, \nthis is a potentially destructive action. \nPlease "
|
"means or what you are doing, \nthis is a potentially destructive action. \nPlease "
|
||||||
"make "
|
"make sure this is what you want \nand optionally make backups.\n\nThis will delete "
|
||||||
"sure this is what you want \nand optionally make backups.\n\nThis will delete your "
|
"your autogenerated key files and re-run the key derivation module."),
|
||||||
"autogenerated key files and re-run the key derivation module."),
|
|
||||||
QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
|
QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
|
||||||
|
|
||||||
if (res == QMessageBox::Cancel)
|
if (res == QMessageBox::Cancel)
|
||||||
|
@ -1818,7 +1832,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
|
||||||
errors +
|
errors +
|
||||||
tr("<br><br>You can get all of these and dump all of your games easily by "
|
tr("<br><br>You can get all of these and dump all of your games easily by "
|
||||||
"following <a href='https://yuzu-emu.org/help/quickstart/'>the "
|
"following <a href='https://yuzu-emu.org/help/quickstart/'>the "
|
||||||
"quickstart guide</a>. Alternatively, you can use another method of dumping "
|
"quickstart guide</a>. Alternatively, you can use another method of dumping"
|
||||||
"to obtain all of your keys."));
|
"to obtain all of your keys."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ class GImageInfo;
|
||||||
class GraphicsBreakPointsWidget;
|
class GraphicsBreakPointsWidget;
|
||||||
class GraphicsSurfaceWidget;
|
class GraphicsSurfaceWidget;
|
||||||
class GRenderWindow;
|
class GRenderWindow;
|
||||||
|
class LoadingScreen;
|
||||||
class MicroProfileDialog;
|
class MicroProfileDialog;
|
||||||
class ProfilerWidget;
|
class ProfilerWidget;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
@ -109,10 +110,10 @@ signals:
|
||||||
void WebBrowserFinishedBrowsing();
|
void WebBrowserFinishedBrowsing();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
void OnLoadComplete();
|
||||||
void ProfileSelectorSelectProfile();
|
void ProfileSelectorSelectProfile();
|
||||||
void SoftwareKeyboardGetText(const Core::Frontend::SoftwareKeyboardParameters& parameters);
|
void SoftwareKeyboardGetText(const Core::Frontend::SoftwareKeyboardParameters& parameters);
|
||||||
void SoftwareKeyboardInvokeCheckDialog(std::u16string error_message);
|
void SoftwareKeyboardInvokeCheckDialog(std::u16string error_message);
|
||||||
|
|
||||||
void WebBrowserOpenPage(std::string_view filename, std::string_view arguments);
|
void WebBrowserOpenPage(std::string_view filename, std::string_view arguments);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -212,6 +213,7 @@ private:
|
||||||
|
|
||||||
GRenderWindow* render_window;
|
GRenderWindow* render_window;
|
||||||
GameList* game_list;
|
GameList* game_list;
|
||||||
|
LoadingScreen* loading_screen;
|
||||||
|
|
||||||
// Status bar elements
|
// Status bar elements
|
||||||
QLabel* message_label = nullptr;
|
QLabel* message_label = nullptr;
|
||||||
|
|
Reference in New Issue