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