Quartz Displayを使った NSWindow の表示画面変更

#Quartz Displayを使った画面制御
プロジェクタに投影することが前提のアプリとか、2画面使うのが前提でそれぞれ、どの画面にどの情報(Window)を出すかが決まっているケース、ありますよね。
そういう時、デバッグも視野に入れると、外部ディスプレイが繋がってる/繋がっていないに関わらず、そこそこ動いて欲しいはずです。

つまり、外部ディスプレイを接続した時には自動的に画面構成が切り替わって欲しい。そんな時に使える Tips です。

ここでは、1つのユーティリティクラスの実装をご紹介します。
このユーティリティクラスを使えば簡単に、好きな画面に好きなウィンドウを、その時の状況に応じて出力できます。

##まずはユーティリティクラスを作る
ちょっと月並みな名前ですが、`ScreenInfo`, `ScreenUtils`というクラスを宣言します。

```objc:ScreenUtils.h
@interface ScreenInfo : NSObject
@property (nonatomic, strong, readonly) NSString *uniqueID;
@property (nonatomic, assign, readonly) uint32_t modelNumber;
@property (nonatomic, assign, readonly) uint32_t vendorNumber;
@property (nonatomic, assign, readonly) uint32_t serialNumber;
@property (nonatomic, assign, readonly) CGRect screenRect;
- (void)moveWindow:(NSWindow *)targetWindow;
- (void)moveWindowFullScreen:(NSWindow *)targetWindow;
@end

@interface ScreenUtils : NSObject
- (ScreenInfo *)defaultScreen;
- (ScreenInfo *)screenWithUniqueID:(NSString *)uniqueID;
- (ScreenInfo *)screenWithIndex:(int)index;
- (ScreenInfo *)defaultScreenWithUniqueID:(NSString *)uniqueID;
- (ScreenInfo *)defaultScreenWithIndex:(int)index;
- (NSArray *)screenInfoArray;
@end
```

そして、その実装です。

```objc:ScreenUtils.m
@interface ScreenInfo () {
  CGDirectDisplayID displayID;
}
- (id)initWithID:(CGDirectDisplayID)directDisplayID rect:(CGRect)rect;
@end

@implementation ScreenInfo
- (id)initWithID:(CGDirectDisplayID)directDisplayID rect:(CGRect)rect {
  if ((self = [super init]) == nil)
    return nil;
  uint32_t v = CGDisplayVendorNumber(directDisplayID);
  uint32_t m = CGDisplayModelNumber(directDisplayID);
  uint32_t s = CGDisplaySerialNumber(directDisplayID);
  _uniqueID = [NSString stringWithFormat:@"%08x%08x", v, m];
  _vendorNumber = v;
  _modelNumber = m;
  _serialNumber = s;
  _screenRect = rect;
  displayID = directDisplayID;
  return self;
}

- (void)moveWindow:(NSWindow *)targetWindow {
  CGRect rect = NSRectToCGRect(targetWindow.frame);
  if (self.screenRect.size.width >= rect.size.width && self.screenRect.size.height >= rect.size.height)
    rect = NSMakeRect((self.screenRect.size.width - rect.size.width) / 2 + self.screenRect.origin.x, (self.screenRect.size.height - rect.size.height) / 2 + self.screenRect.origin.y, rect.size.width, rect.size.height);
  else
    rect = self.screenRect;
  dispatch_async(dispatch_get_main_queue(), ^{
    [targetWindow setFrame:NSRectFromCGRect(rect) display:YES animate:YES];
  });
}

- (void)moveWindowFullScreen:(NSWindow *)targetWindow {
  dispatch_async(dispatch_get_main_queue(), ^{
    [targetWindow setFrame:NSRectFromCGRect(self.screenRect) display:YES animate:YES];
	});
}

- (NSString *)description {
  return [NSString stringWithFormat:@"%@ v:%08x m:%08x s:%08x rect:%@", self.uniqueID, self.vendorNumber, self.modelNumber, self.serialNumber, NSStringFromRect(NSRectFromCGRect(self.screenRect))];
}
@end

@implementation ScreenUtils
- (NSArray *)screenInfoArray {
  @synchronized(self) {
    NSMutableArray *array = [NSMutableArray array];
    for (NSScreen *screen in [NSScreen screens]) {
      BOOL isScreen = [(NSNumber *)screen.deviceDescription[@"NSDeviceIsScreen"] boolValue];
      if (!isScreen)
        continue;
      CGDirectDisplayID displayID = (CGDirectDisplayID)[(NSNumber *)screen.deviceDescription[@"NSScreenNumber"] integerValue];
      ScreenInfo *info = [[ScreenInfo alloc] initWithID:displayID rect:NSRectFromCGRect(screen.frame)];
      [array addObject:info];
    }
    return array;
  }
}

- (ScreenInfo *)defaultScreen {
  NSScreen *screen = [NSScreen mainScreen];
  CGDirectDisplayID displayID = (CGDirectDisplayID)[(NSNumber *)screen.deviceDescription[@"NSDisplayID"] integerValue];
  return [[ScreenInfo alloc] initWithID:displayID rect:NSRectFromCGRect(screen.frame)];
}

- (ScreenInfo *)screenWithUniqueID:(NSString *)uniqueID {
  for (ScreenInfo *info in [self screenInfoArray])
    if ([info.uniqueID isEqual:uniqueID])
      return info;
  return nil;
}

- (ScreenInfo *)screenWithIndex:(int)index {
  NSArray *array = [self screenInfoArray];
  if (array.count >= index)
    return nil;
  return array[index];
}

- (ScreenInfo *)defaultScreenWithUniqueID:(NSString *)uniqueID {
  for (ScreenInfo *info in [self screenInfoArray])
    if ([info.uniqueID isEqual:uniqueID])
      return info;
  return [self defaultScreen];
}

- (ScreenInfo *)defaultScreenWithIndex:(int)index {
  NSArray *array = [self screenInfoArray];
  if (array.count <= index)
    return [self defaultScreen];
  return array[index];
}
@end
```

##画面を切り替える
次は、この `ScreenUtils` クラスを実際に使います。
`NSApplicationDelegate` クラスに `applicationDidChangeScreenParameters:` メソッドを実装しましょう。このメソッドは、スクリーン構成に変更があった際に呼び出されます。
このメソッド内で `ScreenUtils` インスタンスを作成し、すでに作った `moveWindow:` メソッドで希望の `NSWindow` の位置を変更します。

ここの例では、ウィンドウのインスタンスが予め `self.window` 変数に設定してあると仮定し、外部ディスプレイを繋いだ場合はそのディスプレイに表示、外部ディスプレイの接続を解除したらそのウィンドウをメインディスプレイに戻す、ということを自動で実施します。

```objc:AppDelegate.m
#import "ScreenUtils.h"

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
     :
  [self applicationDidChangeScreenParameters: aNotification]; //起動時に一回は呼び出す
     :
}

- (void)applicationDidChangeScreenParameters:(NSNotification *)notification {
	ScreenUtils *screenUtils = [[ScreenUtils alloc] init];
	ScreenInfo *screenInfo = [screenUtils defaultScreenWithIndex:1];
	[screenInfo moveWindow:self.window];  //self.windowは予め設定してある NSWindow と仮定
}
```