mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
feat: Add crash recovery on Windows (#5012)
This commit is contained in:
parent
2cb965d352
commit
25add89b14
|
@ -29,6 +29,7 @@ Checks: "-*,
|
|||
-readability-function-cognitive-complexity,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cert-err58-cpp,
|
||||
-modernize-avoid-c-arrays
|
||||
"
|
||||
CheckOptions:
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
|
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -230,9 +230,9 @@ jobs:
|
|||
run: |
|
||||
cd build
|
||||
set cl=/MP
|
||||
nmake /S /NOLOGO crashpad_handler
|
||||
nmake /S /NOLOGO chatterino-crash-handler
|
||||
mkdir Chatterino2/crashpad
|
||||
cp bin/crashpad/crashpad_handler.exe Chatterino2/crashpad/crashpad_handler.exe
|
||||
cp bin/crashpad/crashpad-handler.exe Chatterino2/crashpad/crashpad-handler.exe
|
||||
7z a bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z bin/chatterino.pdb
|
||||
|
||||
- name: Prepare build dir (windows)
|
||||
|
|
2
.github/workflows/clang-tidy.yml
vendored
2
.github/workflows/clang-tidy.yml
vendored
|
@ -124,7 +124,7 @@ jobs:
|
|||
build_dir: build-clang-tidy
|
||||
config_file: ".clang-tidy"
|
||||
split_workflow: true
|
||||
exclude: "lib/*"
|
||||
exclude: "lib/*,tools/crash-handler/*"
|
||||
cmake_command: >-
|
||||
cmake -S. -Bbuild-clang-tidy
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -38,6 +38,6 @@
|
|||
[submodule "lib/lua/src"]
|
||||
path = lib/lua/src
|
||||
url = https://github.com/lua/lua
|
||||
[submodule "lib/crashpad"]
|
||||
path = lib/crashpad
|
||||
url = https://github.com/getsentry/crashpad
|
||||
[submodule "tools/crash-handler"]
|
||||
path = tools/crash-handler
|
||||
url = https://github.com/Chatterino/crash-handler
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
|
||||
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
|
||||
- Minor: Add a new completion API for experimental plugins feature. (#5000)
|
||||
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)
|
||||
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
|
||||
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
|
||||
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
|
||||
|
|
|
@ -210,7 +210,7 @@ if (CHATTERINO_PLUGINS)
|
|||
endif()
|
||||
|
||||
if (BUILD_WITH_CRASHPAD)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler")
|
||||
endif()
|
||||
|
||||
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 89991e9910bc4c0893e45c8cfad0bdd31cc25a5c
|
|
@ -44,6 +44,11 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
CrashHandler *getCrashHandler() override
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CommandController *getCommands() override
|
||||
{
|
||||
return nullptr;
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
#include "singletons/helper/LoggingChannel.hpp"
|
||||
|
@ -113,6 +114,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
, toasts(&this->emplace<Toasts>())
|
||||
, imageUploader(&this->emplace<ImageUploader>())
|
||||
, seventvAPI(&this->emplace<SeventvAPI>())
|
||||
, crashHandler(&this->emplace<CrashHandler>())
|
||||
|
||||
, commands(&this->emplace<CommandController>())
|
||||
, notifications(&this->emplace<NotificationController>())
|
||||
|
@ -174,7 +176,9 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
singleton->initialize(settings, paths);
|
||||
}
|
||||
|
||||
// add crash message
|
||||
// Show crash message.
|
||||
// On Windows, the crash message was already shown.
|
||||
#ifndef Q_OS_WIN
|
||||
if (!getArgs().isFramelessEmbed && getArgs().crashRecovery)
|
||||
{
|
||||
if (auto selected =
|
||||
|
@ -195,6 +199,7 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->windows->updateWordTypeMask();
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ class FfzBadges;
|
|||
class SeventvBadges;
|
||||
class ImageUploader;
|
||||
class SeventvAPI;
|
||||
class CrashHandler;
|
||||
|
||||
class IApplication
|
||||
{
|
||||
|
@ -60,6 +61,7 @@ public:
|
|||
virtual HotkeyController *getHotkeys() = 0;
|
||||
virtual WindowManager *getWindows() = 0;
|
||||
virtual Toasts *getToasts() = 0;
|
||||
virtual CrashHandler *getCrashHandler() = 0;
|
||||
virtual CommandController *getCommands() = 0;
|
||||
virtual HighlightController *getHighlights() = 0;
|
||||
virtual NotificationController *getNotifications() = 0;
|
||||
|
@ -102,6 +104,7 @@ public:
|
|||
Toasts *const toasts{};
|
||||
ImageUploader *const imageUploader{};
|
||||
SeventvAPI *const seventvAPI{};
|
||||
CrashHandler *const crashHandler{};
|
||||
|
||||
CommandController *const commands{};
|
||||
NotificationController *const notifications{};
|
||||
|
@ -148,6 +151,10 @@ public:
|
|||
{
|
||||
return this->toasts;
|
||||
}
|
||||
CrashHandler *getCrashHandler() override
|
||||
{
|
||||
return this->crashHandler;
|
||||
}
|
||||
CommandController *getCommands() override
|
||||
{
|
||||
return this->commands;
|
||||
|
|
|
@ -289,8 +289,6 @@ set(SOURCE_FILES
|
|||
messages/search/SubtierPredicate.cpp
|
||||
messages/search/SubtierPredicate.hpp
|
||||
|
||||
providers/Crashpad.cpp
|
||||
providers/Crashpad.hpp
|
||||
providers/IvrApi.cpp
|
||||
providers/IvrApi.hpp
|
||||
providers/LinkResolver.cpp
|
||||
|
@ -425,6 +423,8 @@ set(SOURCE_FILES
|
|||
|
||||
singletons/Badges.cpp
|
||||
singletons/Badges.hpp
|
||||
singletons/CrashHandler.cpp
|
||||
singletons/CrashHandler.hpp
|
||||
singletons/Emotes.cpp
|
||||
singletons/Emotes.hpp
|
||||
singletons/Fonts.cpp
|
||||
|
@ -1007,7 +1007,6 @@ endif ()
|
|||
if (BUILD_WITH_CRASHPAD)
|
||||
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD)
|
||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client)
|
||||
set_target_directory_hierarchy(crashpad_handler crashpad)
|
||||
endif()
|
||||
|
||||
# Configure compiler warnings
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "common/Modes.hpp"
|
||||
#include "common/NetworkManager.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
@ -99,21 +100,10 @@ namespace {
|
|||
|
||||
void showLastCrashDialog()
|
||||
{
|
||||
//#ifndef C_DISABLE_CRASH_DIALOG
|
||||
// LastRunCrashDialog dialog;
|
||||
|
||||
// switch (dialog.exec())
|
||||
// {
|
||||
// case QDialog::Accepted:
|
||||
// {
|
||||
// };
|
||||
// break;
|
||||
// default:
|
||||
// {
|
||||
// _exit(0);
|
||||
// }
|
||||
// }
|
||||
//#endif
|
||||
auto *dialog = new LastRunCrashDialog;
|
||||
// Use exec() over open() to block the app from being loaded
|
||||
// and to be able to set the safe mode.
|
||||
dialog->exec();
|
||||
}
|
||||
|
||||
void createRunningFile(const QString &path)
|
||||
|
@ -131,14 +121,13 @@ namespace {
|
|||
}
|
||||
|
||||
std::chrono::steady_clock::time_point signalsInitTime;
|
||||
bool restartOnSignal = false;
|
||||
|
||||
[[noreturn]] void handleSignal(int signum)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
if (restartOnSignal &&
|
||||
std::chrono::steady_clock::now() - signalsInitTime > 30s)
|
||||
if (std::chrono::steady_clock::now() - signalsInitTime > 30s &&
|
||||
getIApp()->getCrashHandler()->shouldRecover())
|
||||
{
|
||||
QProcess proc;
|
||||
|
||||
|
@ -240,9 +229,12 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
|
|||
initResources();
|
||||
initSignalHandler();
|
||||
|
||||
settings.restartOnCrash.connect([](const bool &value) {
|
||||
restartOnSignal = value;
|
||||
});
|
||||
#ifdef Q_OS_WIN
|
||||
if (getArgs().crashRecovery)
|
||||
{
|
||||
showLastCrashDialog();
|
||||
}
|
||||
#endif
|
||||
|
||||
auto thread = std::thread([dir = paths.miscDirectory] {
|
||||
{
|
||||
|
@ -279,30 +271,11 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
|
|||
chatterino::NetworkManager::init();
|
||||
chatterino::Updates::instance().checkForUpdates();
|
||||
|
||||
#ifdef C_USE_BREAKPAD
|
||||
QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes");
|
||||
#endif
|
||||
|
||||
// Running file
|
||||
auto runningPath =
|
||||
paths.miscDirectory + "/running_" + paths.applicationFilePathHash;
|
||||
|
||||
if (QFile::exists(runningPath))
|
||||
{
|
||||
showLastCrashDialog();
|
||||
}
|
||||
else
|
||||
{
|
||||
createRunningFile(runningPath);
|
||||
}
|
||||
|
||||
Application app(settings, paths);
|
||||
app.initialize(settings, paths);
|
||||
app.run(a);
|
||||
app.save();
|
||||
|
||||
removeRunningFile(runningPath);
|
||||
|
||||
if (!getArgs().dontSaveSettings)
|
||||
{
|
||||
pajlada::Settings::SettingManager::gSave();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "Args.hpp"
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/AttachToConsole.hpp"
|
||||
|
@ -14,6 +15,55 @@
|
|||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
namespace {
|
||||
|
||||
template <class... Args>
|
||||
QCommandLineOption hiddenOption(Args... args)
|
||||
{
|
||||
QCommandLineOption opt(args...);
|
||||
opt.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
return opt;
|
||||
}
|
||||
|
||||
QStringList extractCommandLine(
|
||||
const QCommandLineParser &parser,
|
||||
std::initializer_list<QCommandLineOption> options)
|
||||
{
|
||||
QStringList args;
|
||||
for (const auto &option : options)
|
||||
{
|
||||
if (parser.isSet(option))
|
||||
{
|
||||
auto optionName = option.names().first();
|
||||
if (optionName.length() == 1)
|
||||
{
|
||||
optionName.prepend(u'-');
|
||||
}
|
||||
else
|
||||
{
|
||||
optionName.prepend("--");
|
||||
}
|
||||
|
||||
auto values = parser.values(option);
|
||||
if (values.empty())
|
||||
{
|
||||
args += optionName;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto &value : values)
|
||||
{
|
||||
args += optionName;
|
||||
args += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
Args::Args(const QApplication &app)
|
||||
|
@ -23,39 +73,44 @@ Args::Args(const QApplication &app)
|
|||
parser.addHelpOption();
|
||||
|
||||
// Used internally by app to restart after unexpected crashes
|
||||
QCommandLineOption crashRecoveryOption("crash-recovery");
|
||||
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
auto crashRecoveryOption = hiddenOption("crash-recovery");
|
||||
auto exceptionCodeOption = hiddenOption("cr-exception-code", "", "code");
|
||||
auto exceptionMessageOption =
|
||||
hiddenOption("cr-exception-message", "", "message");
|
||||
|
||||
// Added to ignore the parent-window option passed during native messaging
|
||||
QCommandLineOption parentWindowOption("parent-window");
|
||||
parentWindowOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
QCommandLineOption parentWindowIdOption("x-attach-split-to-window", "",
|
||||
"window-id");
|
||||
parentWindowIdOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
auto parentWindowOption = hiddenOption("parent-window");
|
||||
auto parentWindowIdOption =
|
||||
hiddenOption("x-attach-split-to-window", "", "window-id");
|
||||
|
||||
// Verbose
|
||||
QCommandLineOption verboseOption({{"v", "verbose"},
|
||||
"Attaches to the Console on windows, "
|
||||
"allowing you to see debug output."});
|
||||
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
auto verboseOption = QCommandLineOption(
|
||||
QStringList{"v", "verbose"}, "Attaches to the Console on windows, "
|
||||
"allowing you to see debug output.");
|
||||
// Safe mode
|
||||
QCommandLineOption safeModeOption(
|
||||
"safe-mode", "Starts Chatterino without loading Plugins and always "
|
||||
"show the settings button.");
|
||||
|
||||
parser.addOptions({
|
||||
{{"V", "version"}, "Displays version information."},
|
||||
crashRecoveryOption,
|
||||
parentWindowOption,
|
||||
parentWindowIdOption,
|
||||
verboseOption,
|
||||
safeModeOption,
|
||||
});
|
||||
parser.addOption(QCommandLineOption(
|
||||
// Channel layout
|
||||
auto channelLayout = QCommandLineOption(
|
||||
{"c", "channels"},
|
||||
"Joins only supplied channels on startup. Use letters with colons to "
|
||||
"specify platform. Only Twitch channels are supported at the moment.\n"
|
||||
"If platform isn't specified, default is Twitch.",
|
||||
"t:channel1;t:channel2;..."));
|
||||
"t:channel1;t:channel2;...");
|
||||
|
||||
parser.addOptions({
|
||||
{{"V", "version"}, "Displays version information."},
|
||||
crashRecoveryOption,
|
||||
exceptionCodeOption,
|
||||
exceptionMessageOption,
|
||||
parentWindowOption,
|
||||
parentWindowIdOption,
|
||||
verboseOption,
|
||||
safeModeOption,
|
||||
channelLayout,
|
||||
});
|
||||
|
||||
if (!parser.parse(app.arguments()))
|
||||
{
|
||||
|
@ -75,15 +130,25 @@ Args::Args(const QApplication &app)
|
|||
(args.size() > 0 && (args[0].startsWith("chrome-extension://") ||
|
||||
args[0].endsWith(".json")));
|
||||
|
||||
if (parser.isSet("c"))
|
||||
if (parser.isSet(channelLayout))
|
||||
{
|
||||
this->applyCustomChannelLayout(parser.value("c"));
|
||||
this->applyCustomChannelLayout(parser.value(channelLayout));
|
||||
}
|
||||
|
||||
this->verbose = parser.isSet(verboseOption);
|
||||
|
||||
this->printVersion = parser.isSet("V");
|
||||
this->crashRecovery = parser.isSet("crash-recovery");
|
||||
|
||||
this->crashRecovery = parser.isSet(crashRecoveryOption);
|
||||
if (parser.isSet(exceptionCodeOption))
|
||||
{
|
||||
this->exceptionCode =
|
||||
static_cast<uint32_t>(parser.value(exceptionCodeOption).toULong());
|
||||
}
|
||||
if (parser.isSet(exceptionMessageOption))
|
||||
{
|
||||
this->exceptionMessage = parser.value(exceptionMessageOption);
|
||||
}
|
||||
|
||||
if (parser.isSet(parentWindowIdOption))
|
||||
{
|
||||
|
@ -97,6 +162,17 @@ Args::Args(const QApplication &app)
|
|||
{
|
||||
this->safeMode = true;
|
||||
}
|
||||
|
||||
this->currentArguments_ = extractCommandLine(parser, {
|
||||
verboseOption,
|
||||
safeModeOption,
|
||||
channelLayout,
|
||||
});
|
||||
}
|
||||
|
||||
QStringList Args::currentArguments() const
|
||||
{
|
||||
return this->currentArguments_;
|
||||
}
|
||||
|
||||
void Args::applyCustomChannelLayout(const QString &argValue)
|
||||
|
|
|
@ -9,13 +9,37 @@
|
|||
namespace chatterino {
|
||||
|
||||
/// Command line arguments passed to Chatterino.
|
||||
///
|
||||
/// All accepted arguments:
|
||||
///
|
||||
/// Crash recovery:
|
||||
/// --crash-recovery
|
||||
/// --cr-exception-code code
|
||||
/// --cr-exception-message message
|
||||
///
|
||||
/// Native messaging:
|
||||
/// --parent-window
|
||||
/// --x-attach-split-to-window=window-id
|
||||
///
|
||||
/// -v, --verbose
|
||||
/// -V, --version
|
||||
/// -c, --channels=t:channel1;t:channel2;...
|
||||
/// --safe-mode
|
||||
///
|
||||
/// See documentation on `QGuiApplication` for documentation on Qt arguments like -platform.
|
||||
class Args
|
||||
{
|
||||
public:
|
||||
Args(const QApplication &app);
|
||||
|
||||
bool printVersion{};
|
||||
|
||||
bool crashRecovery{};
|
||||
/// Native, platform-specific exception code from crashpad
|
||||
std::optional<uint32_t> exceptionCode{};
|
||||
/// Text version of the exception code. Potentially contains more context.
|
||||
std::optional<QString> exceptionMessage{};
|
||||
|
||||
bool shouldRunBrowserExtensionHost{};
|
||||
// Shows a single chat. Used on windows to embed in another application.
|
||||
bool isFramelessEmbed{};
|
||||
|
@ -28,8 +52,12 @@ public:
|
|||
bool verbose{};
|
||||
bool safeMode{};
|
||||
|
||||
QStringList currentArguments() const;
|
||||
|
||||
private:
|
||||
void applyCustomChannelLayout(const QString &argValue);
|
||||
|
||||
QStringList currentArguments_;
|
||||
};
|
||||
|
||||
void initArgs(const QApplication &app);
|
||||
|
|
|
@ -97,6 +97,11 @@ public:
|
|||
return !this->hasAny(flags);
|
||||
}
|
||||
|
||||
T value() const
|
||||
{
|
||||
return this->value_;
|
||||
}
|
||||
|
||||
private:
|
||||
T value_{};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,8 @@ Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold);
|
|||
Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoCrashhandler, "chatterino.crashhandler",
|
||||
logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoEmoji, "chatterino.emoji", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold);
|
||||
|
|
|
@ -8,6 +8,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoBenchmark);
|
|||
Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCache);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes);
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
#include "common/Modes.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "common/Version.hpp"
|
||||
#include "providers/Crashpad.hpp"
|
||||
#include "providers/IvrApi.hpp"
|
||||
#include "providers/NetworkConfigurationProvider.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "RunGui.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/AttachToConsole.hpp"
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include "providers/Crashpad.hpp"
|
||||
|
||||
# include "common/QLogging.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
|
||||
# include <QApplication>
|
||||
# include <QDir>
|
||||
# include <QString>
|
||||
|
||||
# include <memory>
|
||||
# include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
/// The name of the crashpad handler executable.
|
||||
/// This varies across platforms
|
||||
# if defined(Q_OS_UNIX)
|
||||
const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad_handler");
|
||||
# elif defined(Q_OS_WINDOWS)
|
||||
const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad_handler.exe");
|
||||
# else
|
||||
# error Unsupported platform
|
||||
# endif
|
||||
|
||||
/// Converts a QString into the platform string representation.
|
||||
# if defined(Q_OS_UNIX)
|
||||
std::string nativeString(const QString &s)
|
||||
{
|
||||
return s.toStdString();
|
||||
}
|
||||
# elif defined(Q_OS_WINDOWS)
|
||||
std::wstring nativeString(const QString &s)
|
||||
{
|
||||
return s.toStdWString();
|
||||
}
|
||||
# else
|
||||
# error Unsupported platform
|
||||
# endif
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler()
|
||||
{
|
||||
// Currently, the following directory layout is assumed:
|
||||
// [applicationDirPath]
|
||||
// │
|
||||
// ├─chatterino
|
||||
// │
|
||||
// ╰─[crashpad]
|
||||
// │
|
||||
// ╰─crashpad_handler
|
||||
// TODO: The location of the binary might vary across platforms
|
||||
auto crashpadBinDir = QDir(QApplication::applicationDirPath());
|
||||
|
||||
if (!crashpadBinDir.cd("crashpad"))
|
||||
{
|
||||
qCDebug(chatterinoApp) << "Cannot find crashpad directory";
|
||||
return nullptr;
|
||||
}
|
||||
if (!crashpadBinDir.exists(CRASHPAD_EXECUTABLE_NAME))
|
||||
{
|
||||
qCDebug(chatterinoApp) << "Cannot find crashpad handler executable";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto handlerPath = base::FilePath(nativeString(
|
||||
crashpadBinDir.absoluteFilePath(CRASHPAD_EXECUTABLE_NAME)));
|
||||
|
||||
// Argument passed in --database
|
||||
// > Crash reports are written to this database, and if uploads are enabled,
|
||||
// uploaded from this database to a crash report collection server.
|
||||
const auto databaseDir =
|
||||
base::FilePath(nativeString(getPaths()->crashdumpDirectory));
|
||||
|
||||
auto client = std::make_unique<crashpad::CrashpadClient>();
|
||||
|
||||
// See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md
|
||||
// for documentation on available options.
|
||||
if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, {},
|
||||
true, false))
|
||||
{
|
||||
qCDebug(chatterinoApp) << "Failed to start crashpad handler";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
qCDebug(chatterinoApp) << "Started crashpad handler";
|
||||
return client;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
|
@ -1,14 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include <client/crashpad_client.h>
|
||||
|
||||
# include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler();
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
230
src/singletons/CrashHandler.cpp
Normal file
230
src/singletons/CrashHandler.cpp
Normal file
|
@ -0,0 +1,230 @@
|
|||
#include "singletons/CrashHandler.hpp"
|
||||
|
||||
#include "common/Args.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QString>
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include <QApplication>
|
||||
|
||||
# include <memory>
|
||||
# include <string>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
using namespace literals;
|
||||
|
||||
/// The name of the crashpad handler executable.
|
||||
/// This varies across platforms
|
||||
#if defined(Q_OS_UNIX)
|
||||
const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad-handler");
|
||||
#elif defined(Q_OS_WINDOWS)
|
||||
const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad-handler.exe");
|
||||
#else
|
||||
# error Unsupported platform
|
||||
#endif
|
||||
|
||||
/// Converts a QString into the platform string representation.
|
||||
#if defined(Q_OS_UNIX)
|
||||
std::string nativeString(const QString &s)
|
||||
{
|
||||
return s.toStdString();
|
||||
}
|
||||
#elif defined(Q_OS_WINDOWS)
|
||||
std::wstring nativeString(const QString &s)
|
||||
{
|
||||
return s.toStdWString();
|
||||
}
|
||||
#else
|
||||
# error Unsupported platform
|
||||
#endif
|
||||
|
||||
const QString RECOVERY_FILE = u"chatterino-recovery.json"_s;
|
||||
|
||||
/// The recovery options are saved outside the settings
|
||||
/// to be able to read them without loading the settings.
|
||||
///
|
||||
/// The flags are saved in the `RECOVERY_FILE` as JSON.
|
||||
std::optional<bool> readRecoverySettings(const Paths &paths)
|
||||
{
|
||||
QFile file(QDir(paths.crashdumpDirectory).filePath(RECOVERY_FILE));
|
||||
if (!file.open(QFile::ReadOnly))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError error{};
|
||||
auto doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qCWarning(chatterinoCrashhandler)
|
||||
<< "Failed to parse recovery settings" << error.errorString();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto obj = doc.object();
|
||||
auto shouldRecover = obj["shouldRecover"_L1];
|
||||
if (!shouldRecover.isBool())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return shouldRecover.toBool();
|
||||
}
|
||||
|
||||
bool canRestart(const Paths &paths)
|
||||
{
|
||||
#ifdef NDEBUG
|
||||
const auto &args = chatterino::getArgs();
|
||||
if (args.isFramelessEmbed || args.shouldRunBrowserExtensionHost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto settings = readRecoverySettings(paths);
|
||||
if (!settings)
|
||||
{
|
||||
return false; // default, no settings found
|
||||
}
|
||||
return *settings;
|
||||
#else
|
||||
(void)paths;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// This encodes the arguments into a single string.
|
||||
///
|
||||
/// The command line arguments are joined by '+'. A plus is escaped by an
|
||||
/// additional plus ('++' -> '+').
|
||||
///
|
||||
/// The decoding happens in crash-handler/src/CommandLine.cpp
|
||||
std::string encodeArguments()
|
||||
{
|
||||
std::string args;
|
||||
for (auto arg : getArgs().currentArguments())
|
||||
{
|
||||
if (!args.empty())
|
||||
{
|
||||
args.push_back('+');
|
||||
}
|
||||
args += arg.replace(u'+', u"++"_s).toStdString();
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
void CrashHandler::initialize(Settings & /*settings*/, Paths &paths)
|
||||
{
|
||||
auto optSettings = readRecoverySettings(paths);
|
||||
if (optSettings)
|
||||
{
|
||||
this->shouldRecover_ = *optSettings;
|
||||
}
|
||||
else
|
||||
{
|
||||
// By default, we don't restart after a crash.
|
||||
this->saveShouldRecover(false);
|
||||
}
|
||||
}
|
||||
|
||||
void CrashHandler::saveShouldRecover(bool value)
|
||||
{
|
||||
this->shouldRecover_ = value;
|
||||
|
||||
QFile file(QDir(getPaths()->crashdumpDirectory).filePath(RECOVERY_FILE));
|
||||
if (!file.open(QFile::WriteOnly | QFile::Truncate))
|
||||
{
|
||||
qCWarning(chatterinoCrashhandler)
|
||||
<< "Failed to open" << file.fileName();
|
||||
return;
|
||||
}
|
||||
file.write(QJsonDocument(QJsonObject{
|
||||
{"shouldRecover"_L1, value},
|
||||
})
|
||||
.toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler()
|
||||
{
|
||||
// Currently, the following directory layout is assumed:
|
||||
// [applicationDirPath]
|
||||
// ├─chatterino(.exe)
|
||||
// ╰─[crashpad]
|
||||
// ╰─crashpad-handler(.exe)
|
||||
// TODO: The location of the binary might vary across platforms
|
||||
auto crashpadBinDir = QDir(QApplication::applicationDirPath());
|
||||
|
||||
if (!crashpadBinDir.cd("crashpad"))
|
||||
{
|
||||
qCDebug(chatterinoCrashhandler) << "Cannot find crashpad directory";
|
||||
return nullptr;
|
||||
}
|
||||
if (!crashpadBinDir.exists(CRASHPAD_EXECUTABLE_NAME))
|
||||
{
|
||||
qCDebug(chatterinoCrashhandler)
|
||||
<< "Cannot find crashpad handler executable";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto handlerPath = base::FilePath(nativeString(
|
||||
crashpadBinDir.absoluteFilePath(CRASHPAD_EXECUTABLE_NAME)));
|
||||
|
||||
// Argument passed in --database
|
||||
// > Crash reports are written to this database, and if uploads are enabled,
|
||||
// uploaded from this database to a crash report collection server.
|
||||
auto databaseDir =
|
||||
base::FilePath(nativeString(getPaths()->crashdumpDirectory));
|
||||
|
||||
auto client = std::make_unique<crashpad::CrashpadClient>();
|
||||
|
||||
std::map<std::string, std::string> annotations{
|
||||
{
|
||||
"canRestart"s,
|
||||
canRestart(*getPaths()) ? "true"s : "false"s,
|
||||
},
|
||||
{
|
||||
"exePath"s,
|
||||
QApplication::applicationFilePath().toStdString(),
|
||||
},
|
||||
{
|
||||
"startedAt"s,
|
||||
QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toStdString(),
|
||||
},
|
||||
{
|
||||
"exeArguments"s,
|
||||
encodeArguments(),
|
||||
},
|
||||
};
|
||||
|
||||
// See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md
|
||||
// for documentation on available options.
|
||||
if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, annotations,
|
||||
{}, true, false))
|
||||
{
|
||||
qCDebug(chatterinoCrashhandler) << "Failed to start crashpad handler";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
qCDebug(chatterinoCrashhandler) << "Started crashpad handler";
|
||||
return client;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace chatterino
|
36
src/singletons/CrashHandler.hpp
Normal file
36
src/singletons/CrashHandler.hpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Singleton.hpp"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include <client/crashpad_client.h>
|
||||
|
||||
# include <memory>
|
||||
#endif
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class CrashHandler : public Singleton
|
||||
{
|
||||
public:
|
||||
bool shouldRecover() const
|
||||
{
|
||||
return this->shouldRecover_;
|
||||
}
|
||||
|
||||
/// Sets and saves whether Chatterino should restart on a crash
|
||||
void saveShouldRecover(bool value);
|
||||
|
||||
void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
private:
|
||||
bool shouldRecover_ = false;
|
||||
};
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler();
|
||||
#endif
|
||||
|
||||
} // namespace chatterino
|
|
@ -510,7 +510,6 @@ public:
|
|||
ThumbnailPreviewMode::AlwaysShow,
|
||||
};
|
||||
QStringSetting cachePath = {"/cache/path", ""};
|
||||
BoolSetting restartOnCrash = {"/misc/restartOnCrash", false};
|
||||
BoolSetting attachExtensionToAnyProcess = {
|
||||
"/misc/attachExtensionToAnyProcess", false};
|
||||
BoolSetting askOnImageUpload = {"/misc/askOnImageUpload", true};
|
||||
|
|
|
@ -1,93 +1,109 @@
|
|||
#include "LastRunCrashDialog.hpp"
|
||||
#include "widgets/dialogs/LastRunCrashDialog.hpp"
|
||||
|
||||
#include "singletons/Updates.hpp"
|
||||
#include "common/Args.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/Modes.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QRandomGenerator>
|
||||
#include <QStringBuilder>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino::literals;
|
||||
|
||||
const std::initializer_list<QString> MESSAGES = {
|
||||
u"Oops..."_s, u"NotLikeThis"_s,
|
||||
u"NOOOOOO"_s, u"I'm sorry"_s,
|
||||
u"We're sorry"_s, u"My bad"_s,
|
||||
u"FailFish"_s, u"O_o"_s,
|
||||
u"Sorry :("_s, u"I blame cosmic rays"_s,
|
||||
u"I blame TMI"_s, u"I blame Helix"_s,
|
||||
u"Oopsie woopsie"_s,
|
||||
};
|
||||
|
||||
QString randomMessage()
|
||||
{
|
||||
return *(MESSAGES.begin() +
|
||||
(QRandomGenerator::global()->generate64() % MESSAGES.size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace literals;
|
||||
|
||||
LastRunCrashDialog::LastRunCrashDialog()
|
||||
{
|
||||
this->setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
this->setWindowTitle("Chatterino");
|
||||
this->setWindowTitle(u"Chatterino - " % randomMessage());
|
||||
|
||||
auto layout =
|
||||
LayoutCreator<LastRunCrashDialog>(this).setLayoutType<QVBoxLayout>();
|
||||
|
||||
layout.emplace<QLabel>("The application wasn't terminated properly the "
|
||||
"last time it was executed.");
|
||||
QString text =
|
||||
u"Chatterino unexpectedly crashed and restarted. "_s
|
||||
"<i>You can disable automatic restarts in the settings.</i><br><br>";
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
auto reportsDir =
|
||||
QDir(getPaths()->crashdumpDirectory).filePath(u"reports"_s);
|
||||
text += u"A <b>crash report</b> has been saved to "
|
||||
"<a href=\"file:///" %
|
||||
reportsDir % u"\">" % reportsDir % u"</a>.<br>";
|
||||
|
||||
if (getArgs().exceptionCode)
|
||||
{
|
||||
text += u"The last run crashed with code <code>0x" %
|
||||
QString::number(*getArgs().exceptionCode, 16) % u"</code>";
|
||||
|
||||
if (getArgs().exceptionMessage)
|
||||
{
|
||||
text += u" (" % *getArgs().exceptionMessage % u")";
|
||||
}
|
||||
|
||||
text += u".<br>"_s;
|
||||
}
|
||||
|
||||
text +=
|
||||
"Crash reports are <b>only stored locally</b> and never uploaded.<br>"
|
||||
"<br>Please <a "
|
||||
"href=\"https://github.com/Chatterino/chatterino2/issues/new\">report "
|
||||
"the crash</a> "
|
||||
u"so it can be prevented in the future."_s;
|
||||
|
||||
if (Modes::instance().isNightly)
|
||||
{
|
||||
text += u" Make sure you're using the latest nightly version!"_s;
|
||||
}
|
||||
|
||||
text +=
|
||||
u"<br>For more information, <a href=\"https://wiki.chatterino.com/Crash%20Analysis/\">consult the wiki</a>."_s;
|
||||
#endif
|
||||
|
||||
auto label = layout.emplace<QLabel>(text);
|
||||
label->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
label->setOpenExternalLinks(true);
|
||||
label->setWordWrap(true);
|
||||
|
||||
layout->addSpacing(16);
|
||||
|
||||
// auto update = layout.emplace<QLabel>();
|
||||
auto buttons = layout.emplace<QDialogButtonBox>();
|
||||
|
||||
// auto *installUpdateButton = buttons->addButton("Install Update",
|
||||
// QDialogButtonBox::NoRole); installUpdateButton->setEnabled(false);
|
||||
// QObject::connect(installUpdateButton, &QPushButton::clicked, [this,
|
||||
// update]() mutable {
|
||||
// auto &updateManager = UpdateManager::instance();
|
||||
|
||||
// updateManager.installUpdates();
|
||||
// this->setEnabled(false);
|
||||
// update->setText("Downloading updates...");
|
||||
// });
|
||||
|
||||
auto *okButton =
|
||||
buttons->addButton("Ignore", QDialogButtonBox::ButtonRole::NoRole);
|
||||
auto *okButton = buttons->addButton(u"Ok"_s, QDialogButtonBox::AcceptRole);
|
||||
QObject::connect(okButton, &QPushButton::clicked, [this] {
|
||||
this->accept();
|
||||
});
|
||||
|
||||
// Updates
|
||||
// auto updateUpdateLabel = [update]() mutable {
|
||||
// auto &updateManager = UpdateManager::instance();
|
||||
|
||||
// switch (updateManager.getStatus()) {
|
||||
// case UpdateManager::None: {
|
||||
// update->setText("Not checking for updates.");
|
||||
// } break;
|
||||
// case UpdateManager::Searching: {
|
||||
// update->setText("Checking for updates...");
|
||||
// } break;
|
||||
// case UpdateManager::UpdateAvailable: {
|
||||
// update->setText("Update available.");
|
||||
// } break;
|
||||
// case UpdateManager::NoUpdateAvailable: {
|
||||
// update->setText("No update abailable.");
|
||||
// } break;
|
||||
// case UpdateManager::SearchFailed: {
|
||||
// update->setText("Error while searching for update.\nEither
|
||||
// the update service is "
|
||||
// "temporarily down or there is an issue
|
||||
// with your installation.");
|
||||
// } break;
|
||||
// case UpdateManager::Downloading: {
|
||||
// update->setText(
|
||||
// "Downloading the update. Chatterino will close once
|
||||
// the download is done.");
|
||||
// } break;
|
||||
// case UpdateManager::DownloadFailed: {
|
||||
// update->setText("Download failed.");
|
||||
// } break;
|
||||
// case UpdateManager::WriteFileFailed: {
|
||||
// update->setText("Writing the update file to the hard drive
|
||||
// failed.");
|
||||
// } break;
|
||||
// }
|
||||
// };
|
||||
|
||||
// updateUpdateLabel();
|
||||
// this->signalHolder_.managedConnect(updateManager.statusUpdated,
|
||||
// [updateUpdateLabel](auto) mutable {
|
||||
// postToThread([updateUpdateLabel]() mutable { updateUpdateLabel();
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <pajlada/signals/signalholder.hpp>
|
||||
#include <QDialog>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -9,9 +8,6 @@ class LastRunCrashDialog : public QDialog
|
|||
{
|
||||
public:
|
||||
LastRunCrashDialog();
|
||||
|
||||
private:
|
||||
pajlada::Signals::SignalHolder signalHolder_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "controllers/sound/ISoundController.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
#include "singletons/NativeMessaging.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
|
@ -875,8 +876,14 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
|||
s.openLinksIncognito);
|
||||
}
|
||||
|
||||
layout.addCheckbox(
|
||||
"Restart on crash", s.restartOnCrash, false,
|
||||
layout.addCustomCheckbox(
|
||||
"Restart on crash (requires restart)",
|
||||
[] {
|
||||
return getApp()->crashHandler->shouldRecover();
|
||||
},
|
||||
[](bool on) {
|
||||
return getApp()->crashHandler->saveShouldRecover(on);
|
||||
},
|
||||
"When possible, restart Chatterino if the program crashes");
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(NO_QTKEYCHAIN)
|
||||
|
|
|
@ -125,6 +125,28 @@ QCheckBox *GeneralPageView::addCheckbox(const QString &text,
|
|||
return check;
|
||||
}
|
||||
|
||||
QCheckBox *GeneralPageView::addCustomCheckbox(const QString &text,
|
||||
const std::function<bool()> &load,
|
||||
std::function<void(bool)> save,
|
||||
const QString &toolTipText)
|
||||
{
|
||||
auto *check = new QCheckBox(text);
|
||||
this->addToolTip(*check, toolTipText);
|
||||
|
||||
check->setChecked(load());
|
||||
|
||||
QObject::connect(check, &QCheckBox::toggled, this,
|
||||
[save = std::move(save)](bool state) {
|
||||
save(state);
|
||||
});
|
||||
|
||||
this->addWidget(check);
|
||||
|
||||
this->groups_.back().widgets.push_back({check, {text}});
|
||||
|
||||
return check;
|
||||
}
|
||||
|
||||
ComboBox *GeneralPageView::addDropdown(const QString &text,
|
||||
const QStringList &list,
|
||||
QString toolTipText)
|
||||
|
|
|
@ -103,6 +103,11 @@ public:
|
|||
/// @param inverse Inverses true to false and vice versa
|
||||
QCheckBox *addCheckbox(const QString &text, BoolSetting &setting,
|
||||
bool inverse = false, QString toolTipText = {});
|
||||
QCheckBox *addCustomCheckbox(const QString &text,
|
||||
const std::function<bool()> &load,
|
||||
std::function<void(bool)> save,
|
||||
const QString &toolTipText = {});
|
||||
|
||||
ComboBox *addDropdown(const QString &text, const QStringList &items,
|
||||
QString toolTipText = {});
|
||||
ComboBox *addDropdown(const QString &text, const QStringList &items,
|
||||
|
|
1
tools/crash-handler
Submodule
1
tools/crash-handler
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 9753fe802710b2df00f2287ec2e1ca78c251d085
|
Loading…
Reference in a new issue