〔Mac〕 ShiftItに独自のウィンドウ配置パターンを追加する

# はじめに

前回『[〔Mac〕 ShiftItのメニューをビジュアル化する](https://qiita.com/nak435/items/b8a604716e5609c52af1)』にて、[ShiftIt](https://github.com/fikovnik/ShiftIt/blob/master/README.md)のメニューにアイコンを付加してビジュアル化する方法を記事にしました。
今回は、さらに踏み込んで、独自の配置パターンを追加する方法の記事です。

# 追加する配置パターン

$今回は、左\frac{2}{3} と 右\frac{2}{3}$の配置パターンを追加します。具体的には、次の6パターン。（配置パターンの例として説明します。何個でもどんな配置パターンでも追加できます）

![lefttwothirdstop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/1039becb-8a88-f992-82a5-5894bef5033d.png) ![lefttwothirdsbottom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/462b7284-1f5c-7e81-2418-124697d565e8.png) ![rightwothirdstop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/a4e83907-2ec2-5710-9619-158d1f8b046f.png) ![rightwothirdsbottom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/a3730409-a579-91f8-3be6-669d2f3e71a9.png) 
![lefttwothirds.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/0c15f2cd-0db7-0732-f805-82eb1b590fe5.png) ![righttwothirds.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/61f1b51d-9fe3-a6b0-9c80-b157ed0729e4.png) 

さすがに、プロパティ設定だけでは実現できず、ソースコードに手を入れることになります。合計6(7)ファイルに変更を加えますので、順を追って説明します。

- 1) MeinMenu.xib
- 2) DefaultShiftItActions.m
- 3) DefaultShiftItActions.h
- 4) ShiftItAppDelegate.m
- 5) PreferencesWindowController.m
- 6) ShiftIt-defaults.plist
- 7) Localizable.strings（メニューを日本語化する場合のみ必要）

_コードはObjective-Cで書かれていますが、言語知識はそれほどなくても十分対応可能だと思います。_


# 1) メニュー項目の追加

最初はメニュー項目の追加です。
`MeinMenu.xib`を開き、上記の6つの配置パターンの`MenuItem`を追加します。追加したイメージは下図の通り。

![menuitem.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/a0d5df92-ea91-aab1-a743-63936c8290c2.png)

メニュー項目の並びのどこに追加しても構いませんが、追加したMenuItemの`tag`に2026以降の数値を設定する必要があります（元々のMenuItemが2001〜2025まで使用している）。



# 2) 配置関数の追加

メニュー項目のクリックもしくはショートカットキーにより呼び出される、ウィンドウ配置関数（処理コード）を追加します。
`DefaultShiftItActions.m`を開き、340行目辺りに以下のコードを追加します。

```swift:DefaultShiftItActions.m#340
const SimpleWindowGeometryChangeBlock shiftItTwoThirdsTopLeft = ^AnchoredRect(NSRect windowRect, NSSize screenSize) {
    NSRect r = NSMakeRect(0, 0, 0, 0);
    
    r.origin.x = 0;
    r.origin.y = 0;
    
    r.size.width = (screenSize.width / 3) * 2;
    r.size.height = screenSize.height / 2;
    
    return MakeAnchoredRect(r, kTopDirection | kLeftDirection);
};

const SimpleWindowGeometryChangeBlock shiftItTwoThirdsBottomLeft = ^AnchoredRect(NSRect windowRect, NSSize screenSize) {
    NSRect r = NSMakeRect(0, 0, 0, 0);
    
    r.origin.x = 0;
    r.origin.y = screenSize.height / 2;
    
    r.size.width = (screenSize.width / 3) * 2;
    r.size.height = screenSize.height / 2;
    
    return MakeAnchoredRect(r, kBottomDirection | kLeftDirection);
};

const SimpleWindowGeometryChangeBlock shiftItTwoThirdsTopRight = ^AnchoredRect(NSRect windowRect, NSSize screenSize) {
    NSRect r = NSMakeRect(0, 0, 0, 0);
    
    r.origin.x = screenSize.width - ((screenSize.width / 3) * 2);
    r.origin.y = 0;
    
    r.size.width = (screenSize.width / 3) * 2;
    r.size.height = screenSize.height / 2;
    
    return MakeAnchoredRect(r, kTopDirection);
};

const SimpleWindowGeometryChangeBlock shiftItTwoThirdBottomRight = ^AnchoredRect(NSRect windowRect, NSSize screenSize) {
    NSRect r = NSMakeRect(0, 0, 0, 0);
    
    r.origin.x = screenSize.width - ((screenSize.width / 3) * 2);
    r.origin.y = screenSize.height / 2;
    
    r.size.width = (screenSize.width / 3) * 2;
    r.size.height = screenSize.height / 2;
    
    return MakeAnchoredRect(r, kBottomDirection);
};

const SimpleWindowGeometryChangeBlock shiftItTwoThirdsLeft = ^AnchoredRect(NSRect windowRect, NSSize screenSize) {
    NSRect r = NSMakeRect(0, 0, 0, 0);
    
    r.origin.x = 0;
    r.origin.y = 0;
    
    r.size.width = (screenSize.width / 3) * 2;
    r.size.height = screenSize.height;
    
    return MakeAnchoredRect(r, kLeftDirection);
};

const SimpleWindowGeometryChangeBlock shiftItTwoThirdsRight = ^AnchoredRect(NSRect windowRect, NSSize screenSize) {
    NSRect r = NSMakeRect(0, 0, 0, 0);
    
    r.origin.x = screenSize.width - ((screenSize.width / 3) * 2);
    r.origin.y = 0;
    
    r.size.width = (screenSize.width / 3) * 2;
    r.size.height = screenSize.height;
    
    return MakeAnchoredRect(r, kTopDirection);
};
```

ここは、`SimpleWindowGeometryChangeBlock型`関数の引数`windowRect`と`screenSize`が重要で、`windowRect`が配置されるウィンドウの**矩形**`NSRect型`、`screenSize`はそのウィンドウがあるスクリーン（ディスプレイ）の**サイズ**`NSSize型`が渡されます。これを入力として、目的とする配置になるように計算して、新しい矩形を返すコードを書くことです。

具体的に、メニュー項目（`Left Two Thirds Top`）の場合を例にすると、
![lefttwothirdstop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/1039becb-8a88-f992-82a5-5894bef5033d.png)
左上の赤い矩形の位置になるので、`NSRect r`は、以下の通りとなります。

- r.origin.x = 0　　//左上
- r.origin.y = 0　　//左上
- r.size.width = (screenSize.width / 3) * 2　　//スクリーンの横幅の3分の2
- r.size.height = screenSize.height / 2　　　　//スクリーンの高さの2分の1

このように何分割にでも可能ですし、また、絶対座標・絶対サイズとすることも可能です。お好みの配置パターンを設定すればよいのです。

_**座標系ですが、macOS標準のスクリーン左下が(0, 0)ではなく、左上が(0, 0)としてインタフェースされます。**個人的にもこの方が扱い易いです。_


# 3) 配置関数のextern宣言

上記で追加した配置関数のextern宣言を追加します。
`DefaultShiftItActions.h`を開き、46行目辺りに以下のコードを追加します。

```swift:DefaultShiftItActions.h#46
const extern SimpleWindowGeometryChangeBlock shiftItTwoThirdsTopLeft;
const extern SimpleWindowGeometryChangeBlock shiftItTwoThirdsBottomLeft;
const extern SimpleWindowGeometryChangeBlock shiftItTwoThirdsTopRight;
const extern SimpleWindowGeometryChangeBlock shiftItTwoThirdBottomRight;
const extern SimpleWindowGeometryChangeBlock shiftItTwoThirdsLeft;
const extern SimpleWindowGeometryChangeBlock shiftItTwoThirdsRight;
```


# 4) メニューから呼び出されるアクションの登録

メニュー項目のクリックもしくはショートカットキーにより呼び出されるアクションを登録します。
`ShiftItAppDelegate.m`を開き、604行目辺りに以下のコードを追加します。

```swift:ShiftItAppDelegate.m#604
    REGISTER_ACTION(dict, @"l2tt", NSLocalizedString(@"Left Two Thirds Top", nil), 26, [[[WindowGeometryShiftItAction alloc] initWithBlock:shiftItTwoThirdsTopLeft] autorelease]);
    REGISTER_ACTION(dict, @"l2tb", NSLocalizedString(@"Left Two Thirds Bottom", nil), 27, [[[WindowGeometryShiftItAction alloc] initWithBlock:shiftItTwoThirdsBottomLeft] autorelease]);
    REGISTER_ACTION(dict, @"r2tt", NSLocalizedString(@"Right Two Thirds Top", nil), 28, [[[WindowGeometryShiftItAction alloc] initWithBlock:shiftItTwoThirdsTopRight] autorelease]);
    REGISTER_ACTION(dict, @"r2tb", NSLocalizedString(@"Right Two Thirds Bottom", nil), 29, [[[WindowGeometryShiftItAction alloc] initWithBlock:shiftItTwoThirdBottomRight] autorelease]);
    REGISTER_ACTION(dict, @"l2t", NSLocalizedString(@"Left Two Thirds", nil), 30, [[[WindowGeometryShiftItAction alloc] initWithBlock:shiftItTwoThirdsLeft] autorelease]);
    REGISTER_ACTION(dict, @"r2t", NSLocalizedString(@"Right Two Thirds", nil), 31, [[[WindowGeometryShiftItAction alloc] initWithBlock:shiftItTwoThirdsRight] autorelease]);
```

指定する内容で重要なのは赤字の4箇所で、以下の通りです。

<b>REGISTER_ACTION(dict, <font color=red>@"VVV"</font>, NSLocalizedString(<font color=red>@"XXX"</font>, nil), <font color=red>YYY</font>, [[[WindowGeometryShiftItAction alloc] initWithBlock:<font color=red>ZZZ</font>] autorelease]);</b>

- <font color=red>VVV</font>：識別子（他と被らなければ何でもよい）
- <font color=red>XXX</font>：メニュー項目に表示するローカライズ文字列
- <font color=red>YYY</font>：MenuItemの追加で指定したtagから2000を引いた数値
- <font color=red>ZZZ</font>：『[3) 配置関数のextern宣言](#3-配置関数のextern宣言)』で追加した配置関数名

を指定します。


# 5) ショートカットキーの指定

次にShiftItの環境設定画面の`ショートカットキー`タブの表示に関する`hotkeyIdentifiers配列`に追加します。
`PreferencesWindowController.m`を開き、200行目辺りに以下のコードを追加します。（**hotkeyIdentifiers**配列の途中に追加するので、diff形式で示します。）

```diff_swift
static NSString *hotkeyIdentifiers[] = {
    @"left",
    @"right",
    @"top",
    @"bottom",
    NULL,
    @"tl",
    @"tr",
    @"bl",
    @"br",
    NULL,
    @"ltt",
    @"ltb",
    @"ctt",
    @"ctb",
    @"rtt",
    @"rtb",
    NULL,
+   @"l2tt",
+   @"l2tb",
+   @"r2tt",
+   @"r2tb",
+   NULL,
    @"lt",
    @"ct",
    @"rt",
    NULL,
+   @"l2t",
+   @"r2t",
+   NULL,
    @"center",
    @"zoom",
    @"maximize",
    @"fullScreen",
    NULL,
    @"increase",
    @"reduce",
    NULL,
    @"nextscreen",
    @"previousscreen"
};
```

重要なのは、『[1) メニュー項目の追加](#1-メニュー項目の追加)』で追加したメニュー項目の並びと区切り線も含め**一致させる**こと、および、『[4) メニューから呼び出されるアクションの登録](#4-メニューから呼び出されるアクションの登録)』で追加したアクション登録文の『<font color=red>VVV</font>：識別子』と**一致させること**です。

すると、下記のように表示されるようになります。
![shortcut.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/8d6c6569-abcd-8ffa-49ea-6f0db7d8e8fc.png)



# 6) ショートカットキーのデフォルト値を追加

次にショートカットキーのデフォルト値を追加します。環境設定画面の`ショートカットキー`タブにも表示されます（MenuItemで指定したキーバインド(KeyEquivalent)は、この設定値に置き換わります）。
`ShiftIt-defaults.plist`を開き、153行目辺りに以下のコード(XML)を追加します。


```xml:ShiftIt-defaults.plist#153
	<key>l2ttKeyCode</key>
	<integer>16</integer>
	<key>l2ttModifiers</key>
	<integer>1835008</integer>
	<key>l2tbKeyCode</key>
	<integer>4</integer>
	<key>l2tbModifiers</key>
	<integer>1835008</integer>
	<key>r2ttKeyCode</key>
	<integer>35</integer>
	<key>r2ttModifiers</key>
	<integer>1835008</integer>
	<key>r2tbKeyCode</key>
	<integer>41</integer>
	<key>r2tbModifiers</key>
	<integer>1835008</integer>
	<key>l2tKeyCode</key>
	<integer>0</integer>
	<key>l2tModifiers</key>
	<integer>1835008</integer>
	<key>r2tKeyCode</key>
	<integer>2</integer>
	<key>r2tModifiers</key>
	<integer>1835008</integer>
```

これは、一つのメニュー項目に対し、2組のキーバリューペアを指定します。下記のように、1組目がショートカットキーに指定する**修飾キー**、2組目が**キーコード**です。

<b>&lt;key&gt;<font color=red>VVV</font>Modifiers&lt;/key&gt;	&lt;integer&gt;<font color=red>MMM</font>&lt;/integer&gt;
&lt;key&gt;<font color=red>VVV</font>KeyCode&lt;/key&gt; &lt;integer&gt;<font color=red>KKK</font>&lt;/integer&gt;</b>

指定する内容で重要なのは赤字の4箇所で、以下の通り指定します。

- <font color=red>VVV</font>：『[4) メニューから呼び出されるアクションの登録](#4-メニューから呼び出されるアクションの登録)』で指定した『<font color=red>VVV</font>：識別子』
- <font color=red>MMM</font>：ショートカットキーに使用する**修飾キー**（NSEventModifierFlags型の値）
- <font color=red>KKK</font>：ショートカットキーに使用する**キーコード**値（cf.[CGKeyCode(US配列)](https://qiita.com/Kyome/items/33143418d061fd7ec7d6#cgkeycodeus配列)）

ちなみに、修飾キー:**1835008**(=0x1C0000)とキーコード:**0**で、`⌃⌥⌘A`（control＋option＋command＋A）を意味します。

_修飾キーとキーコードは10進数で指定します。_


# 7) メニューの日本語化（任意）

最後に、メニュー項目を日本語化したい場合は、ローカライズ文字列を追加します。
`Localizable.strings(Japanese)`を開き、メニュー項目`Title`に対応する日本語文字列を追加します。

```text:Localizable.strings(Japanese)
"Left Two Thirds Top"     = "左上3分の2";
"Left Two Thirds Bottom"  = "左下3分の2";
"Right Two Thirds Top"    = "右上3分の2";
"Right Two Thirds Bottom" = "右下3分の2";
"Left Two Thirds"         = "左3分の2";
"Right Two Thirds"        = "右3分の2";
```

左側のキーワードは、『[4) メニューから呼び出されるアクションの登録](#4-メニューから呼び出されるアクションの登録)』で指定した、『<font color=red>XXX</font>：ローカライズ文字列』と一致させる必要があります。

メニュー項目を日本語化しないなら、このファイルの変更は不要です。



以上で改修作業は終わり。修正ミスがないことを確認したら、ビルドして動作確認します。

# おわりに

どうでしょうか？、お好みの配置パターンが無事に追加できたでしょうか？
何かの参考になれば幸いです。


## 追伸

Xcodeでデバック実行すると、毎回、アクセシビリティ（システム環境設定**→**セキュリティとプライバシ**→**アクセシビリティ）の再設定が必要で面倒。
パスは同じなのになんでビルドする毎に再設定が必要なのだろうか？ 設定したタイムスタンプと、Appが作成されたタイムスタンプを厳密に比較しているのだろうか？
![security.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/9c15b6cc-a647-63eb-4418-018a3573da0b.png)


<br>

**以上**です。