From 3d282c10784661789881a835779d76f606556465 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 17 Aug 2021 17:59:35 +0100 Subject: [PATCH] Desktop: Deprecate isOSXDarkModeActive() and add isDarkModeActive() for other platforms --- .../native/juce_android_JNIHelpers.h | 9 +- .../juce_gui_basics/desktop/juce_Desktop.cpp | 9 +- .../juce_gui_basics/desktop/juce_Desktop.h | 66 +++++++++++-- .../native/juce_android_Windowing.cpp | 80 ++++++++++++++++ .../native/juce_ios_UIViewComponentPeer.mm | 49 +++++++--- .../native/juce_ios_Windowing.mm | 71 ++++++++++++++ .../native/juce_linux_Windowing.cpp | 12 +++ .../native/juce_mac_Windowing.mm | 73 +++++++++++++-- .../native/juce_win32_Windowing.cpp | 92 ++++++++++++++++++- 9 files changed, 433 insertions(+), 28 deletions(-) diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 0f6d993c3d..4131d326fb 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -503,11 +503,18 @@ DECLARE_JNI_CLASS (AndroidRect, "android/graphics/Rect") #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (getIdentifier, "getIdentifier", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I") \ - METHOD (openRawResourceFd, "openRawResourceFd", "(I)Landroid/content/res/AssetFileDescriptor;") + METHOD (openRawResourceFd, "openRawResourceFd", "(I)Landroid/content/res/AssetFileDescriptor;") \ + METHOD (getConfiguration, "getConfiguration", "()Landroid/content/res/Configuration;") DECLARE_JNI_CLASS (AndroidResources, "android/content/res/Resources") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + FIELD (uiMode, "uiMode", "I") \ + +DECLARE_JNI_CLASS (AndroidConfiguration, "android/content/res/Configuration") +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (getHeight, "getHeight", "()I") \ METHOD (getWidth, "getWidth", "()I") diff --git a/modules/juce_gui_basics/desktop/juce_Desktop.cpp b/modules/juce_gui_basics/desktop/juce_Desktop.cpp index 8dcde1b3a0..8035b700eb 100644 --- a/modules/juce_gui_basics/desktop/juce_Desktop.cpp +++ b/modules/juce_gui_basics/desktop/juce_Desktop.cpp @@ -28,7 +28,8 @@ namespace juce Desktop::Desktop() : mouseSources (new MouseInputSource::SourceList()), - masterScaleFactor ((float) getDefaultMasterScale()) + masterScaleFactor ((float) getDefaultMasterScale()), + nativeDarkModeChangeDetectorImpl (createNativeDarkModeChangeDetectorImpl()) { displays.reset (new Displays (*this)); } @@ -198,6 +199,12 @@ void Desktop::handleAsyncUpdate() }); } +//============================================================================== +void Desktop::addDarkModeSettingListener (DarkModeSettingListener* l) { darkModeSettingListeners.add (l); } +void Desktop::removeDarkModeSettingListener (DarkModeSettingListener* l) { darkModeSettingListeners.remove (l); } + +void Desktop::darkModeChanged() { darkModeSettingListeners.call ([] (DarkModeSettingListener& l) { l.darkModeSettingChanged(); }); } + //============================================================================== void Desktop::resetTimer() { diff --git a/modules/juce_gui_basics/desktop/juce_Desktop.h b/modules/juce_gui_basics/desktop/juce_Desktop.h index e5b84ea98b..93b630a9ba 100644 --- a/modules/juce_gui_basics/desktop/juce_Desktop.h +++ b/modules/juce_gui_basics/desktop/juce_Desktop.h @@ -45,6 +45,26 @@ public: virtual void globalFocusChanged (Component* focusedComponent) = 0; }; +//============================================================================== +/** + Classes can implement this interface and register themselves with the Desktop class + to receive callbacks when the operating system dark mode setting changes. The + Desktop::isDarkModeActive() method can then be used to query the current setting. + + @see Desktop::addDarkModeSettingListener, Desktop::removeDarkModeSettingListener, + Desktop::isDarkModeActive + + @tags{GUI} +*/ +class JUCE_API DarkModeSettingListener +{ +public: + /** Destructor. */ + virtual ~DarkModeSettingListener() = default; + + /** Callback to indicate that the dark mode setting has changed. */ + virtual void darkModeSettingChanged() = 0; +}; //============================================================================== /** @@ -135,8 +155,7 @@ public: */ void addGlobalMouseListener (MouseListener* listener); - /** Unregisters a MouseListener that was added with the addGlobalMouseListener() - method. + /** Unregisters a MouseListener that was added with addGlobalMouseListener(). @see addGlobalMouseListener */ @@ -150,13 +169,36 @@ public: */ void addFocusChangeListener (FocusChangeListener* listener); - /** Unregisters a FocusChangeListener that was added with the addFocusChangeListener() - method. + /** Unregisters a FocusChangeListener that was added with addFocusChangeListener(). @see addFocusChangeListener */ void removeFocusChangeListener (FocusChangeListener* listener); + //============================================================================== + /** Registers a DarkModeSettingListener that will receive a callback when the + operating system dark mode setting changes. To query whether dark mode is on + use the isDarkModeActive() method. + + @see isDarkModeActive, removeDarkModeSettingListener + */ + void addDarkModeSettingListener (DarkModeSettingListener* listener); + + /** Unregisters a DarkModeSettingListener that was added with addDarkModeSettingListener(). + + @see addDarkModeSettingListener + */ + void removeDarkModeSettingListener (DarkModeSettingListener* listener); + + /** True if the operating system "dark mode" is active. + + To receive a callback when this setting changes implement the DarkModeSettingListener + interface and use the addDarkModeSettingListener() to register a listener. + + @see addDarkModeSettingListener, removeDarkModeSettingListener + */ + bool isDarkModeActive() const; + //============================================================================== /** Takes a component and makes it full-screen, removing the taskbar, dock, etc. @@ -352,9 +394,10 @@ public: /** True if the OS supports semitransparent windows */ static bool canUseSemiTransparentWindows() noexcept; - #if JUCE_MAC - /** OSX-specific function to check for the "dark" title-bar and menu mode. */ - static bool isOSXDarkModeActive(); + #if JUCE_MAC && ! defined (DOXYGEN) + [[deprecated ("This macOS-specific method has been deprecated in favour of the cross-platform " + " isDarkModeActive() method.")]] + static bool isOSXDarkModeActive() { return Desktop::getInstance().isDarkModeActive(); } #endif //============================================================================== @@ -376,6 +419,7 @@ private: ListenerList mouseListeners; ListenerList focusListeners; + ListenerList darkModeSettingListeners; Array desktopComponents; Array peers; @@ -423,6 +467,14 @@ private: Desktop(); ~Desktop() override; + //============================================================================== + class NativeDarkModeChangeDetectorImpl; + std::unique_ptr nativeDarkModeChangeDetectorImpl; + + static std::unique_ptr createNativeDarkModeChangeDetectorImpl(); + void darkModeChanged(); + + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Desktop) }; diff --git a/modules/juce_gui_basics/native/juce_android_Windowing.cpp b/modules/juce_gui_basics/native/juce_android_Windowing.cpp index 7ce97192c1..1a63a983bd 100644 --- a/modules/juce_gui_basics/native/juce_android_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_android_Windowing.cpp @@ -1236,6 +1236,86 @@ bool Desktop::canUseSemiTransparentWindows() noexcept return true; } +class Desktop::NativeDarkModeChangeDetectorImpl : public ActivityLifecycleCallbacks +{ +public: + NativeDarkModeChangeDetectorImpl() + { + LocalRef appContext (getAppContext()); + + if (appContext != nullptr) + { + auto* env = getEnv(); + + myself = GlobalRef (CreateJavaInterface (this, "android/app/Application$ActivityLifecycleCallbacks")); + env->CallVoidMethod (appContext.get(), AndroidApplication.registerActivityLifecycleCallbacks, myself.get()); + } + } + + ~NativeDarkModeChangeDetectorImpl() override + { + LocalRef appContext (getAppContext()); + + if (appContext != nullptr && myself != nullptr) + { + auto* env = getEnv(); + + env->CallVoidMethod (appContext.get(), + AndroidApplication.unregisterActivityLifecycleCallbacks, + myself.get()); + clear(); + myself.clear(); + } + } + + bool isDarkModeEnabled() const noexcept { return darkModeEnabled; } + + void onActivityStarted (jobject /*activity*/) override + { + const auto isEnabled = getDarkModeSetting(); + + if (darkModeEnabled != isEnabled) + { + darkModeEnabled = isEnabled; + Desktop::getInstance().darkModeChanged(); + } + } + +private: + static bool getDarkModeSetting() + { + auto* env = getEnv(); + + const LocalRef resources (env->CallObjectMethod (getAppContext().get(), AndroidContext.getResources)); + const LocalRef configuration (env->CallObjectMethod (resources, AndroidResources.getConfiguration)); + + const auto uiMode = env->GetIntField (configuration, AndroidConfiguration.uiMode); + + return ((uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES); + } + + static constexpr int UI_MODE_NIGHT_MASK = 0x00000030, + UI_MODE_NIGHT_NO = 0x00000010, + UI_MODE_NIGHT_UNDEFINED = 0x00000000, + UI_MODE_NIGHT_YES = 0x00000020; + + GlobalRef myself; + bool darkModeEnabled = getDarkModeSetting(); + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl) +}; + +std::unique_ptr Desktop::createNativeDarkModeChangeDetectorImpl() +{ + return std::make_unique(); +} + +bool Desktop::isDarkModeActive() const +{ + return nativeDarkModeChangeDetectorImpl->isDarkModeEnabled(); +} + double Desktop::getDefaultMasterScale() { return 1.0; diff --git a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm index 8788c3f259..8414a6a697 100644 --- a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm @@ -137,6 +137,8 @@ using namespace juce; - (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text; +- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection; + - (BOOL) isAccessibilityElement; - (CGRect) accessibilityFrame; - (NSArray*) accessibilityElements; @@ -270,6 +272,11 @@ public: return getMouseTime ([e timestamp]); } + static NSString* getDarkModeNotificationName() + { + return @"ViewDarkModeChanged"; + } + static MultiTouchMapper currentTouches; private: @@ -296,25 +303,27 @@ private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIViewComponentPeer) }; +static UIViewComponentPeer* getViewPeer (JuceUIViewController* c) +{ + if (JuceUIView* juceView = (JuceUIView*) [c view]) + return juceView->owner; + + jassertfalse; + return nullptr; +} + static void sendScreenBoundsUpdate (JuceUIViewController* c) { - JuceUIView* juceView = (JuceUIView*) [c view]; - - if (juceView != nil && juceView->owner != nullptr) - juceView->owner->updateScreenBounds(); + if (auto* peer = getViewPeer (c)) + peer->updateScreenBounds(); } static bool isKioskModeView (JuceUIViewController* c) { - JuceUIView* juceView = (JuceUIView*) [c view]; + if (auto* peer = getViewPeer (c)) + return Desktop::getInstance().getKioskModeComponent() == &(peer->getComponent()); - if (juceView == nil || juceView->owner == nullptr) - { - jassertfalse; - return false; - } - - return Desktop::getInstance().getKioskModeComponent() == &(juceView->owner->getComponent()); + return false; } MultiTouchMapper UIViewComponentPeer::currentTouches; @@ -544,6 +553,22 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; nsStringToJuce (text)); } +- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection +{ + [super traitCollectionDidChange: previousTraitCollection]; + + #if defined (__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0 + if (@available (iOS 12.0, *)) + { + const auto wasDarkModeActive = ([previousTraitCollection userInterfaceStyle] == UIUserInterfaceStyleDark); + + if (wasDarkModeActive != Desktop::getInstance().isDarkModeActive()) + [[NSNotificationCenter defaultCenter] postNotificationName: UIViewComponentPeer::getDarkModeNotificationName() + object: nil]; + } + #endif +} + - (BOOL) isAccessibilityElement { return NO; diff --git a/modules/juce_gui_basics/native/juce_ios_Windowing.mm b/modules/juce_gui_basics/native/juce_ios_Windowing.mm index f4e466e72b..b76c8b33d1 100644 --- a/modules/juce_gui_basics/native/juce_ios_Windowing.mm +++ b/modules/juce_gui_basics/native/juce_ios_Windowing.mm @@ -678,6 +678,77 @@ bool Desktop::canUseSemiTransparentWindows() noexcept return true; } +bool Desktop::isDarkModeActive() const +{ + #if defined (__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0 + if (@available (iOS 12.0, *)) + return [[[UIScreen mainScreen] traitCollection] userInterfaceStyle] == UIUserInterfaceStyleDark; + #endif + + return false; +} + +class Desktop::NativeDarkModeChangeDetectorImpl +{ +public: + NativeDarkModeChangeDetectorImpl() + { + static DelegateClass delegateClass; + + delegate = [delegateClass.createInstance() init]; + object_setInstanceVariable (delegate, "owner", this); + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") + [[NSNotificationCenter defaultCenter] addObserver: delegate + selector: @selector (darkModeChanged:) + name: UIViewComponentPeer::getDarkModeNotificationName() + object: nil]; + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + } + + ~NativeDarkModeChangeDetectorImpl() + { + object_setInstanceVariable (delegate, "owner", nullptr); + [[NSNotificationCenter defaultCenter] removeObserver: delegate]; + [delegate release]; + } + + void darkModeChanged() + { + Desktop::getInstance().darkModeChanged(); + } + +private: + struct DelegateClass : public ObjCClass + { + DelegateClass() : ObjCClass ("JUCEDelegate_") + { + addIvar ("owner"); + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") + addMethod (@selector (darkModeChanged:), darkModeChanged, "v@:@"); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + + registerClass(); + } + + static void darkModeChanged (id self, SEL, NSNotification*) + { + if (auto* owner = getIvar (self, "owner")) + owner->darkModeChanged(); + } + }; + + id delegate = nil; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl) +}; + +std::unique_ptr Desktop::createNativeDarkModeChangeDetectorImpl() +{ + return std::make_unique(); +} + Point MouseInputSource::getCurrentRawMousePosition() { return juce_lastMousePos; diff --git a/modules/juce_gui_basics/native/juce_linux_Windowing.cpp b/modules/juce_gui_basics/native/juce_linux_Windowing.cpp index 82cb46541e..8fc5b397ed 100644 --- a/modules/juce_gui_basics/native/juce_linux_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_linux_Windowing.cpp @@ -534,6 +534,18 @@ bool Desktop::canUseSemiTransparentWindows() noexcept return XWindowSystem::getInstance()->canUseSemiTransparentWindows(); } +bool Desktop::isDarkModeActive() const +{ + return false; +} + +class Desktop::NativeDarkModeChangeDetectorImpl { public: NativeDarkModeChangeDetectorImpl() = default; }; + +std::unique_ptr Desktop::createNativeDarkModeChangeDetectorImpl() +{ + return nullptr; +} + static bool screenSaverAllowed = true; void Desktop::setScreenSaverEnabled (bool isEnabled) diff --git a/modules/juce_gui_basics/native/juce_mac_Windowing.mm b/modules/juce_gui_basics/native/juce_mac_Windowing.mm index 68a4ed71d4..d96cb2d8e4 100644 --- a/modules/juce_gui_basics/native/juce_mac_Windowing.mm +++ b/modules/juce_gui_basics/native/juce_mac_Windowing.mm @@ -430,6 +430,73 @@ Desktop::DisplayOrientation Desktop::getCurrentOrientation() const return upright; } +bool Desktop::isDarkModeActive() const +{ + return [[[NSUserDefaults standardUserDefaults] stringForKey: nsStringLiteral ("AppleInterfaceStyle")] + isEqualToString: nsStringLiteral ("Dark")]; +} + +class Desktop::NativeDarkModeChangeDetectorImpl +{ +public: + NativeDarkModeChangeDetectorImpl() + { + static DelegateClass delegateClass; + + delegate = [delegateClass.createInstance() init]; + object_setInstanceVariable (delegate, "owner", this); + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") + [[NSDistributedNotificationCenter defaultCenter] addObserver: delegate + selector: @selector (darkModeChanged:) + name: @"AppleInterfaceThemeChangedNotification" + object: nil]; + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + } + + ~NativeDarkModeChangeDetectorImpl() + { + object_setInstanceVariable (delegate, "owner", nullptr); + [[NSDistributedNotificationCenter defaultCenter] removeObserver: delegate]; + [delegate release]; + } + + void darkModeChanged() + { + Desktop::getInstance().darkModeChanged(); + } + +private: + struct DelegateClass : public ObjCClass + { + DelegateClass() : ObjCClass ("JUCEDelegate_") + { + addIvar ("owner"); + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") + addMethod (@selector (darkModeChanged:), darkModeChanged, "v@:@"); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + + registerClass(); + } + + static void darkModeChanged (id self, SEL, NSNotification*) + { + if (auto* owner = getIvar (self, "owner")) + owner->darkModeChanged(); + } + }; + + id delegate = nil; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl) +}; + +std::unique_ptr Desktop::createNativeDarkModeChangeDetectorImpl() +{ + return std::make_unique(); +} + //============================================================================== class ScreenSaverDefeater : public Timer { @@ -674,10 +741,4 @@ void Process::setDockIconVisible (bool isVisible) ignoreUnused (err); } -bool Desktop::isOSXDarkModeActive() -{ - return [[[NSUserDefaults standardUserDefaults] stringForKey: nsStringLiteral ("AppleInterfaceStyle")] - isEqualToString: nsStringLiteral ("Dark")]; -} - } // namespace juce diff --git a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp index 1f456bac20..22581530fa 100644 --- a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp @@ -712,6 +712,8 @@ static void setWindowZOrder (HWND hwnd, HWND insertAfter) } //============================================================================== +extern RTL_OSVERSIONINFOW getWindowsVersionInfo(); + double Desktop::getDefaultMasterScale() { if (! JUCEApplicationBase::isStandaloneApp() || isPerMonitorDPIAwareProcess()) @@ -720,7 +722,95 @@ double Desktop::getDefaultMasterScale() return getGlobalDPI() / USER_DEFAULT_SCREEN_DPI; } -bool Desktop::canUseSemiTransparentWindows() noexcept { return true; } +bool Desktop::canUseSemiTransparentWindows() noexcept +{ + return true; +} + +class Desktop::NativeDarkModeChangeDetectorImpl +{ +public: + NativeDarkModeChangeDetectorImpl() + { + const auto winVer = getWindowsVersionInfo(); + + if (winVer.dwMajorVersion >= 10 && winVer.dwBuildNumber >= 17763) + { + const auto uxtheme = "uxtheme.dll"; + LoadLibraryA (uxtheme); + const auto uxthemeModule = GetModuleHandleA (uxtheme); + + if (uxthemeModule != nullptr) + { + shouldAppsUseDarkMode = (ShouldAppsUseDarkModeFunc) GetProcAddress (uxthemeModule, MAKEINTRESOURCEA (132)); + + if (shouldAppsUseDarkMode != nullptr) + darkModeEnabled = shouldAppsUseDarkMode() && ! isHighContrast(); + } + } + } + + bool isDarkModeEnabled() const noexcept { return darkModeEnabled; } + +private: + static bool isHighContrast() + { + HIGHCONTRASTW highContrast { 0 }; + + if (SystemParametersInfoW (SPI_GETHIGHCONTRAST, sizeof (highContrast), &highContrast, false)) + return highContrast.dwFlags & HCF_HIGHCONTRASTON; + + return false; + } + + static LRESULT CALLBACK callWndProc (int nCode, WPARAM wParam, LPARAM lParam) + { + auto* params = reinterpret_cast (lParam); + + if (nCode >= 0 + && params != nullptr + && params->message == WM_SETTINGCHANGE + && params->lParam != 0 + && CompareStringOrdinal (reinterpret_cast (params->lParam), -1, L"ImmersiveColorSet", -1, true) == CSTR_EQUAL) + { + Desktop::getInstance().nativeDarkModeChangeDetectorImpl->colourSetChanged(); + } + + return CallNextHookEx ({}, nCode, wParam, lParam); + } + + void colourSetChanged() + { + if (shouldAppsUseDarkMode != nullptr) + { + const auto wasDarkModeEnabled = std::exchange (darkModeEnabled, shouldAppsUseDarkMode() && ! isHighContrast()); + + if (darkModeEnabled != wasDarkModeEnabled) + Desktop::getInstance().darkModeChanged(); + } + } + + using ShouldAppsUseDarkModeFunc = bool (WINAPI*)(); + ShouldAppsUseDarkModeFunc shouldAppsUseDarkMode = nullptr; + + bool darkModeEnabled = false; + HHOOK hook { SetWindowsHookEx (WH_CALLWNDPROC, + callWndProc, + (HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(), + GetCurrentThreadId()) }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl) +}; + +std::unique_ptr Desktop::createNativeDarkModeChangeDetectorImpl() +{ + return std::make_unique(); +} + +bool Desktop::isDarkModeActive() const +{ + return nativeDarkModeChangeDetectorImpl->isDarkModeEnabled(); +} Desktop::DisplayOrientation Desktop::getCurrentOrientation() const {