#include "gs_frame.h" #include "gui_settings.h" #include "Utilities/Config.h" #include "Utilities/Timer.h" #include "Utilities/date_time.h" #include "Utilities/File.h" #include "Emu/System.h" #include "Emu/system_config.h" #include "Emu/system_progress.hpp" #include "Emu/IdManager.h" #include "Emu/Cell/Modules/cellScreenshot.h" #include "Emu/Cell/Modules/cellVideoOut.h" #include "Emu/RSX/rsx_utils.h" #include #include #include #include #include #include #include #include #include #include "png.h" #ifdef _WIN32 #include #elif defined(__APPLE__) //nothing #else #ifdef VK_USE_PLATFORM_WAYLAND_KHR #include #include #endif #ifdef HAVE_X11 #include #endif #endif #ifdef _WIN32 #include #elif HAVE_QTDBUS #include #include #endif LOG_CHANNEL(screenshot_log, "SCREENSHOT"); LOG_CHANNEL(mark_log, "MARK"); LOG_CHANNEL(gui_log, "GUI"); extern atomic_t g_user_asked_for_frame_capture; extern atomic_t g_disable_frame_limit; constexpr auto qstr = QString::fromStdString; gs_frame::gs_frame(QScreen* screen, const QRect& geometry, const QIcon& appIcon, std::shared_ptr gui_settings) : QWindow() , m_initial_geometry(geometry) , m_gui_settings(std::move(gui_settings)) { m_disable_mouse = m_gui_settings->GetValue(gui::gs_disableMouse).toBool(); m_disable_kb_hotkeys = m_gui_settings->GetValue(gui::gs_disableKbHotkeys).toBool(); m_show_mouse_in_fullscreen = m_gui_settings->GetValue(gui::gs_showMouseFs).toBool(); m_lock_mouse_in_fullscreen = m_gui_settings->GetValue(gui::gs_lockMouseFs).toBool(); m_hide_mouse_after_idletime = m_gui_settings->GetValue(gui::gs_hideMouseIdle).toBool(); m_hide_mouse_idletime = m_gui_settings->GetValue(gui::gs_hideMouseIdleTime).toUInt(); m_window_title = Emu.GetFormattedTitle(0); if (!appIcon.isNull()) { setIcon(appIcon); } #ifdef __APPLE__ // Needed for MoltenVK to work properly on MacOS if (g_cfg.video.renderer == video_renderer::vulkan) setSurfaceType(QSurface::VulkanSurface); #endif // NOTE: You cannot safely create a wayland window that has hidden initial status and perform any changes on the window while it is still hidden. // Doing this will create a surface with deferred commands that require a buffer. When binding to your session, this may assert in your compositor due to protocol restrictions. Visibility startup_visibility = Hidden; #ifndef _WIN32 if (const char* session_type = ::getenv("XDG_SESSION_TYPE")) { if (!strcasecmp(session_type, "wayland")) { // Start windowed. This is a featureless rectangle on-screen with no window decorations. // It does not even resemble a window until the WM attaches later on. // Fullscreen could technically work with some fiddling, but easily breaks depending on geometry input. startup_visibility = Windowed; } } #endif setMinimumWidth(160); setMinimumHeight(90); setScreen(screen); setGeometry(geometry); setTitle(qstr(m_window_title)); setVisibility(startup_visibility); create(); // Change cursor when in fullscreen. connect(this, &QWindow::visibilityChanged, this, [this](QWindow::Visibility visibility) { handle_cursor(visibility, true, true); }); // Change cursor when this window gets or loses focus. connect(this, &QWindow::activeChanged, this, [this]() { handle_cursor(visibility(), false, true); }); // Configure the mouse hide on idle timer connect(&m_mousehide_timer, &QTimer::timeout, this, &gs_frame::MouseHideTimeout); m_mousehide_timer.setSingleShot(true); #ifdef _WIN32 m_tb_button = new QWinTaskbarButton(); m_tb_progress = m_tb_button->progress(); m_tb_progress->setRange(0, m_gauge_max); m_tb_progress->setVisible(false); #elif HAVE_QTDBUS UpdateProgress(0, false); m_progress_value = 0; #endif } gs_frame::~gs_frame() { #ifdef _WIN32 // QWinTaskbarProgress::hide() will crash if the application is already about to close, even if the object is not null. if (m_tb_progress && !QCoreApplication::closingDown()) { m_tb_progress->hide(); } if (m_tb_button) { m_tb_button->deleteLater(); } #elif HAVE_QTDBUS UpdateProgress(0, false); #endif } void gs_frame::paintEvent(QPaintEvent *event) { Q_UNUSED(event) } void gs_frame::showEvent(QShowEvent *event) { // We have to calculate new window positions, since the frame is only known once the window was created. // We will try to find the originally requested dimensions if possible by moving the frame. // NOTES: The parameter m_initial_geometry is not necessarily equal to our actual geometry() at this point. // That's why we use m_initial_geometry instead of the frameGeometry() in some places. // All of these values, including the screen geometry, can also be negative numbers. const QRect available_geometry = screen()->availableGeometry(); // The available screen geometry const QRect inner_geometry = geometry(); // The current inner geometry const QRect outer_geometry = frameGeometry(); // The current outer geometry // Calculate the left and top frame borders (this will include window handles) const int left_border = inner_geometry.left() - outer_geometry.left(); const int top_border = inner_geometry.top() - outer_geometry.top(); // Calculate the initially expected frame origin const QPoint expected_pos(m_initial_geometry.left() - left_border, m_initial_geometry.top() - top_border); // Make sure that the expected position is inside the screen (check left and top borders) QPoint pos(std::max(expected_pos.x(), available_geometry.left()), std::max(expected_pos.y(), available_geometry.top())); // Find the maximum position that still ensures that the frame is completely visible inside the screen (check right and bottom borders) QPoint max_pos(available_geometry.left() + available_geometry.width() - frameGeometry().width(), available_geometry.top() + available_geometry.height() - frameGeometry().height()); // Make sure that the "maximum" position is inside the screen (check left and top borders) max_pos.setX(std::max(max_pos.x(), available_geometry.left())); max_pos.setY(std::max(max_pos.y(), available_geometry.top())); // Adjust the expected position accordingly pos.setX(std::min(pos.x(), max_pos.x())); pos.setY(std::min(pos.y(), max_pos.y())); // Set the new position setFramePosition(pos); QWindow::showEvent(event); } void gs_frame::keyPressEvent(QKeyEvent *keyEvent) { if (keyEvent->isAutoRepeat()) { keyEvent->ignore(); return; } // NOTE: needs to be updated with keyboard_pad_handler::processKeyEvent switch (keyEvent->key()) { case Qt::Key_L: if (keyEvent->modifiers() == Qt::AltModifier) { static int count = 0; mark_log.success("Made forced mark %d in log", ++count); return; } else if (keyEvent->modifiers() == Qt::ControlModifier) { toggle_mouselock(); return; } break; case Qt::Key_Return: if (keyEvent->modifiers() == Qt::AltModifier) { toggle_fullscreen(); return; } break; case Qt::Key_Escape: if (visibility() == FullScreen) { toggle_fullscreen(); return; } break; case Qt::Key_P: if (keyEvent->modifiers() == Qt::ControlModifier && !m_disable_kb_hotkeys && Emu.IsRunning()) { Emu.Pause(); return; } break; case Qt::Key_R: if (keyEvent->modifiers() == Qt::ControlModifier && !m_disable_kb_hotkeys) { switch (Emu.GetStatus()) { case system_state::ready: { Emu.Run(true); return; } case system_state::paused: { Emu.Resume(); return; } default: break; } } break; case Qt::Key_C: if (keyEvent->modifiers() == Qt::AltModifier && !m_disable_kb_hotkeys) { g_user_asked_for_frame_capture = true; return; } break; case Qt::Key_F10: if (keyEvent->modifiers() == Qt::ControlModifier) { g_disable_frame_limit = !g_disable_frame_limit; gui_log.warning("%s boost mode", g_disable_frame_limit.load() ? "Enabled" : "Disabled"); return; } break; case Qt::Key_F12: screenshot_toggle = true; break; default: break; } } void gs_frame::toggle_fullscreen() { Emu.CallFromMainThread([this]() { if (visibility() == FullScreen) { // Change to the last recorded visibility. Sanitize it just in case. if (m_last_visibility != Visibility::Maximized && m_last_visibility != Visibility::Windowed) { m_last_visibility = Visibility::Windowed; } setVisibility(m_last_visibility); } else { // Backup visibility for exiting fullscreen mode later. Don't do this in the visibilityChanged slot, // since entering fullscreen from maximized will first change the visibility to windowed. m_last_visibility = visibility(); setVisibility(FullScreen); } }); } void gs_frame::toggle_mouselock() { // first we toggle the value m_mouse_hide_and_lock = !m_mouse_hide_and_lock; // and update the cursor handle_cursor(visibility(), false, true); } void gs_frame::update_cursor() { bool show_mouse; if (!isActive()) { // Show the mouse by default if this is not the active window show_mouse = true; } else if (m_hide_mouse_after_idletime && !m_mousehide_timer.isActive()) { // Hide the mouse if the idle timeout was reached (which means that the timer isn't running) show_mouse = false; } else if (visibility() == QWindow::Visibility::FullScreen) { // Fullscreen: Show or hide the mouse depending on the settings. show_mouse = m_show_mouse_in_fullscreen; } else { // Windowed: Hide the mouse if it was locked by the user show_mouse = !m_mouse_hide_and_lock; } // Update Cursor if necessary if (show_mouse != m_show_mouse.exchange(show_mouse)) { setCursor(m_show_mouse ? Qt::ArrowCursor : Qt::BlankCursor); } } bool gs_frame::get_mouse_lock_state() { handle_cursor(visibility(), false, true); return isActive() && m_mouse_hide_and_lock; } void gs_frame::hide_on_close() { if (!(+g_progr)) { // Hide the dialog before stopping if no progress bar is being shown. // Otherwise users might think that the game softlocked if stopping takes too long. QWindow::hide(); } } void gs_frame::close() { if (m_is_closing.exchange(true)) { gui_log.notice("Closing game window (ignored, already closing)"); return; } gui_log.notice("Closing game window"); Emu.CallFromMainThread([this]() { // Hide window if necessary hide_on_close(); if (!Emu.IsStopped()) { // Blocking shutdown request. Obsolete, but I'm keeping it here as last resort. Emu.GracefulShutdown(true, false); } deleteLater(); }); } bool gs_frame::shown() { return QWindow::isVisible(); } void gs_frame::hide() { Emu.CallFromMainThread([this]() { QWindow::hide(); }); } void gs_frame::show() { Emu.CallFromMainThread([this]() { QWindow::show(); if (g_cfg.misc.start_fullscreen) { setVisibility(FullScreen); } }); #ifdef _WIN32 // if we do this before show, the QWinTaskbarProgress won't show if (m_tb_button) { m_tb_button->setWindow(this); m_tb_progress->show(); } #endif } display_handle_t gs_frame::handle() const { #ifdef _WIN32 return reinterpret_cast(this->winId()); #elif defined(__APPLE__) return reinterpret_cast(this->winId()); //NSView #else #ifdef VK_USE_PLATFORM_WAYLAND_KHR QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); struct wl_display *wl_dpy = static_cast( native->nativeResourceForWindow("display", NULL)); struct wl_surface *wl_surf = static_cast( native->nativeResourceForWindow("surface", const_cast(static_cast(this)))); if (wl_dpy != nullptr && wl_surf != nullptr) { return std::make_pair(wl_dpy, wl_surf); } else { #endif #ifdef HAVE_X11 return std::make_pair(XOpenDisplay(0), static_cast(this->winId())); #else fmt::throw_exception("Vulkan X11 support disabled at compile-time."); #endif #ifdef VK_USE_PLATFORM_WAYLAND_KHR } #endif #endif } draw_context_t gs_frame::make_context() { return nullptr; } void gs_frame::set_current(draw_context_t context) { Q_UNUSED(context) } void gs_frame::delete_context(draw_context_t context) { Q_UNUSED(context) } int gs_frame::client_width() { #ifdef _WIN32 RECT rect; if (GetClientRect(reinterpret_cast(winId()), &rect)) { return rect.right - rect.left; } #endif // _WIN32 return width() * devicePixelRatio(); } int gs_frame::client_height() { #ifdef _WIN32 RECT rect; if (GetClientRect(reinterpret_cast(winId()), &rect)) { return rect.bottom - rect.top; } #endif // _WIN32 return height() * devicePixelRatio(); } double gs_frame::client_device_pixel_ratio() const { return devicePixelRatio(); } void gs_frame::flip(draw_context_t, bool /*skip_frame*/) { static Timer fps_t; if (!m_flip_showed_frame) { // Show on first flip m_flip_showed_frame = true; show(); fps_t.Start(); } ++m_frames; if (fps_t.GetElapsedTimeInSec() >= 0.5) { std::string new_title = Emu.GetFormattedTitle(m_frames / fps_t.GetElapsedTimeInSec()); if (new_title != m_window_title) { m_window_title = new_title; Emu.CallFromMainThread([this, title = std::move(new_title)]() { setTitle(qstr(title)); }); } m_frames = 0; fps_t.Start(); } } void gs_frame::take_screenshot(std::vector data, const u32 sshot_width, const u32 sshot_height, bool is_bgra) { std::thread( [sshot_width, sshot_height, is_bgra](std::vector sshot_data) { screenshot_log.notice("Taking screenshot (%dx%d)", sshot_width, sshot_height); const std::string& id = Emu.GetTitleID(); std::string screen_path = fs::get_config_dir() + "screenshots/"; if (!id.empty()) { screen_path += id + "/"; }; if (!fs::create_path(screen_path) && fs::g_tls_error != fs::error::exist) { screenshot_log.error("Failed to create screenshot path \"%s\" : %s", screen_path, fs::g_tls_error); return; } std::string filename = screen_path; if (!id.empty()) { filename += id + "_"; }; filename += "screenshot_" + date_time::current_time_narrow<'_'>() + ".png"; fs::file sshot_file(filename, fs::open_mode::create + fs::open_mode::write + fs::open_mode::excl); if (!sshot_file) { screenshot_log.error("Failed to save screenshot \"%s\" : %s", filename, fs::g_tls_error); return; } std::vector sshot_data_alpha(sshot_data.size()); const u32* sshot_ptr = reinterpret_cast(sshot_data.data()); u32* alpha_ptr = reinterpret_cast(sshot_data_alpha.data()); if (is_bgra) [[likely]] { for (usz index = 0; index < sshot_data.size() / sizeof(u32); index++) { alpha_ptr[index] = ((sshot_ptr[index] & 0xFF) << 16) | (sshot_ptr[index] & 0xFF00) | ((sshot_ptr[index] & 0xFF0000) >> 16) | 0xFF000000; } } else { for (usz index = 0; index < sshot_data.size() / sizeof(u32); index++) { alpha_ptr[index] = sshot_ptr[index] | 0xFF000000; } } screenshot_info manager; { auto& s = g_fxo->get(); std::lock_guard lock(s.mutex); manager = s; } struct scoped_png_ptrs { png_structp write_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); png_infop info_ptr = png_create_info_struct(write_ptr); ~scoped_png_ptrs() { png_free_data(write_ptr, info_ptr, PNG_FREE_ALL, -1); png_destroy_write_struct(&write_ptr, &info_ptr); } }; png_text text[6] = {}; int num_text = 0; const QDateTime date_time = QDateTime::currentDateTime(); const std::string creation_time = date_time.toString("yyyy:MM:dd hh:mm:ss").toStdString(); const std::string photo_title = manager.get_photo_title(); const std::string game_title = manager.get_game_title(); const std::string game_comment = manager.get_game_comment(); const std::string& title_id = Emu.GetTitleID(); // Write tEXt chunk text[num_text].compression = PNG_TEXT_COMPRESSION_NONE; text[num_text].key = const_cast("Creation Time"); text[num_text].text = const_cast(creation_time.c_str()); ++num_text; text[num_text].compression = PNG_TEXT_COMPRESSION_NONE; text[num_text].key = const_cast("Source"); text[num_text].text = const_cast("RPCS3"); // Originally PlayStation(R)3 ++num_text; text[num_text].compression = PNG_TEXT_COMPRESSION_NONE; text[num_text].key = const_cast("Title ID"); text[num_text].text = const_cast(title_id.c_str()); ++num_text; // Write tTXt chunk (they probably meant zTXt) text[num_text].compression = PNG_TEXT_COMPRESSION_zTXt; text[num_text].key = const_cast("Title"); text[num_text].text = const_cast(photo_title.c_str()); ++num_text; text[num_text].compression = PNG_TEXT_COMPRESSION_zTXt; text[num_text].key = const_cast("Game Title"); text[num_text].text = const_cast(game_title.c_str()); ++num_text; text[num_text].compression = PNG_TEXT_COMPRESSION_zTXt; text[num_text].key = const_cast("Comment"); text[num_text].text = const_cast(game_comment.c_str()); // Create image from data QImage img(sshot_data_alpha.data(), sshot_width, sshot_height, sshot_width * 4, QImage::Format_RGBA8888); // Scale image if necessary const auto& avconf = g_fxo->get(); auto new_size = avconf.aspect_convert_dimensions(size2u{ u32(img.width()), u32(img.height()) }); if (new_size.width != static_cast(img.width()) || new_size.height != static_cast(img.height())) { img = img.scaled(QSize(new_size.width, new_size.height), Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation); img.convertTo(QImage::Format_RGBA8888); // The current Qt version changes the image format during smooth scaling, so we have to change it back. } // Create row pointers for libpng std::vector rows(img.height()); for (int y = 0; y < img.height(); y++) rows[y] = img.scanLine(y); std::vector encoded_png; const auto write_png = [&]() { const scoped_png_ptrs ptrs; png_set_IHDR(ptrs.write_ptr, ptrs.info_ptr, img.width(), img.height(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_set_text(ptrs.write_ptr, ptrs.info_ptr, text, 6); png_set_rows(ptrs.write_ptr, ptrs.info_ptr, &rows[0]); png_set_write_fn(ptrs.write_ptr, &encoded_png, [](png_structp png_ptr, png_bytep data, png_size_t length) { std::vector* p = static_cast*>(png_get_io_ptr(png_ptr)); p->insert(p->end(), data, data + length); }, nullptr); png_write_png(ptrs.write_ptr, ptrs.info_ptr, PNG_TRANSFORM_IDENTITY, nullptr); }; write_png(); sshot_file.write(encoded_png.data(), encoded_png.size()); screenshot_log.success("Successfully saved screenshot to %s", filename); if (manager.is_enabled) { const std::string cell_sshot_overlay_path = manager.get_overlay_path(); if (fs::is_file(cell_sshot_overlay_path)) { screenshot_log.notice("Adding overlay to cell screenshot from %s", cell_sshot_overlay_path); QImage overlay_img; if (!overlay_img.load(qstr(cell_sshot_overlay_path))) { screenshot_log.error("Failed to read cell screenshot overlay '%s' : %s", cell_sshot_overlay_path, fs::g_tls_error); return; } // Games choose the overlay file and the offset based on the current video resolution. // We need to scale the overlay if our resolution scaling causes the image to have a different size. // Scale the resolution first (as seen before with the image) new_size = avconf.aspect_convert_dimensions(size2u{ avconf.resolution_x, avconf.resolution_y }); if (new_size.width != static_cast(img.width()) || new_size.height != static_cast(img.height())) { const int scale = rsx::get_resolution_scale_percent(); const int x = (scale * manager.overlay_offset_x) / 100; const int y = (scale * manager.overlay_offset_y) / 100; const int width = (scale * overlay_img.width()) / 100; const int height = (scale * overlay_img.height()) / 100; screenshot_log.notice("Scaling overlay from %dx%d at offset (%d,%d) to %dx%d at offset (%d,%d)", overlay_img.width(), overlay_img.height(), manager.overlay_offset_x, manager.overlay_offset_y, width, height, x, y); manager.overlay_offset_x = x; manager.overlay_offset_y = y; overlay_img = overlay_img.scaled(QSize(width, height), Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation); } if (manager.overlay_offset_x < static_cast(img.width()) && manager.overlay_offset_y < static_cast(img.height()) && manager.overlay_offset_x + overlay_img.width() > 0 && manager.overlay_offset_y + overlay_img.height() > 0) { QImage screenshot_img(rows[0], img.width(), img.height(), QImage::Format_RGBA8888); QPainter painter(&screenshot_img); painter.drawImage(manager.overlay_offset_x, manager.overlay_offset_y, overlay_img); std::memcpy(rows[0], screenshot_img.constBits(), screenshot_img.sizeInBytes()); screenshot_log.success("Applied screenshot overlay '%s'", cell_sshot_overlay_path); } } const std::string cell_sshot_filename = manager.get_screenshot_path(date_time.toString("yyyy/MM/dd").toStdString()); const std::string cell_sshot_dir = fs::get_parent_dir(cell_sshot_filename); screenshot_log.notice("Saving cell screenshot to %s", cell_sshot_filename); if (!fs::create_path(cell_sshot_dir) && fs::g_tls_error != fs::error::exist) { screenshot_log.error("Failed to create cell screenshot dir \"%s\" : %s", cell_sshot_dir, fs::g_tls_error); return; } fs::file cell_sshot_file(cell_sshot_filename, fs::open_mode::create + fs::open_mode::write + fs::open_mode::excl); if (!cell_sshot_file) { screenshot_log.error("Failed to save cell screenshot \"%s\" : %s", cell_sshot_filename, fs::g_tls_error); return; } encoded_png.clear(); write_png(); cell_sshot_file.write(encoded_png.data(), encoded_png.size()); screenshot_log.success("Successfully saved cell screenshot to %s", cell_sshot_filename); } // Play a sound Emu.CallFromMainThread([]() { if (const std::string sound_path = fs::get_config_dir() + "sounds/snd_screenshot.wav"; fs::is_file(sound_path)) { QSound::play(qstr(sound_path)); } else { QApplication::beep(); } }); return; }, std::move(data)) .detach(); } void gs_frame::mouseDoubleClickEvent(QMouseEvent* ev) { if (m_disable_mouse || g_cfg.io.move == move_handler::mouse) return; if (ev->button() == Qt::LeftButton) { toggle_fullscreen(); } } void gs_frame::handle_cursor(QWindow::Visibility visibility, bool from_event, bool start_idle_timer) { // Update the mouse lock state if the visibility changed. if (from_event) { // In fullscreen we default to hiding and locking. In windowed mode we do not want the lock by default. m_mouse_hide_and_lock = (visibility == QWindow::Visibility::FullScreen) && m_lock_mouse_in_fullscreen; } // Update the mouse hide timer if (m_hide_mouse_after_idletime && start_idle_timer) { m_mousehide_timer.start(m_hide_mouse_idletime); } else { m_mousehide_timer.stop(); } // Update the cursor visibility update_cursor(); } void gs_frame::MouseHideTimeout() { // Our idle timeout occured, so we update the cursor if (m_hide_mouse_after_idletime && m_show_mouse) { handle_cursor(visibility(), false, false); } } bool gs_frame::event(QEvent* ev) { if (ev->type() == QEvent::Close) { if (m_gui_settings->GetValue(gui::ib_confirm_exit).toBool()) { if (visibility() == FullScreen) { toggle_fullscreen(); } int result = QMessageBox::Yes; m_gui_settings->ShowConfirmationBox(tr("Exit Game?"), tr("Do you really want to exit the game?

Any unsaved progress will be lost!
"), gui::ib_confirm_exit, &result, nullptr); if (result != QMessageBox::Yes) { return true; } } gui_log.notice("Game window close event issued"); if (Emu.IsStopped()) { // This should be unreachable, but never say never. Properly close the window anyway. close(); } else { // Issue async shutdown Emu.GracefulShutdown(true, true); // Hide window if necessary hide_on_close(); // Do not propagate the close event. It will be closed by the rsx_thread. return true; } } else if (ev->type() == QEvent::MouseMove && (!m_show_mouse || m_mousehide_timer.isActive())) { // This will make the cursor visible again if it was hidden by the mouse idle timeout handle_cursor(visibility(), false, true); } return QWindow::event(ev); } void gs_frame::progress_reset(bool reset_limit) { #ifdef _WIN32 if (m_tb_progress) { m_tb_progress->reset(); } #elif HAVE_QTDBUS UpdateProgress(0, false); #endif if (reset_limit) { progress_set_limit(100); } } void gs_frame::progress_set_value(int value) { #ifdef _WIN32 if (m_tb_progress) { m_tb_progress->setValue(std::clamp(value, m_tb_progress->minimum(), m_tb_progress->maximum())); } #elif HAVE_QTDBUS m_progress_value = std::clamp(value, 0, m_gauge_max); UpdateProgress(m_progress_value, true); #endif } void gs_frame::progress_increment(int delta) { if (delta == 0) { return; } #ifdef _WIN32 if (m_tb_progress) { progress_set_value(m_tb_progress->value() + delta); } #elif HAVE_QTDBUS progress_set_value(m_progress_value + delta); #endif } void gs_frame::progress_set_limit(int limit) { #ifdef _WIN32 if (m_tb_progress) { m_tb_progress->setMaximum(limit); } #elif HAVE_QTDBUS m_gauge_max = limit; #endif } #ifdef HAVE_QTDBUS void gs_frame::UpdateProgress(int progress, bool progress_visible) { QDBusMessage message = QDBusMessage::createSignal ( QStringLiteral("/"), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update") ); QVariantMap properties; // Progress takes a value from 0.0 to 0.1 properties.insert(QStringLiteral("progress"), 1. * progress / m_gauge_max); properties.insert(QStringLiteral("progress-visible"), progress_visible); message << QStringLiteral("application://rpcs3.desktop") << properties; QDBusConnection::sessionBus().send(message); } #endif