Xcodeのウィンドウを自動整列

![スクリーンショット 2021-12-03 14.35.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38013/fe7a129b-7f0e-0225-5a80-ac97a574939b.png)


## 開発効率とWindowレイアウト
- Xcodeなど統合開発環境を使う上で、複数ファイルを開きつつ同時に編集することは日常的によく起きることですが、みなさま効率的にファイル間を行き来できているでしょうか？
- 私はシングルウィンドウだと裏に回ったファイルと同時に参照しづらいなどの理由で、常にファイルを独立した複数のWindowで開くようにしています。
- よく見るファイルは隣同士に並べたり、クリックしてウインドウをフロントにもって来やすいように少しウィンドウの端を、他のウィンドウより少し横にはみ出させたり、細かなウィンドウレイアウトを知らず知らずによく行っていました。
- 20年くらい前はCodeWarriorという開発環境ではウィンドウレイアウトをいい感じに整列してくれるコマンドがありよく使っていたのですが、Xcodeにはそれがないため、フラストレーションが少しずつ蓄積していっていました。
- もちろんこの状態では開発効率もよくありません。

## いい感じに整列

- そこで、今回ウインドウをいい感じに整列するコマンドをAppleScriptで実現することにしました。
- いい感じに整列といっても、単に重ならないようにタイル状にレイアウトしただけです。
- ウインドウが2枚なら左右に、3枚でも横3枚を並べ、4枚なら上段下段それぞれ2枚という具合にタイル状に並ぶようにしました。

![ウィンドウ6枚を整列した様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38013/dd9616b2-bcaa-3a0c-c41a-fd523049485f.png)


## Xcodeのウィンドウレイアウトを変更するAppleScript
- 普段書かないAppleScriptに振り回されながらも、[Script Debugger](https://latenightsw.com/)という強力なアプリの存在を知り、デバッグしながら丸1日掛けてなんとか完成させました。
- 下記にそのまま掲載しておきます。[quickSortMulti](https://macscripter.net/viewtopic.php?pid=205924)はリストをソートする関数で[こちら](https://macscripter.net/viewtopic.php?pid=205924)を参考にさせていただきました。
- ソートしたドキュメントリストの順に左上から右に配列し、右いっぱいになったら下の段の左から並べるというコードになっています。
- AppleScriptはなぜか関数化すると実行がひどく遅くなったりするのでベタ書きとなっている部分があります。
- 参考にどうぞ

## 実際のコード
- 一応このQiita記事にコードを貼り付けますが、あまり更新できないかもしれません。
- 最新のコードは[こちら](https://github.com/shinyaohtani/AppleScriptForXcode/tree/main/Services/) (私のgithubレポジトリへのリンクです。)
  - Services/`Xcode Layout Window.applescript` が本記事のコードです。

```AppleScript

--
--	Created by: Shinya Ohtani
--	Created on: 2021/12/11
--
--	Copyright (C) 2021 Shinya Ohtani, All Rights Reserved
--

use AppleScript version "2.7" -- macOS 10.13 or later
use scripting additions

on run
	-- get default display size
	tell application "Finder"
		tell (do shell script "/usr/sbin/system_profiler SPDisplaysDataType -json | grep -B 5 '\"spdisplays_main\" : \"spdisplays_yes\"' | grep _spdisplays_resolution | cut -d'\"' -f4") to set {scW, scH} to {word 1, word 3}
	end tell
	
	tell application "Xcode-13.2"
		set windocs to getWinDocs() of me # {fname, ext, win, doc}
		set {rows, cols} to numRowsCols(number of windocs) of me
		
		-- Sort the window list
		set sortkey to {1, -2} -- 1st: filename w/o extention, 2nd extention only (".h", ".cpp"). 
		quickSortMulti(windocs, sortkey) of me
		
		set spT to 25 -- top side space (menu bar height)
		set spL to 10
		set spR to 40
		set spB to 20
		set stepRow to round of ((scH - spT - spB) / rows)
		set stepColumn to round of ((scW - spL - spR) / cols)
		
		-- Leave space between each window where we can
		-- click on another window behind it.
		set spRowB to 10 -- distance between two neighbors (shift bottom to up)
		set spColumnR to 10 -- distance between two neighbors (shift right to left)
		
		set row to 0
		set col to 0
		repeat with wd in windocs
			set {fname, ext, win, doc} to wd -- we use only win (= item 3 of wd)
			set L to spL + (stepColumn * col)
			set T to spT + (stepRow * row)
			set R to L + stepColumn - spColumnR
			set B to T + stepRow - spRowB
			if col is equal to cols - 1 then -- This is to recover double space.
				set R to R + spColumnR
			end if
			if row is equal to rows - 1 then -- This is to recover double space.
				set B to B + spRowB
			end if
			if number of windocs is 1 then -- if single window, then move to center.
				set L to L + (stepColumn / 2)
				set R to R + (stepColumn / 2)
			end if
			
			set bounds of win to {L, T, R, B}
			set index of win to 1
			set col to col + 1
			if col ≥ cols then
				set col to 0
				set row to row + 1
			end if
		end repeat
		
	end tell
	
end run

on fnameNthOfWname(xcode_ver)
	# Before xcode 13.2, the win names were simple. Project names were not included.
	# before 13.2: "myFile.h"
	#    or if it is edited and not saved "myFile.h -- edited"
	# from 13.2: "Projname -- myFile.h" or "Projname -- myFile.h -- edited"
	
	set nth to 2
	
	set tmp to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "."
	set v to (xcode_ver as string)
	if number of (text items of v) ≥ 3 then # care sub-sub... version. ex 13.2.1
		set v to ({(text item 1 of v) as string, (text item 2 of v) as string} as string)
	end if
	if (v as number) is less than 13.2 then # Before xcode 13.2, the win names were simple.
		set nth to 1
	end if
	set AppleScript's text item delimiters to tmp
	return nth
end fnameNthOfWname

on getWinDocs() # get the source window list of Xcode.
	set windocs to {}
	tell application "Xcode-13.2"
		activate
		delay 0.1
		
		set allDocs to (every source document)
		set allWins to ((every window) whose (id is not -1 and visible is true))
		-- Refine this list of windows to include only those windows
		-- for which the corresponding document type is "source document".
		set wID to fnameNthOfWname(version) of me
		
		set tmp to AppleScript's text item delimiters
		set AppleScript's text item delimiters to "."
		repeat with win in allWins
			if document of win is not missing value then
				set wname to word wID of (name of win as text)
				repeat with doc in allDocs
					if name of doc is equal to wname then
						-- Mix in a sort key for later sorting.
						set end of windocs to {text item 1 of wname, text item 2 of wname, win, doc}
						exit repeat
					end if
				end repeat
			end if
		end repeat
		set AppleScript's text item delimiters to tmp
		if number of windocs is 0 then
			return
		end if
	end tell
	return windocs
end getWinDocs

on numRowsCols(numOfWindows)
	-- Decide on the number of windows to line up horizontally.
	if numOfWindows ≤ 9 then -- Even if only one window, set to 2 because the right margin is wasted
		set cols to item (numOfWindows) of {2, 2, 3, 2, 3, 3, 4, 4, 3}
	else
		set cols to 4
	end if
	-- The number of rows is calculated from the number of columns.
	set rows to (numOfWindows) div cols
	if (numOfWindows) mod cols is not equal to 0 then
		set rows to rows + 1
	end if
	return {rows, cols}
end numRowsCols

-- For the following, please refer to https://macscripter.net/viewtopic.php?pid=205924
on quickSortMulti(alist, sortList) -- Non-Recursive FASTEST
	local px, lo, hi, j, L, H, sw, c, comp -- px means 'Pivot Index'
	script mL
		property nlist : alist
		property sList : {}
		property oList : {}
		property stack : {}
	end script
	repeat with j in sortList
		if j > 0 then -- if positive, sort ascending
			set end of mL's sList to (contents of j)
		else -- if negative,sort descending
			set end of mL's sList to -(contents of j)
		end if
		set end of mL's oList to (j > 0)
	end repeat
	set end of mL's stack to {1, count of mL's nlist}
	repeat until (count of mL's stack) = 0 --sc
		set lo to item 1 of item 1 of mL's stack
		set hi to item 2 of item 1 of mL's stack
		-- partitionHoare
		set px to item ((hi + lo) div 2) of mL's nlist
		set L to lo
		set H to hi
		repeat
			set comp to true
			repeat while comp
				repeat with j from 1 to count of mL's sList -- do multiple comparisons
					set c to item j of mL's sList
					set comp to false
					if item c of item L of mL's nlist < item c of px then
						if item j of mL's oList then set comp to true -- ascending
						exit repeat
					else if item c of item L of mL's nlist > item c of px then
						if not (item j of mL's oList) then set comp to true --descending
						exit repeat
					end if
				end repeat
				if comp then set L to L + 1
			end repeat
			
			set comp to true
			repeat while comp
				repeat with j from 1 to count of mL's sList -- do multiple comparisons
					set c to item j of mL's sList
					set comp to false
					if item c of item H of mL's nlist > item c of px then
						if item j of mL's oList then set comp to true -- ascending
						exit repeat
					else if item c of item H of mL's nlist < item c of px then
						if not (item j of mL's oList) then set comp to true --descending
						exit repeat
					end if
				end repeat
				if comp then set H to H - 1
			end repeat
			
			if L ≥ H then
				exit repeat
			end if
			set sw to item L of mL's nlist
			set item L of mL's nlist to item H of mL's nlist
			set item H of mL's nlist to sw
			set L to L + 1
			set H to H - 1
		end repeat
		set px to H -- end of partitionHoare
		set mL's stack to rest of mL's stack
		if px + 1 < hi then
			set beginning of mL's stack to {px + 1, hi}
		end if
		if lo < px then
			set beginning of mL's stack to {lo, px}
		end if
	end repeat
end quickSortMulti

```

## キーボードショートカットで簡単にウィンドウ整列
- このコードをAutomatorのAppleScriptコンポーネントにコピペし、
- ~/Library/Service/にAutomatorのクイックアクション書類(*.workflow)として保存し、
- 一度実行して出てくるセキュリティ警告に対応しつつ、
- macOSの設定＞キーボード＞ショートカット＞サービス＞一般を開き、
- そこに追加されている実行したばかりのAutomator書類にお好きなショートカット(⌘^など)を設定し、
- XcodeのPreferences...>Key bindingsを開き、
- 割り当てたショートカットのコンフリクトを解消する。
- するとキーボードからショートカットとして簡単に起動できるようになります。
- もしできなかったら、macOSのシステム環境設定＞セキュリティとプライバシー＞プライバシー＞アクセシビリティに、今回のクイックアクション書類(*.workflow)をドラッグ&ドロップするとうまくいくようになる。
- Xcode 13.2, macOS 11.6.2で動作確認済み
  - ![スクリーンショット 2022-01-05 16.15.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38013/daddd1b6-8e50-d83b-f800-c5302118db6d.png)
  - ![スクリーンショット 2022-01-05 16.13.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38013/1320bde8-a8a7-eedc-52e9-b50b9a9e605c.png)