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

logwindow.cpp (12743B)


      1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "logwindow.h"
      5 #include "mainwindow.h"
      6 #include "qthost.h"
      7 #include "settingwidgetbinder.h"
      8 
      9 #include "util/host.h"
     10 
     11 #include <QtCore/QLatin1StringView>
     12 #include <QtCore/QUtf8StringView>
     13 #include <QtGui/QIcon>
     14 #include <QtWidgets/QMenuBar>
     15 #include <QtWidgets/QScrollBar>
     16 
     17 // TODO: Since log callbacks are synchronized, no mutex is needed here.
     18 // But once I get rid of that, there will be.
     19 LogWindow* g_log_window;
     20 
     21 LogWindow::LogWindow(bool attach_to_main)
     22   : QMainWindow(), m_filter_names(Settings::GetLogFilters()), m_attached_to_main_window(attach_to_main)
     23 {
     24   restoreSize();
     25   createUi();
     26 
     27   Log::RegisterCallback(&LogWindow::logCallback, this);
     28 }
     29 
     30 LogWindow::~LogWindow() = default;
     31 
     32 void LogWindow::updateSettings()
     33 {
     34   const bool new_enabled = Host::GetBoolSettingValue("Logging", "LogToWindow", false);
     35   const bool attach_to_main = Host::GetBoolSettingValue("Logging", "AttachLogWindowToMainWindow", true);
     36   const bool curr_enabled = (g_log_window != nullptr);
     37   if (new_enabled == curr_enabled)
     38   {
     39     if (g_log_window && g_log_window->m_attached_to_main_window != attach_to_main)
     40     {
     41       g_log_window->m_attached_to_main_window = attach_to_main;
     42       if (attach_to_main)
     43         g_log_window->reattachToMainWindow();
     44     }
     45 
     46     return;
     47   }
     48 
     49   if (new_enabled)
     50   {
     51     g_log_window = new LogWindow(attach_to_main);
     52     if (attach_to_main && g_main_window && g_main_window->isVisible())
     53       g_log_window->reattachToMainWindow();
     54 
     55     g_log_window->show();
     56   }
     57   else if (g_log_window)
     58   {
     59     g_log_window->m_destroying = true;
     60     g_log_window->close();
     61     g_log_window->deleteLater();
     62     g_log_window = nullptr;
     63   }
     64 }
     65 
     66 void LogWindow::destroy()
     67 {
     68   if (!g_log_window)
     69     return;
     70 
     71   g_log_window->m_destroying = true;
     72   g_log_window->close();
     73   g_log_window->deleteLater();
     74   g_log_window = nullptr;
     75 }
     76 
     77 void LogWindow::reattachToMainWindow()
     78 {
     79   // Skip when maximized.
     80   if (g_main_window->windowState() & (Qt::WindowMaximized | Qt::WindowFullScreen))
     81     return;
     82 
     83   resize(width(), g_main_window->height());
     84 
     85   const QPoint new_pos = g_main_window->pos() + QPoint(g_main_window->width() + 10, 0);
     86   if (pos() != new_pos)
     87     move(new_pos);
     88 }
     89 
     90 void LogWindow::updateWindowTitle()
     91 {
     92   QString title;
     93 
     94   const QString& serial = QtHost::GetCurrentGameSerial();
     95 
     96   if (QtHost::IsSystemValid() && !serial.isEmpty())
     97   {
     98     const QFileInfo fi(QtHost::GetCurrentGamePath());
     99     title = tr("Log Window - %1 [%2]").arg(serial).arg(fi.fileName());
    100   }
    101   else
    102   {
    103     title = tr("Log Window");
    104   }
    105 
    106   setWindowTitle(title);
    107 }
    108 
    109 void LogWindow::createUi()
    110 {
    111   QIcon icon;
    112   icon.addFile(QString::fromUtf8(":/icons/duck.png"), QSize(), QIcon::Normal, QIcon::Off);
    113   setWindowIcon(icon);
    114   setWindowFlag(Qt::WindowCloseButtonHint, false);
    115   updateWindowTitle();
    116 
    117   QAction* action;
    118 
    119   QMenuBar* menu = new QMenuBar(this);
    120   setMenuBar(menu);
    121 
    122   QMenu* log_menu = menu->addMenu("&Log");
    123   action = log_menu->addAction(tr("&Clear"));
    124   connect(action, &QAction::triggered, this, &LogWindow::onClearTriggered);
    125   action = log_menu->addAction(tr("&Save..."));
    126   connect(action, &QAction::triggered, this, &LogWindow::onSaveTriggered);
    127 
    128   log_menu->addSeparator();
    129 
    130   action = log_menu->addAction(tr("Cl&ose"));
    131   connect(action, &QAction::triggered, this, &LogWindow::close);
    132 
    133   QMenu* settings_menu = menu->addMenu(tr("&Settings"));
    134 
    135   action = settings_menu->addAction(tr("Log To &System Console"));
    136   action->setCheckable(true);
    137   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "LogToConsole", false);
    138 
    139   action = settings_menu->addAction(tr("Log To &Debug Console"));
    140   action->setCheckable(true);
    141   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "LogToDebug", false);
    142 
    143   action = settings_menu->addAction(tr("Log To &File"));
    144   action->setCheckable(true);
    145   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "LogToFile", false);
    146 
    147   settings_menu->addSeparator();
    148 
    149   action = settings_menu->addAction(tr("Attach To &Main Window"));
    150   action->setCheckable(true);
    151   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "AttachLogWindowToMainWindow", true);
    152 
    153   action = settings_menu->addAction(tr("Show &Timestamps"));
    154   action->setCheckable(true);
    155   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "LogTimestamps", true);
    156 
    157   settings_menu->addSeparator();
    158 
    159   m_level_menu = settings_menu->addMenu(tr("&Log Level"));
    160   for (u32 i = 0; i < static_cast<u32>(LOGLEVEL_COUNT); i++)
    161   {
    162     action = m_level_menu->addAction(QString::fromUtf8(Settings::GetLogLevelDisplayName(static_cast<LOGLEVEL>(i))));
    163     action->setCheckable(true);
    164     connect(action, &QAction::triggered, this, [this, i]() { setLogLevel(static_cast<LOGLEVEL>(i)); });
    165   }
    166   updateLogLevelUi();
    167 
    168   QMenu* filters_menu = menu->addMenu(tr("&Filters"));
    169   populateFilters(filters_menu);
    170 
    171   m_text = new QPlainTextEdit(this);
    172   m_text->setReadOnly(true);
    173   m_text->setUndoRedoEnabled(false);
    174   m_text->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse);
    175   m_text->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    176 
    177 #if defined(_WIN32)
    178   QFont font("Consolas");
    179   font.setPointSize(10);
    180 #elif defined(__APPLE__)
    181   QFont font("Monaco");
    182   font.setPointSize(11);
    183 #else
    184   QFont font("Monospace");
    185   font.setStyleHint(QFont::TypeWriter);
    186 #endif
    187   m_text->setFont(font);
    188 
    189   setCentralWidget(m_text);
    190 }
    191 
    192 void LogWindow::updateLogLevelUi()
    193 {
    194   const u32 level = Settings::ParseLogLevelName(Host::GetBaseStringSettingValue("Logging", "LogLevel", "").c_str())
    195                       .value_or(Settings::DEFAULT_LOG_LEVEL);
    196 
    197   const QList<QAction*> actions = m_level_menu->actions();
    198   for (u32 i = 0; i < actions.size(); i++)
    199     actions[i]->setChecked(i == level);
    200 }
    201 
    202 void LogWindow::setLogLevel(LOGLEVEL level)
    203 {
    204   Host::SetBaseStringSettingValue("Logging", "LogLevel", Settings::GetLogLevelName(level));
    205   Host::CommitBaseSettingChanges();
    206   g_emu_thread->applySettings(false);
    207 }
    208 
    209 void LogWindow::populateFilters(QMenu* filter_menu)
    210 {
    211   const std::string filters = Host::GetBaseStringSettingValue("Logging", "LogFilter", "");
    212   for (size_t i = 0; i < m_filter_names.size(); i++)
    213   {
    214     const char* filter = m_filter_names[i];
    215     const bool is_currently_filtered = (filters.find(filter) == std::string::npos);
    216     QAction* action = filter_menu->addAction(QString::fromUtf8(filter));
    217     action->setCheckable(action);
    218     action->setChecked(is_currently_filtered);
    219     connect(action, &QAction::triggered, this, [this, i](bool checked) { setChannelFiltered(i, !checked); });
    220   }
    221 }
    222 
    223 void LogWindow::setChannelFiltered(size_t index, bool enabled)
    224 {
    225   const char* filter = m_filter_names[index];
    226   const size_t filter_len = std::strlen(filter);
    227 
    228   std::string filters = Host::GetBaseStringSettingValue("Logging", "LogFilter", "");
    229   const std::string::size_type pos = filters.find(filter);
    230 
    231   if (!enabled)
    232   {
    233     if (pos == std::string::npos)
    234       return;
    235 
    236     const size_t erase_count =
    237       filter_len + (((pos + filter_len) < filters.length() && filters[pos + filter_len] == ' ') ? 1 : 0);
    238     filters.erase(pos, erase_count);
    239   }
    240   else
    241   {
    242     if (pos != std::string::npos)
    243       return;
    244 
    245     if (!filters.empty() && filters.back() != ' ')
    246       filters.push_back(' ');
    247     filters.append(filter);
    248   }
    249 
    250   Host::SetBaseStringSettingValue("Logging", "LogFilter", filters.c_str());
    251   Host::CommitBaseSettingChanges();
    252   g_emu_thread->applySettings(false);
    253 }
    254 
    255 void LogWindow::onClearTriggered()
    256 {
    257   m_text->clear();
    258 }
    259 
    260 void LogWindow::onSaveTriggered()
    261 {
    262   const QString path = QFileDialog::getSaveFileName(this, tr("Select Log File"), QString(), tr("Log Files (*.txt)"));
    263   if (path.isEmpty())
    264     return;
    265 
    266   QFile file(path);
    267   if (!file.open(QFile::WriteOnly | QFile::Text))
    268   {
    269     QMessageBox::critical(this, tr("Error"), tr("Failed to open file for writing."));
    270     return;
    271   }
    272 
    273   file.write(m_text->toPlainText().toUtf8());
    274   file.close();
    275 
    276   appendMessage(QLatin1StringView("LogWindow"), LOGLEVEL_INFO, tr("Log was written to %1.\n").arg(path));
    277 }
    278 
    279 void LogWindow::logCallback(void* pUserParam, const char* channelName, const char* functionName, LOGLEVEL level,
    280                             std::string_view message)
    281 {
    282   LogWindow* this_ptr = static_cast<LogWindow*>(pUserParam);
    283 
    284   // TODO: Split message based on lines.
    285   // I don't like the memory allocations here either...
    286 
    287   QString qmessage;
    288   qmessage.reserve(message.length() + 1);
    289   qmessage.append(QUtf8StringView(message.data(), message.length()));
    290   qmessage.append(QChar('\n'));
    291 
    292   const QLatin1StringView qchannel((level <= LOGLEVEL_WARNING) ? functionName : channelName);
    293 
    294   if (g_emu_thread->isOnUIThread())
    295   {
    296     this_ptr->appendMessage(qchannel, level, qmessage);
    297   }
    298   else
    299   {
    300     QMetaObject::invokeMethod(this_ptr, "appendMessage", Qt::QueuedConnection,
    301                               Q_ARG(const QLatin1StringView&, qchannel), Q_ARG(quint32, static_cast<u32>(level)),
    302                               Q_ARG(const QString&, qmessage));
    303   }
    304 }
    305 
    306 void LogWindow::closeEvent(QCloseEvent* event)
    307 {
    308   if (!m_destroying)
    309   {
    310     event->ignore();
    311     return;
    312   }
    313 
    314   Log::UnregisterCallback(&LogWindow::logCallback, this);
    315 
    316   saveSize();
    317 
    318   QMainWindow::closeEvent(event);
    319 }
    320 
    321 void LogWindow::appendMessage(const QLatin1StringView& channel, quint32 level, const QString& message)
    322 {
    323   QTextCursor temp_cursor = m_text->textCursor();
    324   QScrollBar* scrollbar = m_text->verticalScrollBar();
    325   const bool cursor_at_end = temp_cursor.atEnd();
    326   const bool scroll_at_end = scrollbar->sliderPosition() == scrollbar->maximum();
    327 
    328   temp_cursor.movePosition(QTextCursor::End);
    329 
    330   {
    331     static constexpr const QChar level_characters[LOGLEVEL_COUNT] = {'X', 'E', 'W', 'I', 'V', 'D', 'B', 'T'};
    332     static constexpr const QColor level_colors[LOGLEVEL_COUNT] = {
    333       QColor(255, 255, 255),    // NONE
    334       QColor(0xE7, 0x48, 0x56), // ERROR, Red Intensity
    335       QColor(0xF9, 0xF1, 0xA5), // WARNING, Yellow Intensity
    336       QColor(0xF2, 0xF2, 0xF2), // INFO, White Intensity
    337       QColor(0x16, 0xC6, 0x0C), // VERBOSE, Green Intensity
    338       QColor(0xCC, 0xCC, 0xCC), // DEV, White
    339       QColor(0x13, 0xA1, 0x0E), // DEBUG, Green
    340       QColor(0x00, 0x37, 0xDA), // TRACE, Blue
    341     };
    342     static constexpr const QColor timestamp_color = QColor(0xcc, 0xcc, 0xcc);
    343     static constexpr const QColor channel_color = QColor(0xf2, 0xf2, 0xf2);
    344 
    345     QTextCharFormat format = temp_cursor.charFormat();
    346 
    347     if (g_settings.log_timestamps)
    348     {
    349       const float message_time = Log::GetCurrentMessageTime();
    350       const QString qtimestamp = QStringLiteral("[%1] ").arg(message_time, 10, 'f', 4);
    351       format.setForeground(QBrush(timestamp_color));
    352       temp_cursor.setCharFormat(format);
    353       temp_cursor.insertText(qtimestamp);
    354     }
    355 
    356     const QString qchannel = (level <= LOGLEVEL_WARNING) ?
    357                                QStringLiteral("%1(%2): ").arg(level_characters[level]).arg(channel) :
    358                                QStringLiteral("%1/%2: ").arg(level_characters[level]).arg(channel);
    359     format.setForeground(QBrush(channel_color));
    360     temp_cursor.setCharFormat(format);
    361     temp_cursor.insertText(qchannel);
    362 
    363     // message has \n already
    364     format.setForeground(QBrush(level_colors[level]));
    365     temp_cursor.setCharFormat(format);
    366     temp_cursor.insertText(message);
    367   }
    368 
    369   if (cursor_at_end)
    370   {
    371     if (scroll_at_end)
    372     {
    373       m_text->setTextCursor(temp_cursor);
    374       scrollbar->setSliderPosition(scrollbar->maximum());
    375     }
    376     else
    377     {
    378       // Can't let changing the cursor affect the scroll bar...
    379       const int pos = scrollbar->sliderPosition();
    380       m_text->setTextCursor(temp_cursor);
    381       scrollbar->setSliderPosition(pos);
    382     }
    383   }
    384 }
    385 
    386 void LogWindow::saveSize()
    387 {
    388   const int current_width = Host::GetBaseIntSettingValue("UI", "LogWindowWidth", DEFAULT_WIDTH);
    389   const int current_height = Host::GetBaseIntSettingValue("UI", "LogWindowHeight", DEFAULT_HEIGHT);
    390   const QSize wsize = size();
    391 
    392   bool changed = false;
    393   if (current_width != wsize.width())
    394   {
    395     Host::SetBaseIntSettingValue("UI", "LogWindowWidth", wsize.width());
    396     changed = true;
    397   }
    398   if (current_height != wsize.height())
    399   {
    400     Host::SetBaseIntSettingValue("UI", "LogWindowHeight", wsize.height());
    401     changed = true;
    402   }
    403 
    404   if (changed)
    405     Host::CommitBaseSettingChanges();
    406 }
    407 
    408 void LogWindow::restoreSize()
    409 {
    410   const int width = Host::GetBaseIntSettingValue("UI", "LogWindowWidth", DEFAULT_WIDTH);
    411   const int height = Host::GetBaseIntSettingValue("UI", "LogWindowHeight", DEFAULT_HEIGHT);
    412   resize(width, height);
    413 }