duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

qtutils.cpp (10612B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "qtutils.h"
      5 
      6 #include "core/game_list.h"
      7 #include "core/system.h"
      8 
      9 #include "common/log.h"
     10 
     11 #include <QtCore/QCoreApplication>
     12 #include <QtCore/QMetaObject>
     13 #include <QtGui/QDesktopServices>
     14 #include <QtGui/QGuiApplication>
     15 #include <QtGui/QKeyEvent>
     16 #include <QtWidgets/QComboBox>
     17 #include <QtWidgets/QDialog>
     18 #include <QtWidgets/QHeaderView>
     19 #include <QtWidgets/QInputDialog>
     20 #include <QtWidgets/QLabel>
     21 #include <QtWidgets/QMainWindow>
     22 #include <QtWidgets/QMessageBox>
     23 #include <QtWidgets/QScrollBar>
     24 #include <QtWidgets/QSlider>
     25 #include <QtWidgets/QStatusBar>
     26 #include <QtWidgets/QStyle>
     27 #include <QtWidgets/QTableView>
     28 #include <QtWidgets/QTreeView>
     29 #include <algorithm>
     30 #include <array>
     31 #include <map>
     32 
     33 #if !defined(_WIN32) && !defined(APPLE)
     34 #include <qpa/qplatformnativeinterface.h>
     35 #endif
     36 
     37 #ifdef _WIN32
     38 #include "common/windows_headers.h"
     39 #endif
     40 
     41 Log_SetChannel(QtUtils);
     42 
     43 namespace QtUtils {
     44 
     45 QFrame* CreateHorizontalLine(QWidget* parent)
     46 {
     47   QFrame* line = new QFrame(parent);
     48   line->setFrameShape(QFrame::HLine);
     49   line->setFrameShadow(QFrame::Sunken);
     50   return line;
     51 }
     52 
     53 QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog)
     54 {
     55   QWidget* next_parent = widget->parentWidget();
     56   while (next_parent)
     57   {
     58     if (stop_at_window_or_dialog && (widget->metaObject()->inherits(&QMainWindow::staticMetaObject) ||
     59                                      widget->metaObject()->inherits(&QDialog::staticMetaObject)))
     60     {
     61       break;
     62     }
     63 
     64     widget = next_parent;
     65     next_parent = widget->parentWidget();
     66   }
     67 
     68   return widget;
     69 }
     70 
     71 void ShowOrRaiseWindow(QWidget* window)
     72 {
     73   if (!window)
     74     return;
     75 
     76   if (!window->isVisible())
     77   {
     78     window->show();
     79   }
     80   else
     81   {
     82     window->raise();
     83     window->activateWindow();
     84     window->setFocus();
     85   }
     86 }
     87 
     88 template<typename T>
     89 ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initializer_list<int>& widths)
     90 {
     91   QHeaderView* header;
     92   if constexpr (std::is_same_v<T, QTableView>)
     93     header = view->horizontalHeader();
     94   else
     95     header = view->header();
     96 
     97   const int min_column_width = header->minimumSectionSize();
     98   const int scrollbar_width = ((view->verticalScrollBar() && view->verticalScrollBar()->isVisible()) ||
     99                                view->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOn) ?
    100                                 view->verticalScrollBar()->width() :
    101                                 0;
    102   int num_flex_items = 0;
    103   int total_width = 0;
    104   int column_index = 0;
    105   for (const int spec_width : widths)
    106   {
    107     if (!view->isColumnHidden(column_index))
    108     {
    109       if (spec_width < 0)
    110         num_flex_items++;
    111       else
    112         total_width += std::max(spec_width, min_column_width);
    113     }
    114 
    115     column_index++;
    116   }
    117 
    118   const int flex_width =
    119     (num_flex_items > 0) ?
    120       std::max((view->contentsRect().width() - total_width - scrollbar_width) / num_flex_items, 1) :
    121       0;
    122 
    123   column_index = 0;
    124   for (const int spec_width : widths)
    125   {
    126     if (view->isColumnHidden(column_index))
    127     {
    128       column_index++;
    129       continue;
    130     }
    131 
    132     const int width = spec_width < 0 ? flex_width : (std::max(spec_width, min_column_width));
    133     view->setColumnWidth(column_index, width);
    134     column_index++;
    135   }
    136 }
    137 
    138 void ResizeColumnsForTableView(QTableView* view, const std::initializer_list<int>& widths)
    139 {
    140   ResizeColumnsForView(view, widths);
    141 }
    142 
    143 void ResizeColumnsForTreeView(QTreeView* view, const std::initializer_list<int>& widths)
    144 {
    145   ResizeColumnsForView(view, widths);
    146 }
    147 
    148 void OpenURL(QWidget* parent, const QUrl& qurl)
    149 {
    150   if (!QDesktopServices::openUrl(qurl))
    151   {
    152     QMessageBox::critical(parent, QObject::tr("Failed to open URL"),
    153                           QObject::tr("Failed to open URL.\n\nThe URL was: %1").arg(qurl.toString()));
    154   }
    155 }
    156 
    157 void OpenURL(QWidget* parent, const char* url)
    158 {
    159   return OpenURL(parent, QUrl::fromEncoded(QByteArray(url, static_cast<int>(std::strlen(url)))));
    160 }
    161 
    162 std::optional<unsigned> PromptForAddress(QWidget* parent, const QString& title, const QString& label, bool code)
    163 {
    164   const QString address_str(
    165     QInputDialog::getText(parent, title, qApp->translate("DebuggerWindow", "Enter memory address:")));
    166   if (address_str.isEmpty())
    167     return std::nullopt;
    168 
    169   bool ok;
    170   uint address;
    171   if (address_str.startsWith("0x"))
    172     address = address_str.mid(2).toUInt(&ok, 16);
    173   else
    174     address = address_str.toUInt(&ok, 16);
    175   if (code)
    176     address = address & 0xFFFFFFFC; // disassembly address should be divisible by 4 so make sure
    177 
    178   if (!ok)
    179   {
    180     QMessageBox::critical(
    181       parent, title,
    182       qApp->translate("DebuggerWindow", "Invalid address. It should be in hex (0x12345678 or 12345678)"));
    183     return std::nullopt;
    184   }
    185 
    186   return address;
    187 }
    188 
    189 QString StringViewToQString(std::string_view str)
    190 {
    191   return str.empty() ? QString() : QString::fromUtf8(str.data(), str.size());
    192 }
    193 
    194 void SetWidgetFontForInheritedSetting(QWidget* widget, bool inherited)
    195 {
    196   if (widget->font().italic() != inherited)
    197   {
    198     QFont new_font(widget->font());
    199     new_font.setItalic(inherited);
    200     widget->setFont(new_font);
    201   }
    202 }
    203 
    204 void BindLabelToSlider(QSlider* slider, QLabel* label, float range /*= 1.0f*/)
    205 {
    206   auto update_label = [label, range](int new_value) {
    207     label->setText(QString::number(static_cast<int>(new_value) / range));
    208   };
    209   update_label(slider->value());
    210   QObject::connect(slider, &QSlider::valueChanged, label, std::move(update_label));
    211 }
    212 
    213 void SetWindowResizeable(QWidget* widget, bool resizeable)
    214 {
    215   if (QMainWindow* window = qobject_cast<QMainWindow*>(widget); window)
    216   {
    217     // update status bar grip if present
    218     if (QStatusBar* sb = window->statusBar(); sb)
    219       sb->setSizeGripEnabled(resizeable);
    220   }
    221 
    222   if ((widget->sizePolicy().horizontalPolicy() == QSizePolicy::Preferred) != resizeable)
    223   {
    224     if (resizeable)
    225     {
    226       // Min/max numbers come from uic.
    227       widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
    228       widget->setMinimumSize(1, 1);
    229       widget->setMaximumSize(16777215, 16777215);
    230     }
    231     else
    232     {
    233       widget->setFixedSize(widget->size());
    234       widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    235     }
    236   }
    237 }
    238 
    239 void ResizePotentiallyFixedSizeWindow(QWidget* widget, int width, int height)
    240 {
    241   width = std::max(width, 1);
    242   height = std::max(height, 1);
    243   if (widget->sizePolicy().horizontalPolicy() == QSizePolicy::Fixed)
    244     widget->setFixedSize(width, height);
    245 
    246   widget->resize(width, height);
    247 }
    248 
    249 QIcon GetIconForRegion(ConsoleRegion region)
    250 {
    251   switch (region)
    252   {
    253     case ConsoleRegion::NTSC_J:
    254       return QIcon(QStringLiteral(":/icons/flag-jp.svg"));
    255     case ConsoleRegion::PAL:
    256       return QIcon(QStringLiteral(":/icons/flag-eu.svg"));
    257     case ConsoleRegion::NTSC_U:
    258       return QIcon(QStringLiteral(":/icons/flag-uc.svg"));
    259     default:
    260       return QIcon::fromTheme(QStringLiteral("file-unknow-line"));
    261   }
    262 }
    263 
    264 QIcon GetIconForRegion(DiscRegion region)
    265 {
    266   switch (region)
    267   {
    268     case DiscRegion::NTSC_J:
    269       return QIcon(QStringLiteral(":/icons/flag-jp.svg"));
    270     case DiscRegion::PAL:
    271       return QIcon(QStringLiteral(":/icons/flag-eu.svg"));
    272     case DiscRegion::NTSC_U:
    273       return QIcon(QStringLiteral(":/icons/flag-uc.svg"));
    274     case DiscRegion::Other:
    275     case DiscRegion::NonPS1:
    276     default:
    277       return QIcon::fromTheme(QStringLiteral("file-unknow-line"));
    278   }
    279 }
    280 
    281 QIcon GetIconForEntryType(GameList::EntryType type)
    282 {
    283   switch (type)
    284   {
    285     case GameList::EntryType::Disc:
    286       return QIcon::fromTheme(QStringLiteral("disc-line"));
    287     case GameList::EntryType::Playlist:
    288     case GameList::EntryType::DiscSet:
    289       return QIcon::fromTheme(QStringLiteral("play-list-2-line"));
    290     case GameList::EntryType::PSF:
    291       return QIcon::fromTheme(QStringLiteral("file-music-line"));
    292     case GameList::EntryType::PSExe:
    293     default:
    294       return QIcon::fromTheme(QStringLiteral("settings-3-line"));
    295   }
    296 }
    297 
    298 QIcon GetIconForCompatibility(GameDatabase::CompatibilityRating rating)
    299 {
    300   return QIcon(QStringLiteral(":/icons/star-%1.png").arg(static_cast<u32>(rating)));
    301 }
    302 
    303 qreal GetDevicePixelRatioForWidget(const QWidget* widget)
    304 {
    305   const QScreen* screen_for_ratio = widget->screen();
    306   if (!screen_for_ratio)
    307     screen_for_ratio = QGuiApplication::primaryScreen();
    308 
    309   return screen_for_ratio ? screen_for_ratio->devicePixelRatio() : static_cast<qreal>(1);
    310 }
    311 
    312 std::optional<WindowInfo> GetWindowInfoForWidget(QWidget* widget)
    313 {
    314   WindowInfo wi;
    315 
    316   // Windows and Apple are easy here since there's no display connection.
    317 #if defined(_WIN32)
    318   wi.type = WindowInfo::Type::Win32;
    319   wi.window_handle = reinterpret_cast<void*>(widget->winId());
    320 #elif defined(__APPLE__)
    321   wi.type = WindowInfo::Type::MacOS;
    322   wi.window_handle = reinterpret_cast<void*>(widget->winId());
    323 #else
    324   QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface();
    325   const QString platform_name = QGuiApplication::platformName();
    326   if (platform_name == QStringLiteral("xcb"))
    327   {
    328     wi.type = WindowInfo::Type::X11;
    329     wi.display_connection = pni->nativeResourceForWindow("display", widget->windowHandle());
    330     wi.window_handle = reinterpret_cast<void*>(widget->winId());
    331   }
    332   else if (platform_name == QStringLiteral("wayland"))
    333   {
    334     wi.type = WindowInfo::Type::Wayland;
    335     wi.display_connection = pni->nativeResourceForWindow("display", widget->windowHandle());
    336     wi.window_handle = pni->nativeResourceForWindow("surface", widget->windowHandle());
    337   }
    338   else
    339   {
    340     qCritical() << "Unknown PNI platform " << platform_name;
    341     return std::nullopt;
    342   }
    343 #endif
    344 
    345   const qreal dpr = GetDevicePixelRatioForWidget(widget);
    346   wi.surface_width = static_cast<u32>(static_cast<qreal>(widget->width()) * dpr);
    347   wi.surface_height = static_cast<u32>(static_cast<qreal>(widget->height()) * dpr);
    348   wi.surface_scale = static_cast<float>(dpr);
    349 
    350   // Query refresh rate, we need it for sync.
    351   std::optional<float> surface_refresh_rate = WindowInfo::QueryRefreshRateForWindow(wi);
    352   if (!surface_refresh_rate.has_value())
    353   {
    354     // Fallback to using the screen, getting the rate for Wayland is an utter mess otherwise.
    355     const QScreen* widget_screen = widget->screen();
    356     if (!widget_screen)
    357       widget_screen = QGuiApplication::primaryScreen();
    358     surface_refresh_rate = widget_screen ? static_cast<float>(widget_screen->refreshRate()) : 0.0f;
    359   }
    360 
    361   wi.surface_refresh_rate = surface_refresh_rate.value();
    362   INFO_LOG("Surface refresh rate: {} hz", wi.surface_refresh_rate);
    363 
    364   return wi;
    365 }
    366 
    367 } // namespace QtUtils