Browse Source

feat: allow restoring macOS BrowserWindow state across relaunches

Shelley Vohr 1 year ago
parent
commit
ec539d6f27

+ 1 - 0
docs/api/structures/base-window-options.md

@@ -41,6 +41,7 @@
 * `skipTaskbar` boolean (optional) _macOS_ _Windows_ - Whether to show the window in taskbar.
   Default is `false`.
 * `hiddenInMissionControl` boolean (optional) _macOS_ - Whether window should be hidden when the user toggles into mission control.
+*  `restoreOnRelaunch` boolean (optional) _macOS_ - Whether the window should launch with full fidelity across app restarts.
 * `kiosk` boolean (optional) - Whether the window is in kiosk mode. Default is `false`.
 * `title` string (optional) - Default window title. Default is `"Electron"`. If the HTML tag `<title>` is defined in the HTML file loaded by `loadURL()`, this property will be ignored.
 * `icon` ([NativeImage](../native-image.md) | string) (optional) - The window icon. On Windows it is

+ 6 - 5
shell/browser/electron_browser_main_parts_mac.mm

@@ -28,11 +28,12 @@ void ElectronBrowserMainParts::PreCreateMainMessageLoop() {
 
   PreCreateMainMessageLoopCommon();
 
-  // Prevent Cocoa from turning command-line arguments into
-  // |-application:openFiles:|, since we already handle them directly.
-  [[NSUserDefaults standardUserDefaults]
-      setObject:@"NO"
-         forKey:@"NSTreatUnknownArgumentsAsOpen"];
+  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+  // Prevent Cocoa from turning command-line arguments into -[NSApplication
+  // application:openFile:], because they are handled directly. @"NO" looks
+  // like a mistake, but the value really is supposed to be a string.
+  [defaults setObject:@"NO" forKey:@"NSTreatUnknownArgumentsAsOpen"];
+  [defaults setBool:NO forKey:@"NSWindowRestoresWorkspaceAtLaunch"];
 
   if (!device::GeolocationSystemPermissionManager::GetInstance()) {
     device::GeolocationSystemPermissionManager::SetInstance(

+ 4 - 0
shell/browser/native_window_mac.h

@@ -164,6 +164,8 @@ class NativeWindowMac : public NativeWindow,
   // Detach window from parent without destroying it.
   void DetachChildren() override;
 
+  void OnWindowStateRestorationDataChanged(const std::vector<uint8_t>& data);
+
   void NotifyWindowWillEnterFullScreen();
   void NotifyWindowWillLeaveFullScreen();
 
@@ -286,6 +288,8 @@ class NativeWindowMac : public NativeWindow,
 
   bool user_set_bounds_maximized_ = false;
 
+  std::vector<uint8_t> state_restoration_data_;
+
   // Simple (pre-Lion) Fullscreen Settings
   bool always_simple_fullscreen_ = false;
   bool is_simple_fullscreen_ = false;

+ 26 - 2
shell/browser/native_window_mac.mm

@@ -47,7 +47,6 @@
 #include "ui/gfx/skia_util.h"
 #include "ui/gl/gpu_switching_manager.h"
 #include "ui/views/background.h"
-#include "ui/views/cocoa/native_widget_mac_ns_window_host.h"
 #include "ui/views/widget/widget.h"
 #include "ui/views/window/native_frame_view_mac.h"
 
@@ -261,6 +260,18 @@ NativeWindowMac::NativeWindowMac(const gin_helper::Dictionary& options,
   window_delegate_ = [[ElectronNSWindowDelegate alloc] initWithShell:this];
   [window_ setDelegate:window_delegate_];
 
+  if (options.Get(options::kRestoreOnRestart, &restore) && !restore) {
+    NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+    NSData* restore_ns_data = [defaults objectForKey:@"state_restoration_data"];
+    if (restore_ns_data != nil) {
+      NSKeyedUnarchiver* decoder =
+          [[NSKeyedUnarchiver alloc] initForReadingFromData:restore_ns_data
+                                                      error:nil];
+      [window_ restoreStateWithCoder:decoder];
+      state_restoration_data_.clear();
+    }
+  }
+
   // Only use native parent window for non-modal windows.
   if (parent && !is_modal()) {
     SetParentWindow(parent);
@@ -355,7 +366,14 @@ NativeWindowMac::NativeWindowMac(const gin_helper::Dictionary& options,
   original_level_ = [window_ level];
 }
 
-NativeWindowMac::~NativeWindowMac() = default;
+NativeWindowMac::~NativeWindowMac() {
+  if (!state_restoration_data_.empty()) {
+    NSData* data = [NSData dataWithBytes:state_restoration_data_.data()
+                                  length:state_restoration_data_.size()];
+    NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+    [defaults setObject:data forKey:@"state_restoration_data"];
+  }
+}
 
 void NativeWindowMac::SetContentView(views::View* view) {
   views::View* root_view = GetContentsView();
@@ -1668,6 +1686,12 @@ void NativeWindowMac::NotifyWindowLeaveFullScreen() {
     [window_ setTitlebarAppearsTransparent:YES];
 }
 
+void NativeWindowMac::OnWindowStateRestorationDataChanged(
+    const std::vector<uint8_t>& data) {
+  LOG(INFO) << "OnWindowStateRestorationDataChanged";
+  state_restoration_data_ = data;
+}
+
 void NativeWindowMac::NotifyWindowWillEnterFullScreen() {
   UpdateVibrancyRadii(true);
 }

+ 3 - 1
shell/browser/ui/cocoa/electron_ns_window.h

@@ -29,9 +29,11 @@ class ScopedDisableResize {
 
 }  // namespace electron
 
-@interface ElectronNSWindow : NativeWidgetMacNSWindow {
+@interface ElectronNSWindow
+    : NativeWidgetMacNSWindow <NSKeyedArchiverDelegate> {
  @private
   raw_ptr<electron::NativeWindowMac> shell_;
+  BOOL _willUpdateRestorableState;
 }
 @property BOOL acceptsFirstMouse;
 @property BOOL enableLargerThanScreen;

+ 58 - 0
shell/browser/ui/cocoa/electron_ns_window.mm

@@ -4,6 +4,7 @@
 
 #include "shell/browser/ui/cocoa/electron_ns_window.h"
 
+#include "base/mac/mac_util.h"
 #include "base/strings/sys_string_conversions.h"
 #include "shell/browser/native_window_mac.h"
 #include "shell/browser/ui/cocoa/electron_preview_item.h"
@@ -23,6 +24,7 @@ int ScopedDisableResize::disable_resize_ = 0;
 @interface NSWindow (PrivateAPI)
 - (NSImage*)_cornerMask;
 - (int64_t)_resizeDirectionForMouseLocation:(CGPoint)location;
+- (BOOL)_isConsideredOpenForPersistentState;
 @end
 
 // See components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
@@ -186,8 +188,64 @@ void SwizzleSwipeWithEvent(NSView* view, SEL swiz_selector) {
     return nil;
 }
 
+// Called when the window is the delegate of the archiver passed to
+// |-encodeRestorableStateWithCoder:|, below. It prevents the archiver from
+// trying to encode the window or an NSView, say, to represent the first
+// responder. When AppKit calls |-encodeRestorableStateWithCoder:|, it
+// accomplishes the same thing by passing a custom coder.
+- (id)archiver:(NSKeyedArchiver*)archiver willEncodeObject:(id)object {
+  if (object == self)
+    return nil;
+  if ([object isKindOfClass:[NSView class]])
+    return nil;
+  return object;
+}
+
+- (void)saveRestorableState {
+  if (![self _isConsideredOpenForPersistentState])
+    return;
+
+  // On macOS 12+, create restorable state archives with secure encoding.
+  NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc]
+      initRequiringSecureCoding:base::mac::MacOSMajorVersion() >= 12];
+  encoder.delegate = self;
+  [self encodeRestorableStateWithCoder:encoder];
+  [encoder finishEncoding];
+  NSData* restorableStateData = encoder.encodedData;
+
+  auto* bytes = static_cast<uint8_t const*>(restorableStateData.bytes);
+  shell_->OnWindowStateRestorationDataChanged(
+      std::vector<uint8_t>(bytes, bytes + restorableStateData.length));
+
+  _willUpdateRestorableState = NO;
+}
+
+// AppKit calls -invalidateRestorableState when a property of the window which
+// affects its restorable state changes.
+- (void)invalidateRestorableState {
+  [super invalidateRestorableState];
+
+  if ([self _isConsideredOpenForPersistentState]) {
+    if (_willUpdateRestorableState)
+      return;
+    _willUpdateRestorableState = YES;
+    [self performSelectorOnMainThread:@selector(saveRestorableState)
+                           withObject:nil
+                        waitUntilDone:NO
+                                modes:@[ NSDefaultRunLoopMode ]];
+  } else if (_willUpdateRestorableState) {
+    _willUpdateRestorableState = NO;
+    [NSObject cancelPreviousPerformRequestsWithTarget:self];
+  }
+}
+
 // NSWindow overrides.
 
+- (void)dealloc {
+  _willUpdateRestorableState = YES;
+  [NSObject cancelPreviousPerformRequestsWithTarget:self];
+}
+
 - (void)rotateWithEvent:(NSEvent*)event {
   shell_->NotifyWindowRotateGesture(event.rotation);
 }

+ 1 - 0
shell/common/options_switches.cc

@@ -26,6 +26,7 @@ const char kMovable[] = "movable";
 const char kMinimizable[] = "minimizable";
 const char kMaximizable[] = "maximizable";
 const char kFullScreenable[] = "fullscreenable";
+const char kRestoreOnRestart[] = "restoreOnRestart";
 const char kClosable[] = "closable";
 const char kFullscreen[] = "fullscreen";
 const char kTrafficLightPosition[] = "trafficLightPosition";

+ 1 - 0
shell/common/options_switches.h

@@ -29,6 +29,7 @@ extern const char kMovable[];
 extern const char kMinimizable[];
 extern const char kMaximizable[];
 extern const char kFullScreenable[];
+extern const char kRestoreOnRestart[];
 extern const char kClosable[];
 extern const char kHiddenInMissionControl[];
 extern const char kFullscreen[];