diff --git a/apps/light/CMakeLists.txt b/apps/light/CMakeLists.txt index 89e2f06a..2a66383b 100644 --- a/apps/light/CMakeLists.txt +++ b/apps/light/CMakeLists.txt @@ -233,15 +233,17 @@ if(MONKY_LIGHT_WITH_RTC) RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin" RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/bin") if(WIN32) - add_library(monky_light_ui STATIC src/ui/tray_windows.cpp src/ui/connect_windows.cpp) - target_link_libraries(monky_light_ui PUBLIC Shell32 User32 Gdi32 Ole32) + add_library(monky_light_ui STATIC src/ui/tray_windows.cpp src/ui/connect_windows.cpp + src/ui/startup_windows.cpp) + target_link_libraries(monky_light_ui PUBLIC Shell32 User32 Gdi32 Ole32 Advapi32) target_compile_definitions(monky_light_ui PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) else() find_library(MONKY_APPKIT_FRAMEWORK AppKit REQUIRED) # AXIsProcessTrusted: push-to-talk needs Accessibility to see keys. find_library(MONKY_APPLICATION_SERVICES_FRAMEWORK ApplicationServices REQUIRED) - add_library(monky_light_ui STATIC src/ui/tray_macos.mm src/ui/connect_macos.mm) - set_source_files_properties(src/ui/tray_macos.mm src/ui/connect_macos.mm PROPERTIES COMPILE_OPTIONS "-fobjc-arc") + add_library(monky_light_ui STATIC src/ui/tray_macos.mm src/ui/connect_macos.mm src/ui/startup_macos.mm) + set_source_files_properties(src/ui/tray_macos.mm src/ui/connect_macos.mm src/ui/startup_macos.mm + PROPERTIES COMPILE_OPTIONS "-fobjc-arc") target_link_libraries(monky_light_ui PUBLIC "${MONKY_APPKIT_FRAMEWORK}" "${MONKY_APPLICATION_SERVICES_FRAMEWORK}") endif() diff --git a/apps/light/QA.en.md b/apps/light/QA.en.md index 79685f75..f702806d 100644 --- a/apps/light/QA.en.md +++ b/apps/light/QA.en.md @@ -156,3 +156,4 @@ available, so the terminal shows what each click did. | 6.37 | `Change server…` in the tray | The Light leaves the current server, the icon disappears, and the window reopens filled with the current server, without a password | | | | 6.38 | Open a second copy with the same profile | Profile unavailable notice; the first copy stays connected | | | | 6.39 | Cancel the window | The Light quits without connecting and leaves no process | | | +| 6.40 | Turn `Start with the system` on, sign out of the system and back in | The Light opens by itself, tray only, and connects to the last server; turning it off removes the registration and it no longer opens | | | diff --git a/apps/light/QA.md b/apps/light/QA.md index 0488299d..3398c35c 100644 --- a/apps/light/QA.md +++ b/apps/light/QA.md @@ -157,3 +157,4 @@ valendo, então o terminal serve para conferir o que cada clique fez. | 6.37 | `Trocar de servidor…` na bandeja | O Light sai do servidor atual, o ícone some e a janela reabre preenchida com o servidor atual, sem senha | | | | 6.38 | Abrir uma segunda cópia com o mesmo perfil | Aviso de perfil indisponível; a primeira cópia continua conectada | | | | 6.39 | Cancelar a janela | O Light encerra sem conectar e sem deixar processo | | | +| 6.40 | Ligar `Iniciar com o sistema`, sair da sessão do sistema e entrar de novo | O Light abre sozinho, só com a bandeja, e conecta ao último servidor; desligar remove o registro e ele não abre mais | | | diff --git a/apps/light/README.en.md b/apps/light/README.en.md index 2d57c952..6accb92f 100644 --- a/apps/light/README.en.md +++ b/apps/light/README.en.md @@ -208,6 +208,15 @@ process. console for an instant, which the Light lets go right away. A console shared with the terminal that started it is kept. +- **Start with the system**, off by default, registers this executable with no + arguments to open at sign-in: a value under + `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` on Windows and a + LaunchAgent `~/Library/LaunchAgents/org.monky.light.plist` on macOS + (effective from the next login; works from macOS 12). The row is checked only + when the registration points at this very executable, and it is not offered + with `MONKY_LIGHT_PROFILE`, so a development build never registers itself + with the real profile. + Any argument keeps the command-line behaviour documented above, even without `--tray`. @@ -239,8 +248,8 @@ rendering between activations. It carries: - `In the call`, with each participant, who is speaking, and the volume, in presets from 0 to 200%; - `Audio devices`, with input, output, and `System default`; -- `Reconnect now`, `Change server…` (only when opened as an application), and - `Quit Monky Light`. +- `Reconnect now`, `Change server…` and `Start with the system` (both only when + opened as an application), and `Quit Monky Light`. In a native menu an exclusive choice (channel, device) uses a radio mark and a toggle uses a check mark: these are the native equivalents of the selectable card diff --git a/apps/light/README.md b/apps/light/README.md index 87b49528..71e25276 100644 --- a/apps/light/README.md +++ b/apps/light/README.md @@ -209,6 +209,15 @@ no mesmo processo. por um instante, que o Light solta logo em seguida. Um console compartilhado com o terminal que o iniciou é mantido. +- **Iniciar com o sistema**, desligado por padrão, registra este executável + sem argumentos para abrir no login: um valor em + `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` no Windows e um + LaunchAgent `~/Library/LaunchAgents/org.monky.light.plist` no macOS (vale a + partir do próximo login; funciona desde o macOS 12). O item só aparece + marcado quando o registro aponta para este mesmo executável, e não é + oferecido com `MONKY_LIGHT_PROFILE`, para um build de desenvolvimento nunca + se registrar com o perfil real. + Qualquer argumento mantém o comportamento de linha de comando documentado acima, inclusive sem `--tray`. @@ -240,8 +249,8 @@ renderizando entre as ativações. Ele traz: - `Na chamada`, com cada participante, quem está falando e o volume, em predefinições de 0 a 200%; - `Dispositivos de áudio`, com entrada, saída e `Padrão do sistema`; -- `Reconectar agora`, `Trocar de servidor…` (só quando aberto como aplicativo) - e `Sair do Monky Light`. +- `Reconectar agora`, `Trocar de servidor…` e `Iniciar com o sistema` (os dois + só quando aberto como aplicativo) e `Sair do Monky Light`. Em menu nativo, escolha exclusiva (canal, dispositivo) usa marca de rádio e liga/desliga usa marca de seleção: são os equivalentes nativos do card diff --git a/apps/light/src/main.cpp b/apps/light/src/main.cpp index c099792b..e13f29ff 100644 --- a/apps/light/src/main.cpp +++ b/apps/light/src/main.cpp @@ -3,6 +3,7 @@ #include "profile_identity.hpp" #include "standalone.hpp" #include "ui/connect.hpp" +#include "ui/startup.hpp" #include "ui/tray.hpp" #include @@ -97,9 +98,14 @@ int runApplication() { if (GetConsoleProcessList(processes, 2) == 1) FreeConsole(); #endif monky::light::StandaloneHooks hooks; + // A build started with a development or QA profile must not register itself + // to open at sign-in, where it would use the real profile instead. + bool developmentProfile = false; try { // Development and QA point this elsewhere, never at the installed profile. - hooks.profile = profileOverride().value_or(ui::defaultProfileDirectory()); + const auto override = profileOverride(); + developmentProfile = override.has_value(); + hooks.profile = override ? *override : ui::defaultProfileDirectory(); // The profile creates only its own folder; the one above is the Light's. std::filesystem::create_directories(hooks.profile.parent_path()); } catch (const std::exception& error) { @@ -123,9 +129,14 @@ int runApplication() { return ui::askConnection(current, issue, language); }; hooks.profileUnavailable = [language](const std::string& detail) { ui::showProfileUnavailable(detail, language); }; - hooks.run = [](std::vector arguments, std::optional password, - std::function)> askAgain) { + hooks.run = [developmentProfile, language](std::vector arguments, std::optional password, + std::function)> askAgain) { auto audio = configuration(); + if (!developmentProfile) { + audio.createInterface = [language](monky::light::CoreCommands core) { + return ui::createTray(std::move(core), language, ui::systemStartup()); + }; + } audio.console = false; audio.password = std::move(password); audio.askAgain = std::move(askAgain); diff --git a/apps/light/src/ui/startup.hpp b/apps/light/src/ui/startup.hpp new file mode 100644 index 00000000..3ed1042c --- /dev/null +++ b/apps/light/src/ui/startup.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace monky::light::ui { + +// Starting with the system, for an application start only. Both functions run +// on the interface thread; `set` throws when the system refuses the change. +struct Startup { + std::function enabled; + std::function set; +}; + +// Registers this executable, with no arguments, to open at sign-in: a value +// under HKCU\...\CurrentVersion\Run on Windows, a LaunchAgent in +// ~/Library/LaunchAgents on macOS. Reported as enabled only when the +// registration points at this very executable, so another copy's entry is +// never shown as ours. +Startup systemStartup(); + +} // namespace monky::light::ui diff --git a/apps/light/src/ui/startup_macos.mm b/apps/light/src/ui/startup_macos.mm new file mode 100644 index 00000000..4a41144e --- /dev/null +++ b/apps/light/src/ui/startup_macos.mm @@ -0,0 +1,71 @@ +#include "ui/startup.hpp" + +#import + +#include + +namespace monky::light::ui { +namespace { + +// A per-user LaunchAgent works from macOS 12; SMAppService needs macOS 13. +NSString* const kLabel = @"org.monky.light"; + +NSURL* agentFile() { + NSURL* library = [[NSFileManager defaultManager] URLsForDirectory:NSLibraryDirectory + inDomains:NSUserDomainMask].firstObject; + if (!library) throw std::runtime_error("Could not find the Library folder"); + return [[library URLByAppendingPathComponent:@"LaunchAgents" isDirectory:YES] + URLByAppendingPathComponent:[kLabel stringByAppendingString:@".plist"]]; +} + +NSString* executable() { + NSString* path = [NSBundle mainBundle].executablePath; + if (!path) throw std::runtime_error("Could not find the Monky Light executable"); + return path; +} + +bool enabled() { + @autoreleasepool { + NSDictionary* agent = [NSDictionary dictionaryWithContentsOfURL:agentFile() error:nil]; + NSArray* arguments = agent[@"ProgramArguments"]; + return [arguments isKindOfClass:[NSArray class]] && arguments.count == 1 && + [arguments.firstObject isEqual:executable()]; + } +} + +void set(bool wanted) { + @autoreleasepool { + NSURL* file = agentFile(); + NSError* error = nil; + if (!wanted) { + if (![[NSFileManager defaultManager] removeItemAtURL:file error:&error] && + !(error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError)) { + throw std::runtime_error("macOS refused to stop starting Monky Light at login"); + } + return; + } + if (![[NSFileManager defaultManager] createDirectoryAtURL:file.URLByDeletingLastPathComponent + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + throw std::runtime_error("Could not create the LaunchAgents folder"); + } + // Takes effect at the next login; nothing is started now. + NSDictionary* agent = @{ + @"Label" : kLabel, + @"ProgramArguments" : @[ executable() ], + @"RunAtLoad" : @YES, + @"ProcessType" : @"Interactive", + @"LimitLoadToSessionType" : @"Aqua", + }; + if (![agent writeToURL:file error:&error]) { + throw std::runtime_error("macOS refused to start Monky Light at login"); + } + } +} + +} // namespace + +Startup systemStartup() { return {&enabled, &set}; } + +} // namespace monky::light::ui diff --git a/apps/light/src/ui/startup_windows.cpp b/apps/light/src/ui/startup_windows.cpp new file mode 100644 index 00000000..fee9c940 --- /dev/null +++ b/apps/light/src/ui/startup_windows.cpp @@ -0,0 +1,61 @@ +#include "platform/utf8.hpp" // Defines NOMINMAX and WIN32_LEAN_AND_MEAN before . +#include "ui/startup.hpp" + +#include +#include +#include + +namespace monky::light::ui { +namespace { + +constexpr wchar_t kRunKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; +constexpr wchar_t kValue[] = L"Monky Light"; + +// The quoted path of this executable and nothing else: no arguments is an +// application start, which reuses the saved server. +std::wstring command() { + std::vector path(MAX_PATH); + for (;;) { + const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (length == 0) throw std::runtime_error("Could not find the Monky Light executable"); + if (length < path.size()) return L"\"" + std::wstring(path.data(), length) + L"\""; + path.resize(path.size() * 2); + } +} + +bool enabled() { + DWORD size = 0; + if (RegGetValueW(HKEY_CURRENT_USER, kRunKey, kValue, RRF_RT_REG_SZ, nullptr, nullptr, &size) != ERROR_SUCCESS || + size == 0) { + return false; + } + std::wstring value(size / sizeof(wchar_t), L'\0'); + if (RegGetValueW(HKEY_CURRENT_USER, kRunKey, kValue, RRF_RT_REG_SZ, nullptr, value.data(), &size) != + ERROR_SUCCESS) { + return false; + } + value.resize(size / sizeof(wchar_t)); + while (!value.empty() && value.back() == L'\0') value.pop_back(); + return CompareStringOrdinal(value.c_str(), -1, command().c_str(), -1, TRUE) == CSTR_EQUAL; +} + +void set(bool wanted) { + if (wanted) { + const auto value = command(); + const auto bytes = static_cast((value.size() + 1) * sizeof(wchar_t)); + if (RegSetKeyValueW(HKEY_CURRENT_USER, kRunKey, kValue, REG_SZ, value.c_str(), bytes) != ERROR_SUCCESS) { + throw std::runtime_error("Windows refused to start Monky Light with the system"); + } + return; + } + const auto result = RegDeleteKeyValueW(HKEY_CURRENT_USER, kRunKey, kValue); + if (result != ERROR_SUCCESS && result != ERROR_FILE_NOT_FOUND) { + throw std::runtime_error("Windows refused to stop starting Monky Light with the system"); + } +} + +} // namespace + +Startup systemStartup() { return {&enabled, &set}; } + +} // namespace monky::light::ui diff --git a/apps/light/src/ui/tray.hpp b/apps/light/src/ui/tray.hpp index 317578f5..ea091308 100644 --- a/apps/light/src/ui/tray.hpp +++ b/apps/light/src/ui/tray.hpp @@ -1,6 +1,7 @@ #pragma once #include "core_commands.hpp" +#include "ui/startup.hpp" #include "ui/tray_strings.hpp" #include @@ -10,7 +11,9 @@ namespace monky::light::ui { // Creates the platform tray. It must be called on the process main thread, // before the core thread starts, because the returned interface is observed from // the core thread. Throws when the platform interface cannot be created. -std::unique_ptr createTray(CoreCommands core, Language language); +// `startup` is offered only to an application start (see systemStartup()). +std::unique_ptr createTray(CoreCommands core, Language language, + std::optional startup = std::nullopt); // The operating system interface language, falling back to English. Language systemLanguage(); diff --git a/apps/light/src/ui/tray_controller.hpp b/apps/light/src/ui/tray_controller.hpp index e8e46fbc..5ff78d35 100644 --- a/apps/light/src/ui/tray_controller.hpp +++ b/apps/light/src/ui/tray_controller.hpp @@ -1,8 +1,10 @@ #pragma once #include "core_commands.hpp" +#include "ui/startup.hpp" #include "ui/tray_model.hpp" +#include #include #include #include @@ -16,8 +18,8 @@ namespace monky::light::ui { // the same command-accepted/command-error answers. class TrayController final { public: - TrayController(CoreCommands core, Language language) - : core_(std::move(core)), model_(language) {} + TrayController(CoreCommands core, Language language, std::optional startup = std::nullopt) + : core_(std::move(core)), model_(language), startup_(std::move(startup)) {} // Core thread. Returns true when the interface must redraw or notify. bool observe(const Json& event) { @@ -39,14 +41,33 @@ class TrayController final { std::lock_guard lock(mutex_); auto result = model_.presentation(); result.canSwitchServer = static_cast(core_.switchServer); + result.startupAvailable = startup_.has_value(); + // Read when the menu is built, so a change made elsewhere is never hidden. + try { + result.startupChecked = startup_ && startup_->enabled(); + } catch (const std::exception&) { + result.startupChecked = false; + } return result; } std::optional takeAlert() { std::lock_guard lock(mutex_); + if (startupFailure_) return std::exchange(startupFailure_, std::nullopt); return model_.takeAlert(); } + // Interface thread. A refusal is shown like any other failure. + void setStartup(bool enabled) { + if (!startup_) return; + try { + startup_->set(enabled); + } catch (const std::exception& error) { + std::lock_guard lock(mutex_); + startupFailure_ = error.what(); + } + } + void join(const std::string& channelId) { core_.command(Json{{"command", "join"}, {"channelId", channelId}}); } @@ -117,6 +138,8 @@ class TrayController final { mutable std::mutex mutex_; TrayModel model_; bool pressed_ = false; + std::optional startup_; + std::optional startupFailure_; }; } // namespace monky::light::ui diff --git a/apps/light/src/ui/tray_macos.mm b/apps/light/src/ui/tray_macos.mm index 9df9a2a9..061c7eec 100644 --- a/apps/light/src/ui/tray_macos.mm +++ b/apps/light/src/ui/tray_macos.mm @@ -29,6 +29,7 @@ constexpr NSInteger kPushToTalk = 9; constexpr NSInteger kVolume = 10; constexpr NSInteger kSwitchServer = 11; +constexpr NSInteger kStartup = 12; // Device-dependent modifier bits (NX_DEVICER*KEYMASK): a right-hand modifier // arrives as flagsChanged, and only these bits tell it from the left one. @@ -62,8 +63,8 @@ - (instancetype)initWithOwner:(void*)owner; class MacTray final : public CoreInterface { public: - MacTray(CoreCommands core, Language language) - : controller_(std::move(core), language), text_(textFor(language)) { + MacTray(CoreCommands core, Language language, std::optional startup) + : controller_(std::move(core), language, std::move(startup)), text_(textFor(language)) { @autoreleasepool { NSApplication* application = [NSApplication sharedApplication]; // An accessory application has no Dock tile and no window; the status item @@ -173,6 +174,7 @@ void build(NSMenu* menu) { [menu addItem:[NSMenuItem separatorItem]]; add(menu, text_.reconnectNow, kReconnect, true, false, nil); if (view.canSwitchServer) add(menu, text_.switchServer, kSwitchServer, true, false, nil); + if (view.startupAvailable) add(menu, text_.startWithSystem, kStartup, true, view.startupChecked, nil); add(menu, text_.quit, kQuit, true, false, nil); } @@ -192,6 +194,10 @@ void activate(NSMenuItem* item) { case kReconnect: controller_.reconnect(); break; case kPushToTalk: controller_.setPushToTalk(wanted); break; case kSwitchServer: controller_.switchServer(); break; + case kStartup: + controller_.setStartup(wanted); + refresh(); // A refusal arrives as an alert. + break; case kJoin: controller_.join(identifier); break; case kInput: controller_.selectInput(identifier); break; case kOutput: controller_.selectOutput(identifier); break; @@ -314,8 +320,8 @@ void unhook() { std::optional alert_; }; -std::unique_ptr createTray(CoreCommands core, Language language) { - return std::make_unique(std::move(core), language); +std::unique_ptr createTray(CoreCommands core, Language language, std::optional startup) { + return std::make_unique(std::move(core), language, std::move(startup)); } Language systemLanguage() { diff --git a/apps/light/src/ui/tray_model.hpp b/apps/light/src/ui/tray_model.hpp index 62324e35..f7488f29 100644 --- a/apps/light/src/ui/tray_model.hpp +++ b/apps/light/src/ui/tray_model.hpp @@ -67,6 +67,9 @@ struct Presentation { std::vector people; // Only when opened as an application, where the window can ask again. bool canSwitchServer = false; + // Same condition, minus development and QA profiles: see systemStartup(). + bool startupAvailable = false; + bool startupChecked = false; }; // Rebuilds the interface state from the events the console consumer already diff --git a/apps/light/src/ui/tray_strings.hpp b/apps/light/src/ui/tray_strings.hpp index 4c799408..21544216 100644 --- a/apps/light/src/ui/tray_strings.hpp +++ b/apps/light/src/ui/tray_strings.hpp @@ -68,6 +68,7 @@ struct Strings { const char* nicknameTaken; const char* nicknameRefused; const char* profileUnavailable; + const char* startWithSystem; }; inline constexpr Strings kEnglish{ @@ -127,6 +128,7 @@ inline constexpr Strings kEnglish{ "Someone on this server already uses this nickname. Choose another one.", "The server did not accept this nickname. Choose another one.", "Monky Light could not open its profile. It may already be running: look for its icon near the clock.", + "Start with the system", }; inline constexpr Strings kPortuguese{ @@ -186,6 +188,7 @@ inline constexpr Strings kPortuguese{ "Alguém neste servidor já usa este apelido. Escolha outro.", "O servidor não aceitou este apelido. Escolha outro.", "O Monky Light não conseguiu abrir o perfil. Ele talvez já esteja aberto: procure o ícone perto do relógio.", + "Iniciar com o sistema", }; inline constexpr const Strings& textFor(Language language) { diff --git a/apps/light/src/ui/tray_windows.cpp b/apps/light/src/ui/tray_windows.cpp index 13d2ce5b..25861e91 100644 --- a/apps/light/src/ui/tray_windows.cpp +++ b/apps/light/src/ui/tray_windows.cpp @@ -31,6 +31,7 @@ constexpr UINT kDeafen = 4; constexpr UINT kReconnect = 5; constexpr UINT kPushToTalk = 6; constexpr UINT kSwitchServer = 7; +constexpr UINT kStartup = 8; constexpr UINT kRange = 0x0400; constexpr UINT kChannels = 0x1000; constexpr UINT kInputs = kChannels + kRange; @@ -139,8 +140,8 @@ void copyInto(WCHAR* destination, std::size_t capacity, const std::string& value class WindowsTray final : public CoreInterface { public: - WindowsTray(CoreCommands core, Language language) - : controller_(std::move(core), language), text_(textFor(language)) { + WindowsTray(CoreCommands core, Language language, std::optional startup) + : controller_(std::move(core), language, std::move(startup)), text_(textFor(language)) { WNDCLASSEXW description{}; description.cbSize = sizeof(description); description.lpfnWndProc = &WindowsTray::proc; @@ -434,6 +435,7 @@ class WindowsTray final : public CoreInterface { separator(menu); append(menu, kReconnect, text_.reconnectNow, false, true, false); if (view.canSwitchServer) append(menu, kSwitchServer, text_.switchServer, false, true, false); + if (view.startupAvailable) append(menu, kStartup, text_.startWithSystem, view.startupChecked, true, false); append(menu, kQuit, text_.quit, false, true, false); POINT cursor{}; GetCursorPos(&cursor); @@ -458,6 +460,10 @@ class WindowsTray final : public CoreInterface { else if (identifier == kReconnect) controller_.reconnect(); else if (identifier == kPushToTalk) controller_.setPushToTalk(!view.pushToTalkChecked); else if (identifier == kSwitchServer) controller_.switchServer(); + else if (identifier == kStartup) { + controller_.setStartup(!view.startupChecked); + refresh(); // A refusal arrives as an alert. + } else if (identifier >= kPeople) { const auto person = static_cast((identifier - kPeople) / kPresets); const auto preset = static_cast((identifier - kPeople) % kPresets); @@ -546,8 +552,8 @@ class WindowsTray final : public CoreInterface { } // namespace -std::unique_ptr createTray(CoreCommands core, Language language) { - return std::make_unique(std::move(core), language); +std::unique_ptr createTray(CoreCommands core, Language language, std::optional startup) { + return std::make_unique(std::move(core), language, std::move(startup)); } Language systemLanguage() { diff --git a/apps/light/test/tray_model_test.cpp b/apps/light/test/tray_model_test.cpp index 53dd9b2e..06a0d1b7 100644 --- a/apps/light/test/tray_model_test.cpp +++ b/apps/light/test/tray_model_test.cpp @@ -441,6 +441,29 @@ void changingServerExistsOnlyForAnApplicationStart() { require(switched, "Changing server must reach the application start"); } +void startingWithTheSystemFollowsTheRegistration() { + TrayController none({[](Json) {}, [] {}}, Language::english); + require(!none.presentation().startupAvailable, "Without a registration the row is not offered"); + bool registered = false; + bool refuse = false; + monky::light::ui::Startup startup{[&] { return registered; }, [&](bool wanted) { + if (refuse) throw std::runtime_error("registry is read-only"); + registered = wanted; + }}; + TrayController controller({[](Json) {}, [] {}}, Language::english, startup); + auto view = controller.presentation(); + require(view.startupAvailable && !view.startupChecked, "An unregistered start is offered unchecked"); + controller.setStartup(true); + require(registered && controller.presentation().startupChecked, "Checking the row registers the start"); + registered = false; + require(!controller.presentation().startupChecked, "The row reads the system each time, not a copy"); + refuse = true; + controller.setStartup(true); + const auto alert = controller.takeAlert(); + require(alert && *alert == "registry is read-only", "A refused registration must be shown"); + require(!controller.presentation().startupChecked, "A refused registration stays unchecked"); +} + void pushToTalkKeysAreNamedForBothPlatforms() { using monky::light::parsePushToTalkKey; require(!parsePushToTalkKey("CapsLock") && !parsePushToTalkKey("f13") && !parsePushToTalkKey(""), @@ -479,6 +502,7 @@ int main() { peopleCarrySpeechAndVolume(); theControllerGatesPushToTalk(); changingServerExistsOnlyForAnApplicationStart(); + startingWithTheSystemFollowsTheRegistration(); pushToTalkKeysAreNamedForBothPlatforms(); malformedEventsAreIgnored(); std::cout << "Tray interface scenarios passed\n"; diff --git a/release-notes/658-light-start-with-system.json b/release-notes/658-light-start-with-system.json new file mode 100644 index 00000000..ebf0ff2b --- /dev/null +++ b/release-notes/658-light-start-with-system.json @@ -0,0 +1,5 @@ +{ + "group": "novidades", + "pt-BR": "A bandeja do Monky Light ganhou Iniciar com o sistema, desligado por padrão: com ele ligado, o Light abre sozinho ao entrar no computador e conecta ao último servidor.", + "en": "The Monky Light tray gained Start with the system, off by default: when on, the Light opens by itself when you sign in and connects to the last server." +}