Hotkey driven window tiling scripts

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Hotkey driven window tiling scripts

10 Jun 2016, 20:27

This code is the result of some tinkering around, but the results seem pretty good.
It replaces the normal Win+Arrow keys with a system that allows you to tile windows into quarters or halves on each axis.
You can also still move windows between monitors using these keys, but you have to "walk" it (ie full width at top > right hotkey > quarter with top right > right hotkey > quarter width top left of next monitor > right hotkey > finally full width on next monitor)
However, a further hotkey combo (Win+ALT+Arrow) can be used to move the window to the next monitor and keep the same tiling layout.
Also, if you have multiple windows in the same Tile configuration on a monitor (eg a stack of window all tiled on the right side of a monitor), then you can cycle between them with Win+Mouse Wheel.

I might see if I can add a hotkey to switch the active window with direction keys, though I am not too sure what the best logic would be.

Code: Select all

/*
Window Tiling system
Takes the default windows (Win+Arrow) hotkeys and makes them a bit better
Adds the ability to Tile windows into quarters / halves on both axes
Win+Alt+Arrow can be used to force a monitor change
Also adds a Win+Wheel hotkey to cycle between windows in the same Tile
*/
#SingleInstance force

new TileEngine()

Class TileEngine {
	MonitorCount := 0
	WindowCount := 0
	MonitorPrimary := 0
	monitors := []
	windows := {}
	HwndList := []	; windows array is sparse. this is a lookup table that can be iterated forwards or backwards
	
	__New(){
		static DirectionKeys := {x: {-1: "#Left", 1: "#Right"}, y: {-1: "#Up", 1: "#Down"}}
		static CycleKeys := {-1: "#WheelUp", 1: "#WheelDown"}
		static ModeModifier := "!"
		
		; Query monitor info
		SysGet, MonitorCount, MonitorCount
		this.MonitorCount := MonitorCount
		SysGet, MonitorPrimary, MonitorPrimary
		this.MonitorPrimary := MonitorPrimary
		
		; Create Monitor objects
		Loop % this.MonitorCount {
			this.monitors.push(new Monitor(A_Index))
		}
		
		; Bind Direction Keys to WindowSizeMove
		for axis, hks in DirectionKeys {
			for dir, hk in hks {
				fn := this.WindowSizeMove.Bind(this, 0, axis, dir)
				hotkey, % hk, % fn
				if (ModeModifier){
					fn := this.WindowSizeMove.Bind(this, 1, axis, dir)
					hotkey, % ModeModifier hk, % fn
				}
			}
		}
		
		; Bind Cycle Keys to cycle between windows in the same tile
		for dir, hk in CycleKeys {
			fn := this.TileCycle.Bind(this, dir)
			hotkey, % hk, % fn
		}
	}
	
	; Moves a window around
	; Mode 0 = resize / move
	; Mode 1 = move monitor
	WindowSizeMove(mode, axis, dir, hwnd := 0){
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		if (this.RegisterNewWindow(hwnd)){
			; Window is new to the tiling system - fill half of monitor in direction requested
			this.SetInitialWindowPosition(this.windows[hwnd], axis, dir)
			return
		}
		window := this.windows[hwnd]
		if (mode){
			; Move mode
			if (axis == "y")
				return	; move on y axis not supported
			window.monitor := this.WrapMonitor(window.monitor + dir)
		} else{
			; Resize / Move mode
			if (window.Alignments[axis] == dir){
				; The window is already against an edge and the move direction is towards the edge
				if (axis == "y")
					return	; move on y axis not supported
				; move to next monitor
				window.monitor := this.WrapMonitor(window.monitor + dir)
				; Flip layout to opposite edge
				window.Alignments[axis] *= -1
			} else {
				; Move the layout towards the direction.
				; If the x axis is -1 (on the left) and we move right (+1), it becomes 0 (spanned)
				window.Alignments[axis] := this.ClampDirection(window.Alignments[axis] + dir)
			}
		}
		this.TileWindow(window)
	}

	; Cycles between items in a tile, in hwnd order (forwards or backwards)
	TileCycle(dir){
		hwnd := this.GetActiveHwnd()
		if (!ObjHasKey(this.windows, hwnd))
			return
		window := this.windows[hwnd]
		i := window.order
		Loop % this.WindowCount - 1 {
			i += dir
			if (i < 1)
				i := this.WindowCount
			else if (i > this.WindowCount)
				i := 1
			nw := this.windows[this.HwndList[i]]
			if (nw.monitor == window.monitor && nw.Alignments.x == window.Alignments.x && nw.Alignments.y == window.Alignments.y){
				WinActivate, % "ahk_id " nw.hwnd
				break
			}
		}
	}
	
	; Allows a direction to be applied to a direction
	ClampDirection(dir){
		if dir > 1
			return 1
		if dir < -1
			return -1
		return dir
	}
	
	; Allows a direction to be applied to a monitor and have it wrap around
	; Obviously only works for monitors all lined up left to right in ascending numbers
	WrapMonitor(mon){
		if (mon > this.MonitorCount) {
			return 1
		} else if (mon < 1) {
			return this.MonitorCount
		}
		return mon
	}
	
	; Decides where a window should be placed for the first time
	SetInitialWindowPosition(window, axis, dir){
		if (axis == "x"){
			window.Alignments.x := dir
		} else if (axis == "y"){
			window.Alignments.y := dir
		}
		window.monitor := this.GetWindowMonitor(window)
		this.TileWindow(window)
	}
	
	; Actually does the moving of a window
	; Set the parameters of the window object how you want it, then pass it to this function
	TileWindow(window){
		static divisors := {-1: 2, 0: 1, 1: 2}
		;window.monitor.cx
		monitor := this.monitors[window.monitor].coords

		w := round(monitor.w / divisors[window.Alignments.x])
		h := round(monitor.h / divisors[window.Alignments.y])
		; if Alignments.x is 1 then set position to center x, else left x
		x := window.Alignments.x == 1 ? monitor.cx : monitor.l
		y := window.Alignments.y == 1 ? monitor.cy : monitor.t
		WinMove, % "ahk_id " window.hwnd,, x, y, w, h
	}
	
	; Returns the monitor number that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
	
	; Add a window to the tiling system
	; Returns 0 if window is already in the tiling system
	RegisterNewWindow(hwnd){
		if (ObjHasKey(this.windows, hwnd)){
			; Window has been placed by the tiling system
			return 0
		} else {
			; Window is new to the tiling system
			this.windows[hwnd] := new Window(hwnd)
			this.BuildWindowList()
			return 1
		}
	}
	
	; Builds list of windows in hwnd order, so they can be cycled through in either direction
	BuildWindowList(){
		this.HwndList := []
		ctr := 0
		for hwnd, window in this.windows {
			this.HwndList.push(hwnd)
			ctr++
			window.order := ctr
		}
		this.WindowCount := ctr
	}
	
	GetActiveHwnd(){
		WinGet, hwnd, ID, A
		return hwnd+0
	}
}

; Represents a window that has been positioned by the tiling system
Class Window {
	hwnd := 0
	order := 0		; Order in the HwndList
	monitor := 0	; Which monitor the window is on
	; Alignments for a given axis is 0 when it spans that axis...
	; ... -1 when it half-width and on the left/upper side (towards lower pixel numbers) ...
	; ... and +1 when on the right / bottom side (towards higher pixel numbers)
	Alignments := {x: 0, y: 0}
	
	__New(hwnd){
		this.hwnd := hwnd
	}
	
	; Gets the coordinates of the center of the window
	GetCenter(){
		WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
		cx := wx + round(ww / 2)
		cy := wy + round(wh / 2)
		return {x: cx, y: cy}
	}
}

; Represents a monitor that windows can reside on
Class Monitor {
	id := 0
	coords := 0
	
	__New(id){
		this.id := id
		this.coords := this.GetWorkArea()
	}
	
	; Gets the "Work Area" of a monito (The coordinates of the desktop on that monitor minus the taskbar)
	; also pre-calculates a few values derived from the coordinates
	GetWorkArea(){
		SysGet, coords_, MonitorWorkArea, % this.id
		out := {}
		out.l := coords_left
		out.r := coords_right
		out.t := coords_top
		out.b := coords_bottom
		out.w := coords_right - coords_left
		out.h := coords_bottom - coords_top
		out.cx := coords_left + round(out.w / 2)	; center x
		out.cy := coords_top + round(out.h / 2)		; center y
		out.hw := round(out.w / 2)	; half width
		out.hh := round(out.w / 2)	 ; half height
		return out
	}
}
Last edited by evilC on 25 Jun 2017, 08:46, edited 3 times in total.
guest3456
Posts: 3463
Joined: 09 Oct 2013, 10:31

Re: Win+Arrow Window Tiling System

12 Jun 2016, 14:36

evilC wrote:

Code: Select all

	HwndList := []	; windows array is sparse. this is a lookup table that can be iterated forwards or backwards
what do you mean by this?

User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

13 Jun 2016, 04:11

If HwndList is {1: "a", 3: "b", 5: "c"} then you can iterate through this forwards with no problem using for k, v in HwndList.
However, I do not see how to iterate this backwards.
So what I do is to build a lookup array: [1,3,5] so that the list can be iterated backwards.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

13 Jun 2016, 15:07

Here is what has changed:

Major bug fixed:
I was using the class name window as well as a variable of the same name inside methods. What a dummy. Class names are now prefixed with a C.
JSON library added for save / load of settings. Currently only saves windows which have been tiled - mainly so when I am writing it, I do not have to re-set everything up each run.

Move (Was ALT) is now on Shift
Ctrl is now a modifier for "Navigate" - it activates the window in the requested direction.
The code is very experimental, I am sure there is a better way of doing it, and I have a thread about that algorithm here.

Placeholder code in for "Bias detection". If in a spanned window (eg a "West" window spanned on the left edge of a monitor) and there are two quarter-size windows on the right (NorthEast / SouthEast), which one should it pick? This code detects the cursor (ie text insertion caret) position and uses that to decide.
Future versions may use the mouse position as well, if it moved recently (But for now I wanna keep timers / callbacks out to make debugging easier).

There is also disabled code in there for detecting when a new window comes into existence. If I can get all this working, I would like to be able to set rules for new windows (eg "New browsers go on monitor 3, spanned at top")

Code: Select all

/*
Window Tiling system
Takes the default windows (Win+Arrow) hotkeys and makes them a bit better
Adds the ability to Tile windows into quarters / halves on both axes
Win+Alt+Arrow can be used to force a monitor change
Also adds a Win+Wheel hotkey to cycle between windows in the same Tile
*/
#SingleInstance force
new TileEngine()

Class TileEngine {
	MonitorCount := 0
	WindowCount := 0
	MonitorPrimary := 0
	monitors := []
	windows := {}
	HwndList := []	; windows array is sparse. this is a lookup table that can be iterated forwards or backwards
	
	__New(){
		static DirectionKeys := {x: {-1: "#Left", 1: "#Right"}, y: {-1: "#Up", 1: "#Down"}}
		static CycleKeys := {-1: "#WheelUp", 1: "#WheelDown"}
		static MonitorModifier := "+"
		static SwitchModifier := "^"
		
		CoordMode, Caret, Screen
		CoordMode, Mouse, Screen
		
		Gui +Hwndhwnd
		this.hwnd := hwnd
		DllCall( "RegisterShellHookWindow", UInt,hWnd )
		MsgNum := DllCall( "RegisterWindowMessage", Str,"SHELLHOOK" )
		fn := this.ShellMessage.Bind(this)
		;OnMessage( MsgNum, fn )

		this.IniFile := RegexReplace(A_ScriptName, "(.ahk|.exe)$", ".ini")
		
		; Query monitor info
		SysGet, MonitorCount, MonitorCount
		this.MonitorCount := MonitorCount
		SysGet, MonitorPrimary, MonitorPrimary
		this.MonitorPrimary := MonitorPrimary
		
		; Create Monitor objects
		Loop % this.MonitorCount {
			this.monitors.push(new CMonitor(A_Index))
		}
		
		; Bind Direction Keys to WindowSizeMove
		for axis, hks in DirectionKeys {
			for dir, hk in hks {
				fn := this.WindowSizeMove.Bind(this, 0, axis, dir)
				hotkey, % hk, % fn
				if (MonitorModifier){
					fn := this.WindowSizeMove.Bind(this, 1, axis, dir)
					hotkey, % MonitorModifier hk, % fn
				}
				if (SwitchModifier){
					fn := this.TileSwitch.Bind(this, axis, dir)
					hotkey, % SwitchModifier hk, % fn
				}
			}
		}
		
		; Bind Cycle Keys to cycle between windows in the same tile
		for dir, hk in CycleKeys {
			fn := this.TileCycle.Bind(this, dir)
			hotkey, % hk, % fn
		}
		
		this.LoadSettings()
	}
	
	; Moves a window around
	; Mode 0 = resize / move
	; Mode 1 = move monitor
	WindowSizeMove(mode, axis, dir, hwnd := 0){
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		if (this.RegisterNewWindow(hwnd)){
			; Window is new to the tiling system - fill half of monitor in direction requested
			this.SetInitialWindowPosition(this.windows[hwnd], axis, dir)
			return
		}
		window := this.windows[hwnd]
		if (mode){
			; Move mode
			if (axis == "y")
				return	; move on y axis not supported
			window.monitor := this.WrapMonitor(window.monitor + dir)
		} else{
			; Resize / Move mode
			if (window.Alignments[axis] == dir){
				; The window is already against an edge and the move direction is towards the edge
				if (axis == "y")
					return	; move on y axis not supported
				; move to next monitor
				window.monitor := this.WrapMonitor(window.monitor + dir)
				; Flip layout to opposite edge
				window.Alignments[axis] *= -1
			} else {
				; Move the layout towards the direction.
				; If the x axis is -1 (on the left) and we move right (+1), it becomes 0 (spanned)
				window.Alignments[axis] := this.ClampDirection(window.Alignments[axis] + dir)
			}
		}
		this.TileWindow(window)
		this.SaveSettings()
	}

	; Cycles between items in a tile, in hwnd order (forwards or backwards)
	TileCycle(dir){
		hwnd := this.GetActiveHwnd()
		if (!ObjHasKey(this.windows, hwnd))
			return
		window := this.windows[hwnd]
		i := window.order
		Loop % this.WindowCount - 1 {
			i += dir
			if (i < 1)
				i := this.WindowCount
			else if (i > this.WindowCount)
				i := 1
			nw := this.windows[this.HwndList[i]]
			if (nw.monitor == window.monitor && nw.Alignments.x == window.Alignments.x && nw.Alignments.y == window.Alignments.y){
				WinActivate, % "ahk_id " nw.hwnd
				break
			}
		}
	}
	
	TileSwitch(axis, dir, hwnd := 0){
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		if (this.RegisterNewWindow(hwnd)){
			; Window is new to the tiling system - fill half of monitor in direction requested
			this.SetInitialWindowPosition(this.windows[hwnd], axis, dir)
			return
		}
		window := this.windows[hwnd]
		foundwindow := 0
		monitor := window.monitor
		align := window.Alignments.clone()
		other := this.OtherAxis(axis)
		;msgbox % this.GetCaretEdge(window, other)
		OutputDebug % "Movement Requested from " window.title ", monitor " window.monitor ", align x: " align.x ", y: " align.y ", axis: " axis ", dir: " dir
		if (window.Alignments[axis] == 0 || window.Alignments[axis] == dir){
			if (axis != "y"){
				; move towards a tile on different monitor
				monitor := this.WrapMonitor(monitor + dir)
				OutputDebug % "Movement is in direction of edge - monitor set to " monitor
				if (nw := this.FindWindow(monitor, {x: 0, y: 0})){
					foundwindow := 0
				}
			}
		}
		
		; Not really a loop, we just want to be able to break out at any point
		Loop {
			if (foundwindow)
				break
			; Check same window size, but on opposite edge for axis of movement
			align[axis] *= -1
			OutputDebug % "Check #1 - position x: " align.x ", y: " align.y
			nw := this.FindWindow(monitor, align)
			if (nw){
				OutputDebug % "Matched"
				foundwindow := nw
				break
			}
			
			; Check other possible alignment for movement axis
			if (align[axis] == 0){
				;align[axis] := this.GetCaretEdge(window, other)
				align[axis] := dir * -1
			} else {
				align[axis] := 0
			}
			OutputDebug % "Check #2 - position x: " align.x ", y: " align.y
			if (nw := this.FindWindow(monitor, align)){
				OutputDebug % "Matched"
				foundwindow := nw
				break
			}
			
			; Swap other axis
			if (align[other] == 0){
				align[other] := this.GetCaretEdge(window, other)
				;align[other] := dir * -1
			} else {
				align[other] := 0
			}
			OutputDebug % "Check #3 - position x: " align.x ", y: " align.y
			if (nw := this.FindWindow(monitor, align)){
				OutputDebug % "Matched"
				foundwindow := nw
				break
			}
			
			; Go back to original iteration for [axis]
			align[axis] := window.Alignments[axis] * -1
			OutputDebug % "Check #4 - position x: " align.x ", y: " align.y
			if (nw := this.FindWindow(monitor, align)){
				OutputDebug % "Matched"
				foundwindow := nw
				break
			}

			break
		}
		if (!foundwindow || foundwindow == window.hwnd)
			return
		OutputDebug % "Found Window: " foundwindow " - " this.windows[foundwindow].title
		WinActivate, % "ahk_id " foundwindow
	}
	
	OtherAxis(axis){
		static opposites := {x: "y", y: "x"}
		return opposites[axis]
	}
	
	GetCaretEdge(window, axis){
		wc := window.GetCenter()
		cp := {x: A_CaretX, y: A_CaretY}
		if (cp[axis] < wc[axis])
			return -1
		else if (cp[axis] > wc[axis])
			return 1
		else
			return 0
	}
	
	FindWindow(monitor, align){
		for hwnd, w in this.windows {
			if (w.monitor == monitor && w.Alignments.x == align.x && w.Alignments.y == align.y)
				return hwnd
		}
		return 0
	}
	
	; Allows a direction to be applied to a direction
	ClampDirection(dir){
		if dir > 1
			return 1
		if dir < -1
			return -1
		return dir
	}
	
	; Allows a direction to be applied to a monitor and have it wrap around
	; Obviously only works for monitors all lined up left to right in ascending numbers
	WrapMonitor(mon){
		if (mon > this.MonitorCount) {
			return 1
		} else if (mon < 1) {
			return this.MonitorCount
		}
		return mon
	}
	
	; Decides where a window should be placed for the first time
	SetInitialWindowPosition(window, axis, dir){
		if (axis == "x"){
			window.Alignments.x := dir
		} else if (axis == "y"){
			window.Alignments.y := dir
		}
		window.monitor := this.GetWindowMonitor(window)
		this.TileWindow(window)
	}
	
	; Actually does the moving of a window
	; Set the parameters of the window object how you want it, then pass it to this function
	TileWindow(window){
		static divisors := {-1: 2, 0: 1, 1: 2}
		;window.monitor.cx
		monitor := this.monitors[window.monitor].coords

		w := round(monitor.w / divisors[window.Alignments.x])
		h := round(monitor.h / divisors[window.Alignments.y])
		; if Alignments.x is 1 then set position to center x, else left x
		x := window.Alignments.x == 1 ? monitor.cx : monitor.l
		y := window.Alignments.y == 1 ? monitor.cy : monitor.t
		;OutputDebug % "Moving hwnd " window.hwnd " to x:" x ", y:" y ", w: " w ", h: " h
		WinMove, % "ahk_id " window.hwnd,, x, y, w, h
	}
	
	; Returns the monitor number that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
	
	; Add a window to the tiling system
	; Returns 0 if window is already in the tiling system
	RegisterNewWindow(hwnd){
		if (ObjHasKey(this.windows, hwnd)){
			; Window has been placed by the tiling system
			return 0
		} else {
			; Window is new to the tiling system
			this.windows[hwnd] := new CWindow(hwnd)
			this.BuildWindowList()
			this.SaveSettings()
			return 1
		}
	}
	
	; Builds list of windows in hwnd order, so they can be cycled through in either direction
	BuildWindowList(){
		this.HwndList := []
		ctr := 0
		for hwnd, window in this.windows {
			this.HwndList.push(hwnd)
			ctr++
			window.order := ctr
		}
		this.WindowCount := ctr
	}
	
	GetActiveHwnd(){
		WinGet, hwnd, ID, A
		return hwnd+0
	}
	
	ShellMessage( wParam,lParam ) {
		If ( wParam = 1 ) { ;  HSHELL_WINDOWCREATED := 1
			hwnd := lParam
			;SetTimer, MsgBox, -1
			WinGetTitle, Title, % "ahk_id " hwnd
			WinGetClass, Class, % "ahk_id " hwnd
			if (class == "StartMenuSizingFrame")
				return
			;~ TrayTip, New Window Opened, Title:`t%Title%`nClass:`t%Class%
			;~ if (class == "Notepad"){
				;~ this.RegisterNewWindow(hwnd)
				;~ this.windows[hwnd].Alignments := {x: 0, y: -1}
				;~ this.windows[hwnd].monitor := 1
				;~ this.TileWindow(this.windows[hwnd])
			;~ }
		}
	}
	
	LoadSettings(){
		FileRead, j, % this.IniFile
		if (j == "")
			return
		obj := JSON.Load(j)
		this.windows := {}
		for hwnd, settings in obj.windows {
			if (!WinExist("ahk_id " hwnd))
				continue
			this.windows[hwnd] := new CWindow(hwnd)
			for k, v in settings {
				this.windows[hwnd, k] := v
				;this.TileWindow(this.windows[hwnd])
			}
		}
		this.BuildWindowList()
		
	}
	
	SaveSettings(){
		obj := {windows: {}}
		for hwnd, window in this.Windows {
			obj.windows[hwnd] := window.Serialize()
		}
		j := JSON.Dump(obj, true)
		FileDelete, % this.IniFile
		FileAppend, % j, % this.IniFile
	}
}

; Represents a window that has been positioned by the tiling system
Class CWindow {
	hwnd := 0
	order := 0		; Order in the HwndList
	monitor := 0	; Which monitor the window is on
	; Alignments for a given axis is 0 when it spans that axis...
	; ... -1 when it half-width and on the left/upper side (towards lower pixel numbers) ...
	; ... and +1 when on the right / bottom side (towards higher pixel numbers)
	Alignments := {x: 0, y: 0}
	
	__New(hwnd){
		this.hwnd := hwnd
		WinGetTitle, title, % "ahk_id " hwnd
		this.title := title
	}
	
	; Gets the coordinates of the center of the window
	GetCenter(){
		WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
		cx := wx + round(ww / 2)
		cy := wy + round(wh / 2)
		return {x: cx, y: cy}
	}
	
	Serialize(){
		obj := {}
		obj.monitor := this.monitor
		obj.Alignments := this.Alignments
		return obj
	}
}

; Represents a monitor that windows can reside on
Class CMonitor {
	id := 0
	coords := 0
	
	__New(id){
		this.id := id
		this.coords := this.GetWorkArea()
	}
	
	; Gets the "Work Area" of a monito (The coordinates of the desktop on that monitor minus the taskbar)
	; also pre-calculates a few values derived from the coordinates
	GetWorkArea(){
		SysGet, coords_, MonitorWorkArea, % this.id
		out := {}
		out.l := coords_left
		out.r := coords_right
		out.t := coords_top
		out.b := coords_bottom
		out.w := coords_right - coords_left
		out.h := coords_bottom - coords_top
		out.cx := coords_left + round(out.w / 2)	; center x
		out.cy := coords_top + round(out.h / 2)		; center y
		out.hw := round(out.w / 2)	; half width
		out.hh := round(out.w / 2)	 ; half height
		return out
	}
}

; ============================ JSON LIBRARY ==============================
/**
 * Lib: JSON.ahk
 *     JSON lib for AutoHotkey.
 * Version:
 *     v2.0.00.00 [updated 11/07/2015 (MM/DD/YYYY)]
 * License:
 *     WTFPL [http://wtfpl.net/]
 * Requirements:
 *     Latest version of AutoHotkey (v1.1+ or v2.0-a+)
 * Installation:
 *     Use #Include JSON.ahk or copy into a function library folder and then
 *     use #Include <JSON>
 * Links:
 *     GitHub:     - https://github.com/cocobelgica/AutoHotkey-JSON
 *     Forum Topic - http://goo.gl/r0zI8t
 *     Email:      - cocobelgica <at> gmail <dot> com
 */


/**
 * Class: JSON
 *     The JSON object contains methods for parsing JSON and converting values
 *     to JSON. Callable - NO; Instantiable - YES; Subclassable - YES;
 *     Nestable(via #Include) - NO.
 * Methods:
 *     Load() - see relevant documentation before method definition header
 *     Dump() - see relevant documentation before method definition header
 */
class JSON
{
	/**
	 * Method: Load
	 *     Parses a JSON string into an AHK value
	 * Syntax:
	 *     value := JSON.Load( text [, reviver ] )
	 * Parameter(s):
	 *     value      [retval] - parsed value
	 *     text      [in, opt] - JSON formatted string
	 *     reviver   [in, opt] - function object, similar to JavaScript's
	 *                           JSON.parse() 'reviver' parameter
	 */
	class Load extends JSON.Functor
	{
		Call(self, text, reviver:="")
		{
			this.rev := IsObject(reviver) ? reviver : false
			this.keys := this.rev ? {} : false

			static q := Chr(34)
			     , json_value := q . "{[01234567890-tfn"
			     , json_value_or_array_closing := q . "{[]01234567890-tfn"
			     , object_key_or_object_closing := q . "}"

			key := ""
			is_key := false
			root := {}
			stack := [root]
			next := json_value
			pos := 0

			while ((ch := SubStr(text, ++pos, 1)) != "") {
				if InStr(" `t`r`n", ch)
					continue
				if !InStr(next, ch, 1)
					this.ParseError(next, text, pos)

				holder := stack[1]
				is_array := holder.IsArray

				if InStr(",:", ch) {
					next := (is_key := !is_array && ch == ",") ? q : json_value

				} else if InStr("}]", ch) {
					ObjRemoveAt(stack, 1)
					next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}"

				} else {
					if InStr("{[", ch) {
					; Check if Array() is overridden and if its return value has
					; the 'IsArray' property. If so, Array() will be called normally,
					; otherwise, use a custom base object for arrays
						static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0
					
					; sacrifice readability for minor(actually negligible) performance gain
						(ch == "{")
							? ( is_key := true
							  , value := {}
							  , next := object_key_or_object_closing )
						; ch == "["
							: ( value := json_array ? new json_array : []
							  , next := json_value_or_array_closing )
						
						ObjInsertAt(stack, 1, value)

						if (this.keys)
							this.keys[value] := []
					
					} else {
						if (ch == q) {
							i := pos
							while (i := InStr(text, q,, i+1)) {
								value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c")

								static ss_end := A_AhkVersion<"2" ? 0 : -1
								if (SubStr(value, ss_end) != "\")
									break
							}

							if (!i)
								this.ParseError("'", text, pos)

							  value := StrReplace(value,    "\/",  "/")
							, value := StrReplace(value, "\" . q,    q)
							, value := StrReplace(value,    "\b", "`b")
							, value := StrReplace(value,    "\f", "`f")
							, value := StrReplace(value,    "\n", "`n")
							, value := StrReplace(value,    "\r", "`r")
							, value := StrReplace(value,    "\t", "`t")

							pos := i ; update pos
							
							i := 0
							while (i := InStr(value, "\",, i+1)) {
								if !(SubStr(value, i+1, 1) == "u")
									this.ParseError("\", text, pos - StrLen(SubStr(value, i+1)))

								uffff := Abs("0x" . SubStr(value, i+2, 4))
								if (A_IsUnicode || uffff < 0x100)
									value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6)
							}

							if (is_key) {
								key := value, next := ":"
								continue
							}
						
						} else {
							value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos)

							static number := "number", _null := ""
							if value is %number%
								value += 0
							else if (value == "true" || value == "false" || value == "_null")
								value := %value% + 0
							else
							; we can do more here to pinpoint the actual culprit
							; but that's just too much extra work.
								this.ParseError(next, text, pos, i)

							pos += i-1
						}

						next := holder==root ? "" : is_array ? ",]" : ",}"
					} ; If InStr("{[", ch) { ... } else

					is_array? key := ObjPush(holder, value) : holder[key] := value

					if (this.keys && this.keys.HasKey(holder))
						this.keys[holder].Push(key)
				}
			
			} ; while ( ... )

			return this.rev ? this.Walk(root, "") : root[""]
		}

		ParseError(expect, text, pos, len:=1)
		{
			static q := Chr(34)
			
			line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length()
			col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1))
			msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}"
			,     (expect == "")      ? "Extra data"
			    : (expect == "'")     ? "Unterminated string starting at"
			    : (expect == "\")     ? "Invalid \escape"
			    : (expect == ":")     ? "Expecting ':' delimiter"
			    : (expect == q)       ? "Expecting object key enclosed in double quotes"
			    : (expect == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
			    : (expect == ",}")    ? "Expecting ',' delimiter or object closing '}'"
			    : (expect == ",]")    ? "Expecting ',' delimiter or array closing ']'"
			    : InStr(expect, "]")  ? "Expecting JSON value or array closing ']'"
			    :                       "Expecting JSON value(string, number, true, false, null, object or array)"
			, line, col, pos)

			static offset := A_AhkVersion<"2" ? -3 : -4
			throw Exception(msg, offset, SubStr(text, pos, len))
		}

		Walk(holder, key)
		{
			value := holder[key]
			if IsObject(value)
				for i, k in this.keys[value]
					value[k] := this.Walk.Call(this, value, k) ; bypass __Call
			
			return this.rev.Call(holder, key, value)
		}
	}

	/**
	 * Method: Dump
	 *     Converts an AHK value into a JSON string
	 * Syntax:
	 *     str := JSON.Dump( value [, replacer, space ] )
	 * Parameter(s):
	 *     str        [retval] - JSON representation of an AHK value
	 *     value          [in] - any value(object, string, number)
	 *     replacer  [in, opt] - function object, similar to JavaScript's
	 *                           JSON.stringify() 'replacer' parameter
	 *     space     [in, opt] - similar to JavaScript's JSON.stringify()
	 *                           'space' parameter
	 */
	class Dump extends JSON.Functor
	{
		Call(self, value, replacer:="", space:="")
		{
			this.rep := IsObject(replacer) ? replacer : ""

			this.gap := ""
			if (space) {
				static integer := "integer"
				if space is %integer%
					Loop, % ((n := Abs(space))>10 ? 10 : n)
						this.gap .= " "
				else
					this.gap := SubStr(space, 1, 10)

				this.indent := "`n"
			}

			return this.Str({"": value}, "")
		}

		Str(holder, key)
		{
			value := holder[key]

			if (this.rep)
				value := this.rep.Call(holder, key, value)

			if IsObject(value) {
				if (this.gap) {
					stepback := this.indent
					this.indent .= this.gap
				}

				is_array := value.IsArray
				; Array() is not overridden, rollback to old method of
				; identifying array-like objects
				if (!is_array) {
					for i in value
						is_array := i == A_Index
					until !is_array
				}

				str := ""
				if (is_array) {
					Loop, % value.Length() {
						if (this.gap)
							str .= this.indent
						
						str .= value.HasKey(A_Index) ? this.Str(value, A_Index) . "," : "null,"
					}
				} else {
					colon := this.gap ? ": " : ":"
					for k in value {
						if (this.gap)
							str .= this.indent

						str .= this.Quote(k) . colon . this.Str(value, k) . ","
					}
				}

				if (str != "") {
					str := RTrim(str, ",")
					if (this.gap)
						str .= stepback
				}

				if (this.gap)
					this.indent := stepback

				return is_array ? "[" . str . "]" : "{" . str . "}"
			}
			; is_number ? value : "value"
			return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value)
		}

		Quote(string)
		{
			static q := Chr(34)

			if (string != "") {
				  string := StrReplace(string,  "\",    "\\")
				; , string := StrReplace(string,  "/",    "\/") ; optional in ECMAScript
				, string := StrReplace(string,    q, "\" . q)
				, string := StrReplace(string, "`b",    "\b")
				, string := StrReplace(string, "`f",    "\f")
				, string := StrReplace(string, "`n",    "\n")
				, string := StrReplace(string, "`r",    "\r")
				, string := StrReplace(string, "`t",    "\t")

				static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]"
				while RegExMatch(string, rx_escapable, m)
					string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value)))
			}

			return q . string . q
		}
	}

	class Functor
	{
		__Call(method, args*)
		{
		; When casting to Call(), use a new instance of the "function object"
		; so as to avoid directly storing the properties(used across sub-methods)
		; into the "function object" itself.
			if IsObject(method)
				return (new this).Call(method, args*)
			else if (method == "")
				return (new this).Call(args*)
		}
	}
}
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

14 Jun 2016, 10:40

Update:
I think I may have nailed WIN+Ctrl+Arrow navigation.

Any monitor layout now supported (ie any given monitor can be Left, Right, Above or Below another monitor)

Code: Select all

/*
Window Tiling system
Takes the default windows (Win+Arrow) hotkeys and makes them a bit better
Adds the ability to Tile windows into quarters / halves on both axes
Win+Alt+Arrow can be used to force a monitor change
Also adds a Win+Wheel hotkey to cycle between windows in the same Tile
*/
#SingleInstance force
te := new TileEngine()

; Set monitor positions.
; 1st Param is source (from)
; 2nd Param is direction ("Above", "Below", "Left" or "Right")
; 3rd Param is destination (to)
; The opposite (ie dest to source) is automatically added also
te.SetMonitorPosition(2, "Right", 1)	; Right from 2 goes to 1 (And left from 1 goes to 2)
te.SetMonitorPosition(1, "Right", 2)	; Right from 1 goes to 2 (And left from 2 goes to 1)
 
Class TileEngine {
	MonitorMap := {}
	MonitorCount := 0
	WindowCount := 0
	MonitorPrimary := 0
	monitors := []
	windows := {}
	HwndList := []	; windows array is sparse. this is a lookup table that can be iterated forwards or backwards
 
	__New(){
		static DirectionKeys := {x: {-1: "#Left", 1: "#Right"}, y: {-1: "#Up", 1: "#Down"}}
		static CycleKeys := {-1: "#WheelUp", 1: "#WheelDown"}
		static MonitorModifier := "+"
		static SwitchModifier := "^"
 
		CoordMode, Caret, Screen
		CoordMode, Mouse, Screen
 
		Gui +Hwndhwnd
		this.hwnd := hwnd
		DllCall( "RegisterShellHookWindow", UInt,hWnd )
		;~ MsgNum := DllCall( "RegisterWindowMessage", Str,"SHELLHOOK" )
		;~ fn := this.ShellMessage.Bind(this)
		;~ OnMessage( MsgNum, fn )
 
		this.IniFile := RegexReplace(A_ScriptName, "(.ahk|.exe)$", ".ini")
 
		; Query monitor info
		SysGet, MonitorCount, MonitorCount
		this.MonitorCount := MonitorCount
		SysGet, MonitorPrimary, MonitorPrimary
		this.MonitorPrimary := MonitorPrimary
 
		; Create Monitor objects
		Loop % this.MonitorCount {
			this.monitors.push(new CMonitor(A_Index))
		}
 
		; Bind Direction Keys to WindowSizeMove
		for axis, hks in DirectionKeys {
			for dir, hk in hks {
				fn := this.WindowSizeMove.Bind(this, 0, axis, dir)
				hotkey, % hk, % fn
				if (MonitorModifier){
					fn := this.WindowSizeMove.Bind(this, 1, axis, dir)
					hotkey, % MonitorModifier hk, % fn
				}
				if (SwitchModifier){
					fn := this.TileSwitch.Bind(this, axis, dir)
					hotkey, % SwitchModifier hk, % fn
				}
			}
		}
 
		; Bind Cycle Keys to cycle between windows in the same tile
		for dir, hk in CycleKeys {
			fn := this.TileCycle.Bind(this, dir)
			hotkey, % hk, % fn
		}
 
		this.LoadSettings()
	}

	SetMonitorPosition(source, direction, dest){
		static directions := {Above: {axis:"y", dir:-1}, Below: {axis:"y", dir: 1}, Left:{axis:"x", dir: -1}, Right: {axis:"x", dir: 1}}
		axis := directions[direction].axis, dir := directions[direction].dir
		if (!ObjHasKey(this.MonitorMap, source))
			this.MonitorMap[source] := {x: {}, y: {}}
		this.MonitorMap[source, axis, dir] := dest
		
		if (!ObjHasKey(this.MonitorMap, dest))
			this.MonitorMap[dest] := {x: {}, y: {}}
		this.MonitorMap[dest, axis, dir * -1] := source
	}
	
	; Moves a window around
	; Mode 0 = resize / move
	; Mode 1 = move monitor
	WindowSizeMove(mode, axis, dir, hwnd := 0){
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		if (this.RegisterNewWindow(hwnd)){
			; Window is new to the tiling system - fill half of monitor in direction requested
			this.SetInitialWindowPosition(this.windows[hwnd], axis, dir)
			return
		}
		window := this.windows[hwnd]
		OutputDebug % "Move requested for hwnd " window.hwnd " (" window.title ") on monitor " window.monitor " in direction " this.RenderDir(axis, dir)
		if (mode){
			; Move mode
			if (!monitor := this.NavigateMonitor(window.monitor, axis, dir))
				return
			window.monitor := monitor
		} else{
			; Resize / Move mode
			if (window.Alignments[axis] == dir){
				; The window is already against an edge and the move direction is towards the edge
				; move to next monitor
				if (!monitor := this.NavigateMonitor(window.monitor, axis, dir))
					return
				window.monitor := monitor
				; Flip layout to opposite edge
				window.Alignments[axis] *= -1
			} else {
				; Move the layout towards the direction.
				; If the x axis is -1 (on the left) and we move right (+1), it becomes 0 (spanned)
				window.Alignments[axis] := this.ClampDirection(window.Alignments[axis] + dir)
			}
		}
		OutputDebug % "Setting new alignment for hwnd " window.hwnd " (" window.title ") to " this.RenderAlign(window.Alignments, window.monitor)
		this.TileWindow(window)
		this.SaveSettings()
	}
 
	; Cycles between items in a tile, in hwnd order (forwards or backwards)
	TileCycle(dir){
		hwnd := this.GetActiveHwnd()
		if (!ObjHasKey(this.windows, hwnd))
			return
		window := this.windows[hwnd]
		i := window.order
		Loop % this.WindowCount - 1 {
			i += dir
			if (i < 1)
				i := this.WindowCount
			else if (i > this.WindowCount)
				i := 1
			nw := this.windows[this.HwndList[i]]
			if (nw.monitor == window.monitor && nw.Alignments.x == window.Alignments.x && nw.Alignments.y == window.Alignments.y){
				WinActivate, % "ahk_id " nw.hwnd
				break
			}
		}
	}
 
	TileSwitch(axis, dir, hwnd := 0){
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		if (this.RegisterNewWindow(hwnd)){
			; Window is new to the tiling system - fill half of monitor in direction requested
			this.SetInitialWindowPosition(this.windows[hwnd], axis, dir)
			return
		}
		window := this.windows[hwnd]
		monitor := window.monitor
		align := window.Alignments.clone()	
		other_axis := this.OtherAxis(axis)
		spans := (align[other_axis] = 0)
		bias := this.GetCaretEdge(window, other_axis)
		OutputDebug % "Starting at: " this.RenderAlign(align, monitor) ", moving " this.RenderDir(axis, dir) ", bias " this.RenderDir(other_axis, bias)
		if (align[axis] == 0 || align[axis] == dir){
			; Movement is towards monitor edge
			monitor := this.NavigateMonitor(monitor, axis, dir)	; find next monitor in direction <dir>
			OutputDebug % "Movement is towards the " this.RenderDir(axis, dir) " of monitor " window.monitor
			if (!monitor){
				OutputDebug % "There is no monitor to move to, aborting" 
				return
			}
			OutputDebug %  "Changing monitor to " monitor
		}
		align[axis] := this.Invert(align[axis]) ; flip alignment in direction of movement
		
		Loop {
			OutputDebug % "#1 - Trying " this.RenderAlign(align, monitor)
			if (nw := this.FindWindow(monitor, align)){
				OutputDebug % "MATCH " nw " (" this.windows[nw].title ")"
				foundwindow := nw
				break
			}

			if (spans && monitor != window.monitor){
				align[axis] := this.Invert(dir)
				OutputDebug % "#2 - Trying " this.RenderAlign(align, monitor)
				if (nw := this.FindWindow(monitor, align)){
					OutputDebug % "MATCH " nw " (" this.windows[nw].title ")"
					foundwindow := nw
					break
				}
			}
			
			align[axis] := this.Invert(window.Alignments[axis])
			align[other_axis] := this.Alt(align[other_axis], bias)
			OutputDebug % "#3 - Trying " this.RenderAlign(align, monitor)
			if (nw := this.FindWindow(monitor, align)){
				OutputDebug % "MATCH " nw " (" this.windows[nw].title ")"
				foundwindow := nw
				break

			}

			if (spans){
				align := window.Alignments.clone()
				align[axis] := this.Invert(dir)
				align[other_axis] := this.Alt(align[other_axis], bias)
				OutputDebug % "#4 - Trying " this.RenderAlign(align, monitor)
				if (nw := this.FindWindow(monitor, align)){
					OutputDebug % "MATCH " nw " (" this.windows[nw].title ")"
					foundwindow := nw
					break
				}
			
				; Repeat last test, opposite Bias
				align[other_axis] := this.Alt(window.Alignments[other_axis], this.Invert(bias))
				OutputDebug % "#4a - Trying " this.RenderAlign(align, monitor)
				if (nw := this.FindWindow(monitor, align)){
					OutputDebug % "MATCH " nw " (" this.windows[nw].title ")"
					foundwindow := nw
					break
				}
			} else {
				align := window.Alignments.clone()
				align[axis] := this.Alt(align[axis])
				align[other_axis] := this.Alt(align[other_axis])
				OutputDebug % "#4b - Trying " this.RenderAlign(align, monitor)
				if (nw := this.FindWindow(monitor, align)){
					OutputDebug % "MATCH " nw " (" this.windows[nw].title ")"
					return nw
				}
			}
			break
		}
		
		if (!foundwindow)
			return
		WinActivate, % "ahk_id " foundwindow
	}
 
	OtherAxis(axis){
		static opposites := {x: "y", y: "x"}
		return opposites[axis]
	}

	Invert(align){
		return align * -1
	}
	
	Alt(align, bias := 1){
		if (align == 0)
			return bias
		return 0
	}
	
	RenderAlign(align, monitor){
		static names := {x: {-1: "West", 0: "", 1: "East"}, y: {-1: "North", 0: "", 1: "South"}}
		str := names["y", align.y] names["x", align.x]
		if (!str)
			str := "FullScreen"
		str := "(" monitor ")" str
		return % str
	}
	
	RenderDir(axis, dir){
		static names := {x: {-1: "Left", 0: "None", 1: "Right"}, y: {-1: "Up", 0: "None", 1: "Down"}}
		return names[axis, dir]
	}
	
	GetCaretEdge(window, axis){
		wc := window.GetCenter()
		cp := {x: A_CaretX, y: A_CaretY}
		if (cp[axis] < wc[axis])
			return -1
		else if (cp[axis] > wc[axis])
			return 1
		else
			return 0
	}
 
	FindWindow(monitor, align){
		for hwnd, w in this.windows {
			if (w.monitor == monitor && w.Alignments.x == align.x && w.Alignments.y == align.y){
				if (!WinExist("ahk_id " hwnd)){
					this.windows.Delete(hwnd)
					continue
				}
				return hwnd
			}
		}
		return 0
	}
 
	; Allows a direction to be applied to a direction
	ClampDirection(dir){
		if dir > 1
			return 1
		if dir < -1
			return -1
		return dir
	}
 
	; Allows a direction to be applied to a monitor and have it wrap around
	; Obviously only works for monitors all lined up left to right in ascending numbers
	NavigateMonitor(monitor, axis, dir){
		dest := this.MonitorMap[monitor, axis, dir]
		if (dest)
			return dest
		else
			return 0
		;~ monitor += dir
		;~ if (monitor > this.MonitorCount) {
			;~ return 1
		;~ } else if (monitor < 1) {
			;~ return this.MonitorCount
		;~ }
		;~ return mon
	}
 
	; Decides where a window should be placed for the first time
	SetInitialWindowPosition(window, axis, dir){
		if (axis == "x"){
			window.Alignments.x := dir
		} else if (axis == "y"){
			window.Alignments.y := dir
		}
		window.monitor := this.GetWindowMonitor(window)
		OutputDebug % "Got initial monitor of " window.monitor " for hwnd " window.hwnd " (" window.title ")"
		this.TileWindow(window)
	}
 
	; Actually does the moving of a window
	; Set the parameters of the window object how you want it, then pass it to this function
	TileWindow(window){
		static divisors := {-1: 2, 0: 1, 1: 2}
		;window.monitor.cx
		monitor := this.monitors[window.monitor].coords
 
		w := round(monitor.w / divisors[window.Alignments.x])
		h := round(monitor.h / divisors[window.Alignments.y])
		; if Alignments.x is 1 then set position to center x, else left x
		x := window.Alignments.x == 1 ? monitor.cx : monitor.l
		y := window.Alignments.y == 1 ? monitor.cy : monitor.t
		;OutputDebug % "Moving hwnd " window.hwnd " to x:" x ", y:" y ", w: " w ", h: " h
		WinMove, % "ahk_id " window.hwnd,, x, y, w, h
	}
 
	; Returns the monitor number that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
 
	; Add a window to the tiling system
	; Returns 0 if window is already in the tiling system
	RegisterNewWindow(hwnd){
		if (ObjHasKey(this.windows, hwnd)){
			; Window has been placed by the tiling system
			return 0
		} else {
			; Window is new to the tiling system
			this.windows[hwnd] := new CWindow(hwnd)
			this.BuildWindowList()
			this.SaveSettings()
			return 1
		}
	}
 
	; Builds list of windows in hwnd order, so they can be cycled through in either direction
	BuildWindowList(){
		this.HwndList := []
		ctr := 0
		for hwnd, window in this.windows {
			this.HwndList.push(hwnd)
			ctr++
			window.order := ctr
		}
		this.WindowCount := ctr
	}
 
	GetActiveHwnd(){
		WinGet, hwnd, ID, A
		return hwnd+0
	}
 
	ShellMessage( wParam,lParam ) {
		If ( wParam = 1 ) { ;  HSHELL_WINDOWCREATED := 1
			hwnd := lParam
			;SetTimer, MsgBox, -1
			WinGetTitle, Title, % "ahk_id " hwnd
			WinGetClass, Class, % "ahk_id " hwnd
			if (class == "StartMenuSizingFrame")
				return
			;~ TrayTip, New Window Opened, Title:`t%Title%`nClass:`t%Class%
			;~ if (class == "Notepad"){
				;~ this.RegisterNewWindow(hwnd)
				;~ this.windows[hwnd].Alignments := {x: 0, y: -1}
				;~ this.windows[hwnd].monitor := 1
				;~ this.TileWindow(this.windows[hwnd])
			;~ }
		}
	}
 
	LoadSettings(){
		FileRead, j, % this.IniFile
		if (j == "")
			return
		obj := JSON.Load(j)
		this.windows := {}
		for hwnd, settings in obj.windows {
			if (!WinExist("ahk_id " hwnd))
				continue
			this.windows[hwnd] := new CWindow(hwnd)
			for k, v in settings {
				this.windows[hwnd, k] := v
				;this.TileWindow(this.windows[hwnd])
			}
		}
		this.BuildWindowList()
 
	}
 
	SaveSettings(){
		obj := {windows: {}}
		for hwnd, window in this.Windows {
			obj.windows[hwnd] := window.Serialize()
		}
		j := JSON.Dump(obj, true)
		FileDelete, % this.IniFile
		FileAppend, % j, % this.IniFile
	}
}
 
; Represents a window that has been positioned by the tiling system
Class CWindow {
	hwnd := 0
	order := 0		; Order in the HwndList
	monitor := 0	; Which monitor the window is on
	; Alignments for a given axis is 0 when it spans that axis...
	; ... -1 when it half-width and on the left/upper side (towards lower pixel numbers) ...
	; ... and +1 when on the right / bottom side (towards higher pixel numbers)
	Alignments := {x: 0, y: 0}
 
	__New(hwnd){
		this.hwnd := hwnd
		WinGetTitle, title, % "ahk_id " hwnd
		this.title := title
	}
 
	; Gets the coordinates of the center of the window
	GetCenter(){
		WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
		cx := wx + round(ww / 2)
		cy := wy + round(wh / 2)
		return {x: cx, y: cy}
	}
 
	Serialize(){
		obj := {}
		obj.monitor := this.monitor
		obj.Alignments := this.Alignments
		return obj
	}
}
 
; Represents a monitor that windows can reside on
Class CMonitor {
	id := 0
	coords := 0
 
	__New(id){
		this.id := id
		this.coords := this.GetWorkArea()
	}
 
	; Gets the "Work Area" of a monito (The coordinates of the desktop on that monitor minus the taskbar)
	; also pre-calculates a few values derived from the coordinates
	GetWorkArea(){
		SysGet, coords_, MonitorWorkArea, % this.id
		out := {}
		out.l := coords_left
		out.r := coords_right
		out.t := coords_top
		out.b := coords_bottom
		out.w := coords_right - coords_left
		out.h := coords_bottom - coords_top
		out.cx := coords_left + round(out.w / 2)	; center x
		out.cy := coords_top + round(out.h / 2)		; center y
		out.hw := round(out.w / 2)	; half width
		out.hh := round(out.w / 2)	 ; half height
		return out
	}
}
 
; ============================ JSON LIBRARY ==============================
/**
 * Lib: JSON.ahk
 *     JSON lib for AutoHotkey.
 * Version:
 *     v2.0.00.00 [updated 11/07/2015 (MM/DD/YYYY)]
 * License:
 *     WTFPL [http://wtfpl.net/]
 * Requirements:
 *     Latest version of AutoHotkey (v1.1+ or v2.0-a+)
 * Installation:
 *     Use #Include JSON.ahk or copy into a function library folder and then
 *     use #Include <JSON>
 * Links:
 *     GitHub:     - https://github.com/cocobelgica/AutoHotkey-JSON
 *     Forum Topic - http://goo.gl/r0zI8t
 *     Email:      - cocobelgica <at> gmail <dot> com
 */
 
 
/**
 * Class: JSON
 *     The JSON object contains methods for parsing JSON and converting values
 *     to JSON. Callable - NO; Instantiable - YES; Subclassable - YES;
 *     Nestable(via #Include) - NO.
 * Methods:
 *     Load() - see relevant documentation before method definition header
 *     Dump() - see relevant documentation before method definition header
 */
class JSON
{
	/**
	 * Method: Load
	 *     Parses a JSON string into an AHK value
	 * Syntax:
	 *     value := JSON.Load( text [, reviver ] )
	 * Parameter(s):
	 *     value      [retval] - parsed value
	 *     text      [in, opt] - JSON formatted string
	 *     reviver   [in, opt] - function object, similar to JavaScript's
	 *                           JSON.parse() 'reviver' parameter
	 */
	class Load extends JSON.Functor
	{
		Call(self, text, reviver:="")
		{
			this.rev := IsObject(reviver) ? reviver : false
			this.keys := this.rev ? {} : false
 
			static q := Chr(34)
			     , json_value := q . "{[01234567890-tfn"
			     , json_value_or_array_closing := q . "{[]01234567890-tfn"
			     , object_key_or_object_closing := q . "}"
 
			key := ""
			is_key := false
			root := {}
			stack := [root]
			next := json_value
			pos := 0
 
			while ((ch := SubStr(text, ++pos, 1)) != "") {
				if InStr(" `t`r`n", ch)
					continue
				if !InStr(next, ch, 1)
					this.ParseError(next, text, pos)
 
				holder := stack[1]
				is_array := holder.IsArray
 
				if InStr(",:", ch) {
					next := (is_key := !is_array && ch == ",") ? q : json_value
 
				} else if InStr("}]", ch) {
					ObjRemoveAt(stack, 1)
					next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}"
 
				} else {
					if InStr("{[", ch) {
					; Check if Array() is overridden and if its return value has
					; the 'IsArray' property. If so, Array() will be called normally,
					; otherwise, use a custom base object for arrays
						static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0
 
					; sacrifice readability for minor(actually negligible) performance gain
						(ch == "{")
							? ( is_key := true
							  , value := {}
							  , next := object_key_or_object_closing )
						; ch == "["
							: ( value := json_array ? new json_array : []
							  , next := json_value_or_array_closing )
 
						ObjInsertAt(stack, 1, value)
 
						if (this.keys)
							this.keys[value] := []
 
					} else {
						if (ch == q) {
							i := pos
							while (i := InStr(text, q,, i+1)) {
								value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c")
 
								static ss_end := A_AhkVersion<"2" ? 0 : -1
								if (SubStr(value, ss_end) != "\")
									break
							}
 
							if (!i)
								this.ParseError("'", text, pos)
 
							  value := StrReplace(value,    "\/",  "/")
							, value := StrReplace(value, "\" . q,    q)
							, value := StrReplace(value,    "\b", "`b")
							, value := StrReplace(value,    "\f", "`f")
							, value := StrReplace(value,    "\n", "`n")
							, value := StrReplace(value,    "\r", "`r")
							, value := StrReplace(value,    "\t", "`t")
 
							pos := i ; update pos
 
							i := 0
							while (i := InStr(value, "\",, i+1)) {
								if !(SubStr(value, i+1, 1) == "u")
									this.ParseError("\", text, pos - StrLen(SubStr(value, i+1)))
 
								uffff := Abs("0x" . SubStr(value, i+2, 4))
								if (A_IsUnicode || uffff < 0x100)
									value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6)
							}
 
							if (is_key) {
								key := value, next := ":"
								continue
							}
 
						} else {
							value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos)
 
							static number := "number", _null := ""
							if value is %number%
								value += 0
							else if (value == "true" || value == "false" || value == "_null")
								value := %value% + 0
							else
							; we can do more here to pinpoint the actual culprit
							; but that's just too much extra work.
								this.ParseError(next, text, pos, i)
 
							pos += i-1
						}
 
						next := holder==root ? "" : is_array ? ",]" : ",}"
					} ; If InStr("{[", ch) { ... } else
 
					is_array? key := ObjPush(holder, value) : holder[key] := value
 
					if (this.keys && this.keys.HasKey(holder))
						this.keys[holder].Push(key)
				}
 
			} ; while ( ... )
 
			return this.rev ? this.Walk(root, "") : root[""]
		}
 
		ParseError(expect, text, pos, len:=1)
		{
			static q := Chr(34)
 
			line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length()
			col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1))
			msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}"
			,     (expect == "")      ? "Extra data"
			    : (expect == "'")     ? "Unterminated string starting at"
			    : (expect == "\")     ? "Invalid \escape"
			    : (expect == ":")     ? "Expecting ':' delimiter"
			    : (expect == q)       ? "Expecting object key enclosed in double quotes"
			    : (expect == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
			    : (expect == ",}")    ? "Expecting ',' delimiter or object closing '}'"
			    : (expect == ",]")    ? "Expecting ',' delimiter or array closing ']'"
			    : InStr(expect, "]")  ? "Expecting JSON value or array closing ']'"
			    :                       "Expecting JSON value(string, number, true, false, null, object or array)"
			, line, col, pos)
 
			static offset := A_AhkVersion<"2" ? -3 : -4
			throw Exception(msg, offset, SubStr(text, pos, len))
		}
 
		Walk(holder, key)
		{
			value := holder[key]
			if IsObject(value)
				for i, k in this.keys[value]
					value[k] := this.Walk.Call(this, value, k) ; bypass __Call
 
			return this.rev.Call(holder, key, value)
		}
	}
 
	/**
	 * Method: Dump
	 *     Converts an AHK value into a JSON string
	 * Syntax:
	 *     str := JSON.Dump( value [, replacer, space ] )
	 * Parameter(s):
	 *     str        [retval] - JSON representation of an AHK value
	 *     value          [in] - any value(object, string, number)
	 *     replacer  [in, opt] - function object, similar to JavaScript's
	 *                           JSON.stringify() 'replacer' parameter
	 *     space     [in, opt] - similar to JavaScript's JSON.stringify()
	 *                           'space' parameter
	 */
	class Dump extends JSON.Functor
	{
		Call(self, value, replacer:="", space:="")
		{
			this.rep := IsObject(replacer) ? replacer : ""
 
			this.gap := ""
			if (space) {
				static integer := "integer"
				if space is %integer%
					Loop, % ((n := Abs(space))>10 ? 10 : n)
						this.gap .= " "
				else
					this.gap := SubStr(space, 1, 10)
 
				this.indent := "`n"
			}
 
			return this.Str({"": value}, "")
		}
 
		Str(holder, key)
		{
			value := holder[key]
 
			if (this.rep)
				value := this.rep.Call(holder, key, value)
 
			if IsObject(value) {
				if (this.gap) {
					stepback := this.indent
					this.indent .= this.gap
				}
 
				is_array := value.IsArray
				; Array() is not overridden, rollback to old method of
				; identifying array-like objects
				if (!is_array) {
					for i in value
						is_array := i == A_Index
					until !is_array
				}
 
				str := ""
				if (is_array) {
					Loop, % value.Length() {
						if (this.gap)
							str .= this.indent
 
						str .= value.HasKey(A_Index) ? this.Str(value, A_Index) . "," : "null,"
					}
				} else {
					colon := this.gap ? ": " : ":"
					for k in value {
						if (this.gap)
							str .= this.indent
 
						str .= this.Quote(k) . colon . this.Str(value, k) . ","
					}
				}
 
				if (str != "") {
					str := RTrim(str, ",")
					if (this.gap)
						str .= stepback
				}
 
				if (this.gap)
					this.indent := stepback
 
				return is_array ? "[" . str . "]" : "{" . str . "}"
			}
			; is_number ? value : "value"
			return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value)
		}
 
		Quote(string)
		{
			static q := Chr(34)
 
			if (string != "") {
				  string := StrReplace(string,  "\",    "\\")
				; , string := StrReplace(string,  "/",    "\/") ; optional in ECMAScript
				, string := StrReplace(string,    q, "\" . q)
				, string := StrReplace(string, "`b",    "\b")
				, string := StrReplace(string, "`f",    "\f")
				, string := StrReplace(string, "`n",    "\n")
				, string := StrReplace(string, "`r",    "\r")
				, string := StrReplace(string, "`t",    "\t")
 
				static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]"
				while RegExMatch(string, rx_escapable, m)
					string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value)))
			}
 
			return q . string . q
		}
	}
 
	class Functor
	{
		__Call(method, args*)
		{
		; When casting to Call(), use a new instance of the "function object"
		; so as to avoid directly storing the properties(used across sub-methods)
		; into the "function object" itself.
			if IsObject(method)
				return (new this).Call(method, args*)
			else if (method == "")
				return (new this).Call(args*)
		}
	}
}
I now need code that can work out, given a stack of windows all in the same tile, which window is topmost.
So I am thinking a "get hwnd at coordinates" function would do?
guest3456
Posts: 3463
Joined: 09 Oct 2013, 10:31

Re: Win+Arrow Window Tiling System

14 Jun 2016, 11:48

evilC wrote: I now need code that can work out, given a stack of windows all in the same tile, which window is topmost.
So I am thinking a "get hwnd at coordinates" function would do?
a couple ideas that i've used in the past:

Code: Select all

;// gets the number/position of the window in the z-order 
GetZorderPosition(hwnd)
{
   PtrType := A_PtrSize ? "Ptr" : "UInt" ; AHK_L : AHK_Basic
   z := 0
   h := hwnd
   while (h != 0)
   {
      h := DllCall("user32.dll\GetWindow", PtrType, h, "UInt", GW_HWNDPREV := 3)
      ; WinGetTitle, title, ahk_id %h%
      ; msgbox, h=%h%`ntitle=%title%
      z++
   }
   return z
}

GetTopMostTableInStack()
{
   prev_z := 999999
   Loop, Parse, STACKED_LIST, `,    ;// obviously fill STACKED_LIST
   {
      z := GetZorderPosition(A_LoopField)
      ;WinGetTitle, title, ahk_id %A_LoopField%      
      ;msgbox, checking`n`n%title%`n`nzorder = %z%
      if (z < prev_z)
         hwnd_lowest_z := A_LoopField
      prev_z := z
   }
   return hwnd_lowest_z
}
but WinGet,, List already returns the windows in the correct z-order so you could also parse through that. i believe thats what i'm using currently

User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

14 Jun 2016, 13:47

Cool man, thanks for that.
So I am thinking now that I should refactor a little.

I don't see that one big windows[] array is serving me very well, I am thinking that I should have a multidimensional array with indexes for monitor and alignment - that way I will not need to search the windows array to find windows in a certain position, and I can also easily use your code to find the topmost window.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

15 Jun 2016, 16:54

Quite a big re-factor.
Settings file is gone, will look into putting it back.
Guest3456's z-order code is integrated, so now when you switch to a tile, you get the top window.

Code: Select all

/*
Window Tiling system
Takes the default windows (Win+Arrow) hotkeys and makes them a bit better
Adds the ability to Tile windows into quarters / halves on both axes
Win+Alt+Arrow can be used to force a monitor change
Also adds a Win+Wheel hotkey to cycle between windows in the same Tile
*/
OutputDebug DBGVIEWCLEAR
#SingleInstance force
te := new TileEngine()
 
; Set monitor positions.
; The opposite (ie dest to source) is automatically added also
; My monitor order (L to R) : 1, 4, 2, 5, 3
te.SetMonitorPosition(1, "West", 2)	; Monitor 1 is to the West of monitor 4
te.SetMonitorPosition(2, "West", 4)	; Monitor 4 is to the West of monitor 2
te.SetMonitorPosition(5, "East", 4)	; Monitor 5 is to the East of monitor 3
te.SetMonitorPosition(3, "East", 5)	; Monitor 3 is to the East of monitor 2
te.SetMonitorPosition(1, "East", 3)	; Monitor 1 is to the East of monitor 3 (Enable wrap)

; Standard 3 monitor (1,2,3 order):
;~ te.SetMonitorPosition(1, "West", 2)	; Monitor 1 is to the West of monitor 2
;~ te.SetMonitorPosition(2, "West", 4)	; Monitor 3 is to the East of monitor 2
;~ te.SetMonitorPosition(5, "East", 4)	; Monitor 1 is to the East of monitor 3 (Enable wrap)


; A note about terms
; Vectors (eg {x:-1, y:0} meaning spanned along the left side of the monitor, aka "west")
; These are used to represent tile positions in a mathematical way that can be compared, altered etc
; For x, -1 is left, 1 is right. For y, -1 is up, 1 is down.

; Vector (eg {axis: "x", dir: -1} meaning "west" or "left")
; This is used for input or move calculations (in combination with Vectors arrays)

; "Compass" (ie n,e,s,w,ne,se,sw,nw)
; These are used where one "key" is needed to represent a tile's location

; Main class
Class TileEngine extends TileEngineBase {
	windows := {}
	monitors := []
	MonitorMap := {}
	MonitorCount := 0
	MonitorPrimary := 0
	
	__New(){
		static DirectionKeys := {x: {-1: "#Left", 1: "#Right"}, y: {-1: "#Up", 1: "#Down"}}
		static CycleKeys := {-1: "#WheelUp", 1: "#WheelDown"}
		static MonitorModifier := "+"
		static SwitchModifier := "^"
		
		this.log("Starting up")
		
		CoordMode, Caret, Screen
		CoordMode, Mouse, Screen
 
		Gui +Hwndhwnd
		this.hwnd := hwnd
		;~ DllCall( "RegisterShellHookWindow", UInt,hWnd )
		;~ MsgNum := DllCall( "RegisterWindowMessage", Str,"SHELLHOOK" )
		;~ fn := this.ShellMessage.Bind(this)
		;~ OnMessage( MsgNum, fn )
 
		this.IniFile := RegexReplace(A_ScriptName, "(.ahk|.exe)$", ".ini")
 
		; Query monitor info
		SysGet, MonitorCount, MonitorCount
		this.MonitorCount := MonitorCount
		SysGet, MonitorPrimary, MonitorPrimary
		this.MonitorPrimary := MonitorPrimary
		this.log(MonitorCount " monitors detected")
		
		; Create Monitor objects
		Loop % this.MonitorCount {
			this.monitors.push(new CMonitor(A_Index))
		}
 
		; Bind Direction Keys to WindowSizeMove
		for axis, hks in DirectionKeys {
			for dir, hk in hks {
				fn := this.WindowSizeMove.Bind(this, 0, {axis: axis, dir: dir})
				hotkey, % hk, % fn
				if (MonitorModifier){
					fn := this.WindowSizeMove.Bind(this, 1, {axis: axis, dir: dir})
					hotkey, % MonitorModifier hk, % fn
				}
				if (SwitchModifier){
					fn := this.TileSwitch.Bind(this, {axis: axis, dir: dir})
					hotkey, % SwitchModifier hk, % fn
				}
			}
		}
 
		; Bind Cycle Keys to cycle between windows in the same tile
		for dir, hk in CycleKeys {
			fn := this.TileCycle.Bind(this, dir)
			hotkey, % hk, % fn
		}
 
		this.LoadSettings()
	}
	
	; Called by user to configure monitors
	SetMonitorPosition(dest, direction, source){
		static directions := {North: {axis:"y", dir:-1}, South: {axis:"y", dir: 1}, West:{axis:"x", dir: -1}, East: {axis:"x", dir: 1}}
		axis := directions[direction].axis, dir := directions[direction].dir
		if (!ObjHasKey(this.MonitorMap, source))
			this.MonitorMap[source] := {x: {}, y: {}}
		this.MonitorMap[source, axis, dir] := dest
 
		if (!ObjHasKey(this.MonitorMap, dest))
			this.MonitorMap[dest] := {x: {}, y: {}}
		this.MonitorMap[dest, axis, dir * -1] := source
	}
	
	; === Methods called as a result of user interaction ===
	
	; Window Move or Size requested
	; Mode 0 = resize / move
	; Mode 1 = move monitor
	WindowSizeMove(mode, vector, hwnd := 0){
		static ModeName := {0: "Size", 1: "Move"}
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		axis := vector.axis
		dir := vector.dir
		if (ObjHasKey(this.windows, hwnd)){
			; Existing window
			window := this.windows[hwnd]
			monitor := window.monitor
			vectors := window.vectors.clone()
			this.log(ModeName[mode] " requested - Monitor: " monitor ", Dir: " this.VectorToCompass(vector) ", Window: " hwnd)
			if (mode){
				; Move mode
				if (!nm := this.NavigateMonitor(monitor, vector))
					return
				this.log("Moving " hwnd " to monitor " nm)
				this.monitors[monitor].RemoveWindow(window)
				monitor := nm
			} else if (window.vectors[vector.axis] == vector.dir){
				; Size issued when window is half size against that edge - move window to next monitor
				if (!(monitor := this.NavigateMonitor(monitor, vector))){
					; No monitor in that direction - abort
					return
				}
				vectors[vector.axis] *= -1
				this.log("Moving " hwnd " to monitor " monitor ", flipping " vector.axis)
			} else {
				vectors[vector.axis]:= this.AddDirections(vectors[vector.axis],vector.dir)
			}
			compass := this.VectorsToCompass(vectors)
			this.monitors[monitor].TileWindow(window, compass)
		} else {
			; New window
			window := this.AddWindow(hwnd)
			monitor := this.GetWindowMonitor(window)
			
			vectors := this.VectorToVectors(vector)
			this.log("New window " hwnd " is on monitor " monitor)
			this.monitors[monitor].TileWindow(window, this.VectorsToCompass(vectors))
		}
	}
	
	; User requested switch of focus to tile in specified direction
	TileSwitch(vector){
		hwnd := this.GetActiveHwnd()
		if (!window := this.windows[hwnd])
			return
		monitor_id := window.monitor
		vectors := window.vectors.clone()
		
		other_axis := this.OtherAxis(vector.axis)
		spans := (vectors[other_axis] = 0)
		bias := this.GetCaretEdge(window, other_axis)
		this.log("Tile Switch requested from: " this.VectorsToCompass(vectors) " monitor " monitor_id ", moving " this.VectorToCompass(vector) ", bias " this.VectorToCompass({axis:other_axis, dir: bias}))
		if (vectors[vector.axis] == 0 || vectors[vector.axis] == vector.dir){
			; Movement is towards monitor edge
			monitor_id := this.NavigateMonitor(monitor_id, vector)	; find next monitor in direction <dir>
			this.log("Movement is towards the " this.VectorToCompass(vector) " of monitor_id " window.monitor)
			if (!monitor_id){
				this.log("There is no monitor to move to, aborting")
				return
			}
			this.log("Changing monitor to " monitor_id)
		}
		vectors[vector.axis] := this.Invert(vectors[vector.axis]) ; flip alignment in direction of movement
		monitor := this.monitors[monitor_id]
		
		this.log("#1 - Trying " this.VectorsToCompass(vectors))
		compass := this.VectorsToCompass(vectors)
		if (nw := monitor.ActivateTile(compass)){
			this.log("MATCH " nw.hwnd " (" nw.title ")")
			return
		}

		if (spans && (monitor_id != window.monitor)){
			vectors[vector.axis] := this.Invert(vector.dir)
			compass := this.VectorsToCompass(vectors)
			this.log("#2 - Trying " this.VectorsToCompass(vectors))
			if (nw := monitor.ActivateTile(compass)){
				this.log("MATCH " nw.hwnd " (" nw.title ")")
				return
			}
		}

		vectors[vector.axis] := this.Invert(window.vectors[vector.axis])
		vectors[other_axis] := this.Alt(vectors[other_axis], bias)
		compass := this.VectorsToCompass(vectors)
		this.log("#3 - Trying " this.VectorsToCompass(vectors))
		if (nw := monitor.ActivateTile(compass)){
			this.log("MATCH " nw.hwnd " (" nw.title ")")
			return

		}

		if (spans){
			vectors := window.vectors.clone()
			vectors[vector.axis] := this.Invert(vector.dir)
			vectors[other_axis] := this.Alt(vectors[other_axis], bias)
			compass := this.VectorsToCompass(vectors)
			this.log("#4 - Trying " this.VectorsToCompass(vectors))
			if (nw := monitor.ActivateTile(compass)){
				this.log("MATCH " nw.hwnd " (" nw.title ")")
				return
			}

			; Repeat last test, opposite Bias
			vectors[other_axis] := this.Alt(window.vectors[other_axis], this.Invert(bias))
			compass := this.VectorsToCompass(vectors)
			this.log("#4a - Trying " this.VectorsToCompass(vectors))
			if (nw := monitor.ActivateTile(compass)){
				this.log("MATCH " nw.hwnd " (" nw.title ")")
				return
			}
		} else {
			vectors := window.vectors.clone()
			vectors[vector.axis] := this.Alt(vectors[vector.axis])
			vectors[other_axis] := this.Alt(vectors[other_axis])
			compass := this.VectorsToCompass(vectors)
			this.log("#4b - Trying " this.VectorsToCompass(vectors))
			if (nw := monitor.ActivateTile(compass)){
				this.log("MATCH " nw.hwnd " (" nw.title ")")
				return
			}
		}
	}
	
	; User requested Cycle between windows in a tile
	TileCycle(dir){
		hwnd := this.GetActiveHwnd()
		if (!window := this.windows[hwnd])
			return
		monitor := window.monitor
		this.monitors[monitor].TileCycle(window, dir)
	}
	
	; ====================== Private Methods ===================
	; Creates window object and adds it to list of windows
	AddWindow(hwnd){
		window := new CWindow(hwnd)
		this.windows[hwnd] := window
		this.log(ModeName[mode] "New window added - " hwnd " ( " window.title ")")
		return window
	}

	GetActiveHwnd(){
		WinGet, hwnd, ID, A
		return hwnd+0
	}
	
	; Returns the monitor number that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
	
	; Specify a monitor and a direction and it will tell you which monitor (if any) is there
	NavigateMonitor(monitor, vector){
		dest := this.MonitorMap[monitor, vector.axis, vector.dir]
		if (dest)
			return dest
		else
			return 0
	}

	GetCaretEdge(window, axis){
		wc := window.GetCenter()
		cp := {x: A_CaretX, y: A_CaretY}
		if (cp[axis] < wc[axis])
			return -1
		else if (cp[axis] > wc[axis])
			return 1
		else
			return 0
	}

	LoadSettings(){
		
	}
	
	SaveSettings(){
		
	}

}

; Represents one of your monitors, and the windows that have been tiled to it
Class CMonitor extends TileEngineBase {
	id := 0		; Monitor number
	HwndToCompass := {}	; Which Tile each window is in. Also used by other classes to tell if a window is on this monitor
	CompassToVectors := {n: {x:0,y:-1}, e: {x:1,y:0}, s: {x:0,y:1}, w: {x:-1,y:0}
		, ne: {x:1,y:-1}, se: {x:1,y:1}, sw: {x:-1,y:1}, nw: {x:-1,y:-1}, full: {x:0,y:0}}	; Conversion table for vectors to compass
	Tiles := {n: {}, e: {}, s: {}, w: {}, ne: {}, se: {}, sw: {}, nw: {}, full: {}}	; Compass-indexed array of windows in each tile
	TileOrders := {n: [], e: [], s: [], w: [], ne: [], se: [], sw: [], nw: [], full: []}
	
	; ============== Public Methods ===================
	__New(id){
		this.log("Creating Monitor " id)
		this.id := id
		this.coords := this.GetWorkArea()
		this.log("Work Area is " this.coords.w "x" this.coords.h)
	}
	
	; Request a window be placed in a certain tile
	TileWindow(window, compass){
		if (ObjHasKey(this.HwndToCompass, window.hwnd)){
			; Window already on this monitor, update
			this.RemoveWindow(window)
		}
		this.AddWindow(window, compass)
		
		static divisors := {-1: 2, 0: 1, 1: 2}
		;window.monitor.cx
		monitor := this.coords
 
		w := round(monitor.w / divisors[window.vectors.x])
		h := round(monitor.h / divisors[window.vectors.y])
		; if vectors.x is 1 then set position to center x, else left x
		x := window.vectors.x == 1 ? monitor.cx : monitor.l
		y := window.vectors.y == 1 ? monitor.cy : monitor.t
		this.log("Moving hwnd " window.hwnd " to x:" x ", y:" y ", w: " w ", h: " h)
		WinMove, % "ahk_id " window.hwnd,, x, y, w, h
	}
	
	; A request has been made to cycle windows in a tile
	TileCycle(window, dir){
		compass := this.HwndToCompass[window.hwnd]
		this.log("Cycling windows in tile " compass " on monitor " this.id)
		i := window.order + dir
		max := this.TileOrders[compass].length()
		if (i < 1)
			i := max
		else if (i > max)
			i := 1
		nw := this.TileOrders[compass, i]
		this.log("Activating window " nw.hwnd " (" nw.title ")")
		WinActivate, % "ahk_id " nw.hwnd
	}
	
	; Tries to activate the top-most window in the given tile
	; returns window object that was activated or 0 for none found
	ActivateTile(compass){
		window := this.GetTopMostTableInStack(compass)
		if (window)
			WinActivate, % "ahk_id " window.hwnd
		return window
	}
	
	; ===================== Private =======================
	AddWindow(window, compass){
		hwnd := window.hwnd
		this.Tiles[compass, hwnd] := window
		this.HwndToCompass[hwnd] := compass
		this.BuildTileList(compass)
		window.monitor := this.id
		window.vectors := this.CompassToVectors[compass]
	}
	
	BuildTileList(compass){
		this.TileOrders[compass] := []
		for hwnd, window in this.Tiles[compass] {
			this.TileOrders[compass].push(window)
			window.Order := A_Index
		}
	}
	
	RemoveWindow(window){
		hwnd := window.hwnd
		compass := this.HwndToCompass[window.hwnd]
		this.Tiles[compass].Delete(hwnd)
		this.HwndToCompass.Delete(hwnd)
		this.BuildTileList(compass)
		window.monitor := 0
		window.vectors := 0
	}
	
	; Gets the "Work Area" of a monito (The coordinates of the desktop on that monitor minus the taskbar)
	; also pre-calculates a few values derived from the coordinates
	GetWorkArea(){
		SysGet, coords_, MonitorWorkArea, % this.id
		out := {}
		out.l := coords_left
		out.r := coords_right
		out.t := coords_top
		out.b := coords_bottom
		out.w := coords_right - coords_left
		out.h := coords_bottom - coords_top
		out.cx := coords_left + round(out.w / 2)	; center x
		out.cy := coords_top + round(out.h / 2)		; center y
		out.hw := round(out.w / 2)	; half width
		out.hh := round(out.w / 2)	 ; half height
		return out
	}

	; GetZorderPosition / GetTopMostTableInStack by Guest3456 https://autohotkey.com/boards/viewtopic.php?p=92069#p92069
	;// gets the number/position of the window in the z-order 
	GetZorderPosition(hwnd)	{
	   PtrType := A_PtrSize ? "Ptr" : "UInt" ; AHK_L : AHK_Basic
	   z := 0
	   h := hwnd
	   while (h != 0)
	   {
		  h := DllCall("user32.dll\GetWindow", PtrType, h, "UInt", GW_HWNDPREV := 3)
		  ; WinGetTitle, title, ahk_id %h%
		  ; msgbox, h=%h%`ntitle=%title%
		  z++
	   }
	   return z
	}
	 
	GetTopMostTableInStack(compass)	{
	   prev_z := 999999
	   hwnd_lowest_z := 0
	   for hwnd, window in this.Tiles[compass] {
		  z := this.GetZorderPosition(hwnd)
		  ;WinGetTitle, title, ahk_id %A_LoopField%      
		  ;msgbox, checking`n`n%title%`n`nzorder = %z%
		  if (z < prev_z)
			 lowest_z := window
		  prev_z := z
	   }
	   return lowest_z
	}
}

; Represents a window that has been added by the tiling system
Class CWindow extends TileEngineBase {
	monitor := 0	; which monitor ID the window is on
	vectors := {}		; what alignment the window has
	Order := 0		; Window's Order in tile stack
	
	__New(hwnd){
		this.hwnd := hwnd
		WinGetTitle, title, % "ahk_id " hwnd
		this.title := title
	}
	
	; Gets the coordinates of the center of the window
	GetCenter(){
		WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
		cx := wx + round(ww / 2)
		cy := wy + round(wh / 2)
		return {x: cx, y: cy}
	}
}

; Helper functions that any class may use
Class TileEngineBase {
	; Debug logger to prefix string for filtering in DebugView
	Log(str){
		OutputDebug % "TileEngine| " str
	}
	
	; Converts vector objects ({axis: "x", dir: 1}) to compass types ("e")
	VectorToCompass(vector){
		static directions := {x: {-1: "w", 1: "e"}, y: {-1: "n", 1: "s"}}
		return directions[vector.axis, vector.dir]
	}
	
	; Converts a vector (eg: {axis: "x", dir: -1}) to a vectors (eg {x: -1, y: 0})
	VectorToVectors(vector){
		vectors := {x: 0, y: 0}
		vectors[vector.axis] := vector.dir
		return vectors
	}
	
	; Converts vectors objects ({x:1, y:0}) to compass types ("e")
	VectorsToCompass(vectors){
		static directions := {x: {-1: "w", 1: "e"}, y: {-1: "n", 1: "s"}}
		str := this.VectorToCompass({axis: "y", dir: vectors.y}) this.VectorToCompass({axis: "x", dir: vectors.x})
		return (str ? str : "full")
	}
	
	; Adds two -1/0/+1 directions together, but clamps to -1/0/+1
	AddDirections(dir, dir2){
		dir += dir2
		if dir > 1
			return 1
		if dir < -1
			return -1
		return dir
	}
	
	; Returns the other axis to the one passed
	OtherAxis(axis){
		static opposites := {x: "y", y: "x"}
		return opposites[axis]
	}
	
	; Inverts a vector
	Invert(vector){
		return vector * -1
	}
 
	; Returns the "Alternative" of a vector.
	; If vector is +1 or -1, return 0
	; else, return the bias (which should be either +1 or -1)
	Alt(vector, bias := 1){
		if (vector == 0)
			return bias
		return 0
	}
}
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

22 Jun 2016, 08:34

New version

On launch, detects windows that are already in a tiled position and adds them to the tiling database.
Coordinates for tiles are now calculated when the monitor is initialized, instead of each time a window is positioned.

Code: Select all

/*
Window Tiling system
Takes the default windows (Win+Arrow) hotkeys and makes them a bit better
Adds the ability to Tile windows into quarters / halves on both axes
Win+Alt+Arrow can be used to force a monitor change
Also adds a Win+Wheel hotkey to cycle between windows in the same Tile
*/
OutputDebug DBGVIEWCLEAR
#SingleInstance force
te := new TileEngine()
 
; Set monitor positions.
; The opposite (ie dest to source) is automatically added also
; My monitor order (L to R) : 1, 4, 2, 5, 3
;~ te.SetMonitorPosition(1, "West", 2)	; Monitor 1 is to the West of monitor 4
;~ te.SetMonitorPosition(2, "West", 4)	; Monitor 4 is to the West of monitor 2
;~ te.SetMonitorPosition(5, "East", 4)	; Monitor 5 is to the East of monitor 3
;~ te.SetMonitorPosition(3, "East", 5)	; Monitor 3 is to the East of monitor 2
;~ te.SetMonitorPosition(1, "East", 3)	; Monitor 1 is to the East of monitor 3 (Enable wrap)
 
;~ ; Standard 3 monitor (1,2,3 order):
te.SetMonitorPosition(1, "West", 2)	; Monitor 1 is to the West of monitor 2
te.SetMonitorPosition(2, "West", 4)	; Monitor 3 is to the East of monitor 2
te.SetMonitorPosition(5, "East", 4)	; Monitor 1 is to the East of monitor 3 (Enable wrap)

;~ te.SetMonitorPosition(1, "West", 2)	; Monitor 1 is to the West of monitor 2
;~ te.SetMonitorPosition(2, "West", 1)	; Monitor 3 is to the East of monitor 2
 
; A note about terms
; Vectors (eg {x:-1, y:0} meaning spanned along the left side of the monitor, aka "west")
; These are used to represent tile positions in a mathematical way that can be compared, altered etc
; For x, -1 is left, 1 is right. For y, -1 is up, 1 is down.
 
; Vector (eg {axis: "x", dir: -1} meaning "west" or "left")
; This is used for input or move calculations (in combination with Vectors arrays)
 
; "Compass" (ie n,e,s,w,ne,se,sw,nw)
; These are used where one "key" is needed to represent a tile's location
 
; Main class
Class TileEngine extends TileEngineBase {
	windows := {}
	monitors := []
	MonitorMap := {}
	MonitorCount := 0
	MonitorPrimary := 0
 
	__New(){
		static DirectionKeys := {x: {-1: "#Left", 1: "#Right"}, y: {-1: "#Up", 1: "#Down"}}
		static CycleKeys := {-1: "#WheelUp", 1: "#WheelDown"}
		static MonitorModifier := "+"
		static SwitchModifier := "^"
 
		this.log("Starting up")
 
		CoordMode, Caret, Screen
		CoordMode, Mouse, Screen
 
		Gui +Hwndhwnd
		this.hwnd := hwnd
		;~ DllCall( "RegisterShellHookWindow", UInt,hWnd )
		;~ MsgNum := DllCall( "RegisterWindowMessage", Str,"SHELLHOOK" )
		;~ fn := this.ShellMessage.Bind(this)
		;~ OnMessage( MsgNum, fn )
 
		this.IniFile := RegexReplace(A_ScriptName, "(.ahk|.exe)$", ".ini")
 
		; Query monitor info
		SysGet, MonitorCount, MonitorCount
		this.MonitorCount := MonitorCount
		SysGet, MonitorPrimary, MonitorPrimary
		this.MonitorPrimary := MonitorPrimary
		this.log(MonitorCount " monitors detected")
 
		; Create Monitor objects
		Loop % this.MonitorCount {
			this.monitors.push(new CMonitor(A_Index))
		}
 
		; Bind Direction Keys to WindowSizeMove
		for axis, hks in DirectionKeys {
			for dir, hk in hks {
				fn := this.WindowSizeMove.Bind(this, 0, {axis: axis, dir: dir})
				hotkey, % hk, % fn
				if (MonitorModifier){
					fn := this.WindowSizeMove.Bind(this, 1, {axis: axis, dir: dir})
					hotkey, % MonitorModifier hk, % fn
				}
				if (SwitchModifier){
					fn := this.TileSwitch.Bind(this, {axis: axis, dir: dir})
					hotkey, % SwitchModifier hk, % fn
				}
			}
		}
 
		; Bind Cycle Keys to cycle between windows in the same tile
		for dir, hk in CycleKeys {
			fn := this.TileCycle.Bind(this, dir)
			hotkey, % hk, % fn
		}
 
		; Add windows which are already located in a tile, but not added to the tiling system
		; (ie windows tiled in last run of script)
		Loop % this.MonitorCount {
			this.AddWindowsAlreadyOnMonitor(A_Index)
		}
		
	}
	
	AddWindowsAlreadyOnMonitor(monitor_id){
		static searchorder := ["full", "n", "e", "s", "w", "ne", "se", "sw", "nw"]
		monitor := this.monitors[monitor_id]
		WinGet, l, List
		windows := []
		Loop, % l {
			windows.push(l%A_Index%)
		}
		max := windows.length()
		
		Loop 9 {
			compass := searchorder[A_Index]
			tile := monitor.TileCoords[compass]
			Loop % max {
				hwnd := windows[A_Index]
				;this.log("Checking " hwnd)
				WinGetPos, x, y, w, h, % "ahk_id " hwnd
				WinGetTitle, title, % "ahk_id " hwnd
				if (x == tile.x && y == tile.y && w == tile.w && h == tile.h){
					this.log("Existing window " title " is in compass " compass " on monitor " this.id)
					if (!title)
						continue
					window := this.AddWindow(hwnd)
					monitor.AddWindow(window, compass)
				}
			}
		}
	}
 
	; Called by user to configure monitors
	SetMonitorPosition(dest, direction, source){
		static directions := {North: {axis:"y", dir:-1}, South: {axis:"y", dir: 1}, West:{axis:"x", dir: -1}, East: {axis:"x", dir: 1}}
		axis := directions[direction].axis, dir := directions[direction].dir
		if (!ObjHasKey(this.MonitorMap, source))
			this.MonitorMap[source] := {x: {}, y: {}}
		this.MonitorMap[source, axis, dir] := dest
 
		if (!ObjHasKey(this.MonitorMap, dest))
			this.MonitorMap[dest] := {x: {}, y: {}}
		this.MonitorMap[dest, axis, dir * -1] := source
	}
 
	; === Methods called as a result of user interaction ===
 
	; Window Move or Size requested
	; Mode 0 = resize / move
	; Mode 1 = move monitor
	WindowSizeMove(mode, vector, hwnd := 0){
		static ModeName := {0: "Size", 1: "Move"}
		if (hwnd == 0)
			hwnd := this.GetActiveHwnd()
		axis := vector.axis
		dir := vector.dir
		if (ObjHasKey(this.windows, hwnd)){
			; Existing window
			window := this.windows[hwnd]
			monitor := window.monitor
			vectors := window.vectors.clone()
			this.log(ModeName[mode] " requested - Monitor: " monitor ", Dir: " this.VectorToCompass(vector) ", Window: " hwnd)
			if (mode){
				; Move mode
				if (!nm := this.NavigateMonitor(monitor, vector))
					return
				this.log("Moving " hwnd " to monitor " nm)
				this.monitors[monitor].RemoveWindow(window)
				monitor := nm
			} else if (window.vectors[vector.axis] == vector.dir){
				; Size issued when window is half size against that edge - move window to next monitor
				if (!(monitor := this.NavigateMonitor(monitor, vector))){
					; No monitor in that direction - abort
					return
				}
				vectors[vector.axis] *= -1
				this.log("Moving " hwnd " to monitor " monitor ", flipping " vector.axis)
			} else {
				vectors[vector.axis]:= this.AddDirections(vectors[vector.axis],vector.dir)
			}
			compass := this.VectorsToCompass(vectors)
			this.monitors[monitor].TileWindow(window, compass)
		} else {
			; New window
			window := this.AddWindow(hwnd)
			monitor := this.GetWindowMonitor(window)
 
			vectors := this.VectorToVectors(vector)
			this.log("New window " hwnd " is on monitor " monitor)
			this.monitors[monitor].TileWindow(window, this.VectorsToCompass(vectors))
		}
	}
 
	; User requested switch of focus to tile in specified direction
	TileSwitch(vector){
		hwnd := this.GetActiveHwnd()
		if (!window := this.windows[hwnd])
			return
		monitor_id := window.monitor
		vectors := window.vectors.clone()
 
		other_axis := this.OtherAxis(vector.axis)
		spans := (vectors[other_axis] = 0)
		bias := this.GetCaretEdge(window, other_axis)
		this.log("Active window is " window.hwnd "(" window.title ")")
		this.log("Tile Switch requested from monitor " monitor_id ", moving " this.VectorToCompass(vector) ", bias " this.VectorToCompass({axis:other_axis, dir: bias}))
		searchorder := []
		if (vectors[vector.axis] == 0 || vectors[vector.axis] == vector.dir){
			; Movement is towards monitor edge
			monitor_id := this.NavigateMonitor(monitor_id, vector)	; find next monitor in direction <dir>
			this.log("Movement is towards the " this.VectorToCompass(vector) " of monitor_id " window.monitor)
			if (!monitor_id){
				this.log("There is no monitor to move to, aborting")
				return
			}
			this.log("Changing monitor to " monitor_id)
		}
		monitor := this.monitors[monitor_id]
		searchorder := this.GetSearchOrder(vectors, vector, bias)
		Loop % searchorder.length(){
			str .= searchorder[A_Index] ", "
		}
		this.log("SearchOrder: " str)
		Loop % searchorder.length(){
			if (nw := monitor.ActivateTile(searchorder[A_Index])){
				this.log("MATCH " nw.hwnd " (" nw.title ")")
				break
			}
		}
	}
	
	GetSearchOrder(original_vectors, vector, bias){
		vectors := original_vectors.clone()
		other_axis := this.OtherAxis(vector.axis)
		spans := (vectors[other_axis] = 0)
		move_monitor := 0
		order := []
		if (vectors[vector.axis] == 0 || vectors[vector.axis] == vector.dir){
			order := this.AddOrder(order, "full")
			move_monitor := 1
		}
		vectors[vector.axis] := this.Invert(vectors[vector.axis]) ; flip alignment in direction of movement
		order := this.AddOrder(order, this.VectorsToCompass(vectors))
		
		if (spans && move_monitor){
			vectors[vector.axis] := this.Invert(vector.dir)
			compass := this.VectorsToCompass(vectors)
			order := this.AddOrder(order, this.VectorsToCompass(vectors))
		}
		
		vectors[vector.axis] := this.Invert(original_vectors[vector.axis])
		vectors[other_axis] := this.Alt(vectors[other_axis], bias)
		compass := this.VectorsToCompass(vectors)
		order := this.AddOrder(order, this.VectorsToCompass(vectors))
		
		if (spans){
			vectors := original_vectors.clone()
			vectors[vector.axis] := this.Invert(vector.dir)
			vectors[other_axis] := this.Alt(vectors[other_axis], bias)
			compass := this.VectorsToCompass(vectors)
			order := this.AddOrder(order, this.VectorsToCompass(vectors))

			vectors[other_axis] := this.Alt(original_vectors[other_axis], this.Invert(bias))
			compass := this.VectorsToCompass(vectors)
			order := this.AddOrder(order, this.VectorsToCompass(vectors))
		} else {
			vectors := original_vectors.clone()
			vectors[vector.axis] := this.Alt(vectors[vector.axis])
			vectors[other_axis] := this.Alt(vectors[other_axis])
			compass := this.VectorsToCompass(vectors)
			order := this.AddOrder(order, this.VectorsToCompass(vectors))
		}
		
		return order
	}
	
	AddOrder(order, compass){
		Loop % order.length(){
			if (order[A_Index] == compass)
				return order
		}
		order.push(compass)
		return order
	}
 
	; User requested Cycle between windows in a tile
	TileCycle(dir){
		hwnd := this.GetActiveHwnd()
		if (!window := this.windows[hwnd])
			return
		monitor := window.monitor
		this.monitors[monitor].TileCycle(window, dir)
	}
 
	; ====================== Private Methods ===================
	; Creates window object and adds it to list of windows
	AddWindow(hwnd){
		window := new CWindow(hwnd)
		this.windows[hwnd] := window
		this.log(ModeName[mode] "New window added - " hwnd " ( " window.title ")")
		return window
	}
 
	GetActiveHwnd(){
		WinGet, hwnd, ID, A
		return hwnd+0
	}
 
	; Returns the monitor number that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
 
	; Specify a monitor and a direction and it will tell you which monitor (if any) is there
	NavigateMonitor(monitor, vector){
		dest := this.MonitorMap[monitor, vector.axis, vector.dir]
		if (dest)
			return dest
		else
			return 0
	}
 
	GetCaretEdge(window, axis){
		wc := window.GetCenter()
		cp := {x: A_CaretX, y: A_CaretY}
		if (cp[axis] < wc[axis])
			return -1
		else if (cp[axis] > wc[axis])
			return 1
		else
			return 0
	}
}
 
; Represents one of your monitors, and the windows that have been tiled to it
Class CMonitor extends TileEngineBase {
	id := 0		; Monitor number
	HwndToCompass := {}	; Which Tile each window is in. Also used by other classes to tell if a window is on this monitor
	CompassToVectors := {n: {x:0,y:-1}, e: {x:1,y:0}, s: {x:0,y:1}, w: {x:-1,y:0}
		, ne: {x:1,y:-1}, se: {x:1,y:1}, sw: {x:-1,y:1}, nw: {x:-1,y:-1}, full: {x:0,y:0}}	; Conversion table for vectors to compass
	Tiles := {n: {}, e: {}, s: {}, w: {}, ne: {}, se: {}, sw: {}, nw: {}, full: {}}	; Compass-indexed array of windows in each tile
	TileOrders := {n: [], e: [], s: [], w: [], ne: [], se: [], sw: [], nw: [], full: []}
	TileCoords := {n: {}, e: {}, s: {}, w: {}, ne: {}, se: {}, sw: {}, nw: {}, full: {}}	; Coordinates for each tile
 
	; ============== Public Methods ===================
	__New(id){
		this.log("Creating Monitor " id)
		this.id := id
		this.coords := this.GetWorkArea()
		this.log("Work Area is " this.coords.w "x" this.coords.h)
		this.BuildTileCoords()
	}

	BuildTileCoords(){
		static divisors := {-1: 2, 0: 1, 1: 2}
		monitor := this.coords
		for compass, nothing in this.TileOrders {
			vectors := this.CompassToVectors[compass]
			w := round(monitor.w / divisors[vectors.x])
			h := round(monitor.h / divisors[vectors.y])
			; if vectors.x is 1 then set position to center x, else left x
			x := vectors.x == 1 ? monitor.cx : monitor.l
			y := vectors.y == 1 ? monitor.cy : monitor.t
			this.TileCoords[compass] := {x: x, y: y, w: w, h: h}
		}
	}
	
	; Request a window be placed in a certain tile
	TileWindow(window, compass){
		if (ObjHasKey(this.HwndToCompass, window.hwnd)){
			; Window already on this monitor, update
			this.RemoveWindow(window)
		}
		this.AddWindow(window, compass)
		c := this.TileCoords[compass]
		this.log("Moving hwnd " window.hwnd " to x:" c.x ", y:" c.y ", w: " c.w ", h: " c.h)
		WinMove, % "ahk_id " window.hwnd,, c.x, c.y, c.w, c.h
	}
 
	; A request has been made to cycle windows in a tile
	TileCycle(window, dir){
		compass := this.HwndToCompass[window.hwnd]
		this.log("Cycling windows in tile " compass " on monitor " this.id)
		i := window.order + dir
		max := this.TileOrders[compass].length()
		if (i < 1)
			i := max
		else if (i > max)
			i := 1
		nw := this.TileOrders[compass, i]
		this.log("Activating window " nw.hwnd " (" nw.title ")")
		WinActivate, % "ahk_id " nw.hwnd
	}
 
	; Tries to activate the top-most window in the given tile
	; returns window object that was activated or 0 for none found
	ActivateTile(compass){
		window := this.GetTopMostTableInStack(compass)
		if (window)
			WinActivate, % "ahk_id " window.hwnd
		return window
	}
 
	; ===================== Private =======================
	AddWindow(window, compass){
		hwnd := window.hwnd
		this.Tiles[compass, hwnd] := window
		this.HwndToCompass[hwnd] := compass
		this.BuildTileList(compass)
		window.monitor := this.id
		window.vectors := this.CompassToVectors[compass]
	}
 
	BuildTileList(compass){
		this.TileOrders[compass] := []
		for hwnd, window in this.Tiles[compass] {
			this.TileOrders[compass].push(window)
			window.Order := A_Index
		}
	}
 
	RemoveWindow(window){
		hwnd := window.hwnd
		compass := this.HwndToCompass[window.hwnd]
		this.Tiles[compass].Delete(hwnd)
		this.HwndToCompass.Delete(hwnd)
		this.BuildTileList(compass)
		window.monitor := 0
		window.vectors := 0
	}
 
	; Gets the "Work Area" of a monito (The coordinates of the desktop on that monitor minus the taskbar)
	; also pre-calculates a few values derived from the coordinates
	GetWorkArea(){
		SysGet, coords_, MonitorWorkArea, % this.id
		out := {}
		out.l := coords_left
		out.r := coords_right
		out.t := coords_top
		out.b := coords_bottom
		out.w := coords_right - coords_left
		out.h := coords_bottom - coords_top
		out.cx := coords_left + round(out.w / 2)	; center x
		out.cy := coords_top + round(out.h / 2)		; center y
		out.hw := round(out.w / 2)	; half width
		out.hh := round(out.w / 2)	 ; half height
		return out
	}
 
	; GetZorderPosition / GetTopMostTableInStack by Guest3456 https://autohotkey.com/boards/viewtopic.php?p=92069#p92069
	;// gets the number/position of the window in the z-order 
	GetZorderPosition(hwnd)	{
	   PtrType := A_PtrSize ? "Ptr" : "UInt" ; AHK_L : AHK_Basic
	   z := 0
	   h := hwnd
	   while (h != 0)
	   {
		  h := DllCall("user32.dll\GetWindow", PtrType, h, "UInt", GW_HWNDPREV := 3)
		  ; WinGetTitle, title, ahk_id %h%
		  ; msgbox, h=%h%`ntitle=%title%
		  z++
	   }
	   return z
	}
 
	GetTopMostTableInStack(compass)	{
	   prev_z := 999999
	   hwnd_lowest_z := 0
	   for hwnd, window in this.Tiles[compass] {
		  z := this.GetZorderPosition(hwnd)
		  ;WinGetTitle, title, ahk_id %A_LoopField%      
		  ;msgbox, checking`n`n%title%`n`nzorder = %z%
		  if (z < prev_z)
			 lowest_z := window
		  prev_z := z
	   }
	   return lowest_z
	}
}
 
; Represents a window that has been added by the tiling system
Class CWindow extends TileEngineBase {
	monitor := 0	; which monitor ID the window is on
	vectors := {}		; what alignment the window has
	Order := 0		; Window's Order in tile stack
 
	__New(hwnd){
		this.hwnd := hwnd
		WinGetTitle, title, % "ahk_id " hwnd
		this.title := title
	}
 
	; Gets the coordinates of the center of the window
	GetCenter(){
		WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
		cx := wx + round(ww / 2)
		cy := wy + round(wh / 2)
		return {x: cx, y: cy}
	}
}
 
; Helper functions that any class may use
Class TileEngineBase {
	; Debug logger to prefix string for filtering in DebugView
	Log(str){
		OutputDebug % "TileEngine| " str
	}
 
	; Converts vector objects ({axis: "x", dir: 1}) to compass types ("e")
	VectorToCompass(vector){
		static directions := {x: {-1: "w", 1: "e"}, y: {-1: "n", 1: "s"}}
		return directions[vector.axis, vector.dir]
	}
 
	; Converts a vector (eg: {axis: "x", dir: -1}) to a vectors (eg {x: -1, y: 0})
	VectorToVectors(vector){
		vectors := {x: 0, y: 0}
		vectors[vector.axis] := vector.dir
		return vectors
	}
 
	; Converts vectors objects ({x:1, y:0}) to compass types ("e")
	VectorsToCompass(vectors){
		static directions := {x: {-1: "w", 1: "e"}, y: {-1: "n", 1: "s"}}
		str := this.VectorToCompass({axis: "y", dir: vectors.y}) this.VectorToCompass({axis: "x", dir: vectors.x})
		return (str ? str : "full")
	}
 
	; Adds two -1/0/+1 directions together, but clamps to -1/0/+1
	AddDirections(dir, dir2){
		dir += dir2
		if dir > 1
			return 1
		if dir < -1
			return -1
		return dir
	}
 
	; Returns the other axis to the one passed
	OtherAxis(axis){
		static opposites := {x: "y", y: "x"}
		return opposites[axis]
	}
 
	; Inverts a vector
	Invert(vector){
		return vector * -1
	}
 
	; Returns the "Alternative" of a vector.
	; If vector is +1 or -1, return 0
	; else, return the bias (which should be either +1 or -1)
	Alt(vector, bias := 1){
		if (vector == 0)
			return bias
		return 0
	}
}
jacola

Re: Win+Arrow Window Tiling System

06 Nov 2016, 20:43

This works very well for the current monitor. Is there a trick to jump the window to the next monitor?

Thank you!
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

07 Nov 2016, 04:58

Your monitor locations need to be set up. In my experience, the monitor numbers are not the same as those shown in the windows display settings though :(
You need to configure the appropriate te.SetMonitorPosition(1, "West", 2) ; Monitor 1 is to the West of monitor 2 style lines at the start of the script.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Win+Arrow Window Tiling System

23 Jun 2017, 18:28

Got a prototype for a new system...
I wanted to find a way to describe, with a few keystrokes, any kind of move or resize for a tiling system and I think I may have nailed it.

It works like this:
ALT + Arrow Keys = Move Window

If you want to resize a window into (or out of) another tile, you add another modifier.
If moving up/down, add Shift or Ctrl (They are arranged vertically)
ALT + SHIFT + Up/Down = Resize top edge
ALT + CTRL + Up/Down = Resize bottom edge

If moving left/right, add Ctrl or Win (They are arranged horizontally)
ALT + CTRL + Left/Right = Resize left edge
ALT + WIN + Left/Right = Resize right edge

Using this system, you can move tiles around and resize them in a really efficient manner.

I added a system to parse your monitors and work out their layout - it still currently only works with horizontally arranged monitors, but any number of them.

I also packaged it as an app - no code editing needed, and you can configure how many rows and columns you want on the fly (one setting for all monitors for now).

I have not yet implemented an initial window placement scheme (ie what it does with a previously untiled window) - on first keypress it will tile to top left of monitor. I guess some sort of "best fit" (ie make the least changes possible to the size and pos to add it to the tiles)

It also does not work around Win10's annoying invisible borders yet. I see some code that just got bumped which may deal with that though.

There is also no "garbage collection". It does not detect when a window is destroyed and remove it from the array.
Should be very simple though.

Looking for feedback on this one - does it conflict with any common keystrokes? Text selection hotkeys do not seem to conflict, well not the ones I use as a coder anyway (Shift and/or ctrl and arrow keys). Does monitor layout detection work ok? My 5 monitor setup (1x landcape with 2x portrait each side) is in a crazy order according to AHK, so I am hoping it is pretty bulletproof.

Code: Select all

#SingleInstance force
OutputDebug DBGVIEWCLEAR

GoSub, ConfigMinimizeToTray

te := new TileEngine()
return

GuiClose:
ExitApp

; About axes, vectors and edges and the controls

; Moving occurs along an axis - it is initiated by holding ALT and hitting an arrow key
;	If you hit left or right, that's a move along the x axis
;	If you hit up/down, that's that's a move along the y axis
; Movement is in the direction of a vector
;	-1 is towards the origin, so left or up
;	+1 is away from the origin, so right or down
;
; Resizing operates upon an edge, along an axis in the direction of a vector.
; eg the right edge moves to the right, sizing up the window horizontally
; = +1 edge of x axis moves in vector +1
; It is initiated by holding an additional modifier when you press an arrow key
; The edge is selected according to the combination of the arrow key and the modifiers held
; When the up/down arrows are used, Shift and Ctrl represent the top and bottom edges
; When the left/right arrows are used, Ctrl and Win represent the left and right edges.

class TileEngine {
    MonitorOrder := []
	MonitorRows := 0
	MonitorCols := 0
    Monitors := []
    TiledWindows := {}
    
    __New(){
		this.IniFile := RegExReplace(A_ScriptName, "\.exe|\.ahk", ".ini")
		this.LoadSettings()

		fn := this.RowColChanged.Bind(this)

        Gui, +hwndhwnd
        this.hwnd := hwnd
		
		;~ Gui, Add, Text, w300 Center, -Settings-
		Gui, Add, GroupBox, w250 h50 Center, Settings
		Gui, Add, Text, x40 yp+25 w50, Rows
		Gui, Add, Edit, x+5 yp-3 hwndhRowsEdit, % this.MonitorRows ? this.MonitorRows : 2
		this.hRowsEdit := hRowsEdit
		GuiControl, +g, % this.hRowsEdit, % fn
        
		Gui, Add, Text, x+20 yp+3 w50, Columns
		Gui, Add, Edit, x+5 yp-3 hwndhColsEdit, % this.MonitorCols ? this.MonitorCols : 2
		this.hColsEdit := hColsEdit
		GuiControl, +g, % this.hColsEdit, % fn

		Gui, Add, GroupBox, xm y+10 w250 h100 Center, Instructions
		text =
		(
ALT + Arrow Keys = Move Window
ALT + SHIFT + Up/Down = Resize top edge
ALT + CTRL + Up/Down = Resize bottom edge
ALT + CTRL + Left/Right = Resize left edge
ALT + WIN + Left/Right = Resize right edge
		)
		Gui, Add, Text, xp+5 yp+25, % text
		
		Gui, Show, Hide, TileEngine
        SysGet, MonitorCount, MonitorCount
        this.MonitorCount := MonitorCount
        
        Loop % this.MonitorCount {
            this.Monitors.push(new this.CMonitor(A_Index))
        }

        if (this.MonitorOrder.length() != this.MonitorCount){
			this.SetupMonitorLayout()
        }
		
        this.Init()
        
		this.RowColChanged()
		
		if (this.MonitorRows != 0 && this.MonitorCols != 0){
			GoSub, OnMinimizeButton
		}
    }
	
	LoadSettings(){
		IniRead, MonitorRows, % this.IniFile, Settings, MonitorRows, 0
		if (MonitorRows != "ERROR"){
			this.MonitorRows := MonitorRows
		}
		IniRead, MonitorCols, % this.IniFile, Settings, MonitorCols, 0
		if (MonitorCols != "ERROR"){
			this.MonitorCols := MonitorCols
		}
	}

    Init(){
        ; === MOVE ===
        ; West
        fn := this.MoveWindow.Bind(this, "x", -1)
        hotkey, !Left, % fn
        
        ; East
        fn := this.MoveWindow.Bind(this, "x", 1)
        hotkey, !Right, % fn
        
        ; North
        fn := this.MoveWindow.Bind(this, "y", -1)
        hotkey, !Up, % fn
        
        ; South
        fn := this.MoveWindow.Bind(this, "y", 1)
        hotkey, !Down, % fn
        
        ; === SIZE ===
        ; west edge west
        fn := this.SizeWindow.Bind(this, "x", -1, -1)
        hotkey, ^!Left, % fn
        
        ; west edge east
        fn := this.SizeWindow.Bind(this, "x", -1, 1)
        hotkey, !^Right, % fn
        
        ; east edge west
        fn := this.SizeWindow.Bind(this, "x", 1, -1)
        hotkey, !#Left, % fn
        
        ; east edge east
        fn := this.SizeWindow.Bind(this, "x", 1, 1)
        hotkey, !#Right, % fn
        
        ; north edge north
        fn := this.SizeWindow.Bind(this, "y", -1, -1)
        hotkey, !+Up, % fn
        
        ; north edge south
        fn := this.SizeWindow.Bind(this, "y", -1, 1)
        hotkey, !+Down, % fn
        
        ; south edge south
        fn := this.SizeWindow.Bind(this, "y", 1, 1)
        hotkey, !^Down, % fn
        
        ; south edge north
        fn := this.SizeWindow.Bind(this, "y", 1, -1)
        hotkey, !^Up, % fn
    }
    
	; curr = Monitor ID (AHK monitor #)
	; vector = direction to look in
	; Returns monitor Object
	GetNextMonitor(curr, vector){
		found := 0
		for i, monid in this.MonitorOrder {
			if (monid == curr){
				found := 1
				break
			}
		}
		if (!found)
			return curr
		i += vector
		if (i > this.MonitorCount)
			i := 1
		else if (i < 1)
			i := this.MonitorCount
		return this.Monitors[this.MonitorOrder[i]]
	}

	; Takes a Monitor ID (AHK Monitor ID)
	; Returns a Monitor ORDER (Monitor 1 = LeftMost)
	GetMonitorOrder(mon){
		found := 0
		for i, monid in this.MonitorOrder {
			if (monid == mon){
				found := 1
				break
			}
		}
		if (found){
			return i
		} else {
			return mon
		}
	}
	
	RowColChanged(){
		GuiControlGet, rows, , % this.hRowsEdit
		this.MonitorRows := rows
		GuiControlGet, cols, , % this.hColsEdit
		this.MonitorCols := cols
		IniWrite, % this.MonitorRows, % this.IniFile, Settings, MonitorRows
		IniWrite, % this.MonitorCols, % this.IniFile, Settings, MonitorCols
		for i, mon in this.Monitors {
			mon.SetRows(this.MonitorRows)
			mon.SetCols(this.MonitorCols)
		}
	}
	
	; Makes sure a window is in the windows array
	; Returns the window class for that window
    GetWindow(hwnd){
        hwnd := WinExist("A")
        if (!this.TiledWindows.HasKey(hwnd)){
            this.TiledWindows[hwnd] := new this.CWindow(hwnd)
        }
        return this.TiledWindows[hwnd]
    }
    
	; Moves a window along a specified axis in the direction of a specified vector
    MoveWindow(axis, vector){
        win := this.GetWindow(hwnd)
        
        if (win.CurrentMonitor == 0){
			win.CurrentMonitor := this.Monitors[this.GetWindowMonitor(win)]
            win.Pos.x := 1
            win.Pos.y := 1
            win.Span.x := 1
            win.Span.y := 1
			new_pos := 1
        } else {
			mon := win.CurrentMonitor
			new_pos := win.Pos[axis] + vector
			if ((new_pos + win.Span[axis] - 1) > mon.TileCount[axis]){
				if (axis == "y")
					return
				new_pos := 1
				mon := this.GetNextMonitor(mon.id, vector)
				win.CurrentMonitor := mon
			} else if (new_pos <= 0){
				if (axis == "y")
					return
				mon := this.GetNextMonitor(mon.id, vector)
				win.CurrentMonitor := mon
				new_pos := (mon.TileCount[axis] - win.Span[axis]) + (vector * -1)
			}
        }
        Win.Pos[axis] := new_pos
        this.TileWindow(win)
    }
    
	; Sizes a window by moving and edge along a specific axis in the direction of a specified vector
    SizeWindow(axis, edge, vector){
        win := this.GetWindow(hwnd)
        if (win.CurrentMonitor == 0){
            win.CurrentMonitor := this.Monitors[this.GetWindowMonitor(win)]
            win.Pos.x := 1
            win.Pos.y := 1
            win.Span.x := 1
            win.Span.y := 1
        } else {
			new_pos := win.Pos[axis]
			new_span := win.Span[axis]
			mon := win.CurrentMonitor
			if (edge == -1){
				; Change in span causes change in pos
				if ((vector == 1 && win.Span[axis] != 1) || (vector == -1 && win.Pos[axis] != 1)){
					new_span += (vector * -1)
					new_pos += vector
				}
			} else {
				new_span += (vector * edge)
			}
			if ((new_span == 0) || ((new_pos + new_span - 1) > mon.TileCount[axis])){
				return
			}
        }
        
		OutputDebug % "AHK| SIZE - Axis: " axis ", Edge: " edge ", Vector: " vector " / New Span: " new_span ", New Pos: " new_pos
		win.Span[axis] := new_span
		win.Pos[axis] := new_pos
		
        this.TileWindow(win)
    }
	
	; Request a window be placed in a certain tile
	TileWindow(win){
        mon := win.CurrentMonitor
		x := mon.TileCoords.x[win.Pos.x]
		y :=  mon.TileCoords.y[win.Pos.y]
		w := mon.TileSizes.x * win.Span.x
		h := mon.TileSizes.y * win.Span.y
		WinMove, % "ahk_id " win.hwnd, , x , y, w, h
		;WinGetTitle, t, "ahk_id " win.hwnd
		WinGetActiveTitle, t
		OutputDebug % "AHK| Window '" t "' Monitor: " this.GetMonitorOrder(mon.id) ", PosX: " win.Pos.x ", PosY: " win.Pos.y ", SpanCols: " win.Span.x ", SpanRows: " win.Span.y
	}
    
	; Returns the monitor number that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window on this monitor?
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
	
    SetupMonitorLayout(){
		tmp := {}
		for i, mon in this.Monitors {
			tmp[mon.Coords.l] := i
		}
		for i, id in tmp {
			this.MonitorOrder.push(id)
		}
	}
	
	Join(sep, arr) {
		for index,param in arr
			str .= param . sep
		return SubStr(str, 1, -StrLen(sep))
	}
    
   
    class CMonitor {
        TileCoords := {x: [], y: []}
        TileSizes := {x: 0, y: 0}
        TileCount := {x: 2, y: 2}
        
        __New(id){
            this.id := id
            this.Coords := this.GetWorkArea()
        }
        
        SetRows(rows){
			this.TileCoords.y := []
            this.TileCount.y := rows
            this.TileSizes.y := this.Coords.h / rows
            o := this.coords.t
            Loop % rows {
                this.TileCoords.y.push(o)
                o += this.TileSizes.y
            }
        }
        
        SetCols(cols){
			this.TileCoords.x := []
            this.TileCount.x := cols
            this.TileSizes.x := this.Coords.w / cols
            o := this.coords.l
            Loop % cols {
                this.TileCoords.x.push(o)
                o += this.TileSizes.x
            }
        }
        
        ; Gets the "Work Area" of a monitor (The coordinates of the desktop on that monitor minus the taskbar)
        ; also pre-calculates a few values derived from the coordinates
        GetWorkArea(){
            SysGet, coords_, MonitorWorkArea, % this.id
            out := {}
            out.l := coords_left
            out.r := coords_right
            out.t := coords_top
            out.b := coords_bottom
            out.w := coords_right - coords_left
            out.h := coords_bottom - coords_top
            out.cx := coords_left + round(out.w / 2)	; center x
            out.cy := coords_top + round(out.h / 2)		; center y
            out.hw := round(out.w / 2)	; half width
            out.hh := round(out.w / 2)	 ; half height
            return out
        }
    }
    
    class CWindow {
        CurrentMonitor := 0
        Pos := {x: 1, y: 1}
        Span := {x: 1, y: 1}
        
        __New(hwnd){
            this.hwnd := hwnd
        }
        
        ; Gets the coordinates of the center of the window
        GetCenter(){
            WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
            cx := wx + round(ww / 2)
            cy := wy + round(wh / 2)
            return {x: cx, y: cy}
        }
    }
}

; Minimze to tray by SKAN http://www.autohotkey.com/board/topic/32487-simple-minimize-to-tray/

ConfigMinimizeToTray:
	Gui, +hwndGui1
	Menu("Tray","Nostandard"), Menu("Tray","Add","Restore","GuiShow"), Menu("Tray","Add")
	Menu("Tray","Default","Restore"), Menu("Tray","Click",1), Menu("Tray","Standard")
	OnMessage(0x112, "WM_SYSCOMMAND")

WM_SYSCOMMAND(wParam){
	If ( wParam = 61472 ) {
		SetTimer, OnMinimizeButton, -1
		Return 0
	}
}

Menu( MenuName, Cmd, P3="", P4="", P5="" ) {
	Menu, %MenuName%, %Cmd%, %P3%, %P4%, %P5%
	Return errorLevel
}

OnMinimizeButton:
	MinimizeGuiToTray( R, Gui1 )
	Menu("Tray","Icon")
	Return

GuiShow:
	DllCall("DrawAnimatedRects", UInt,Gui1, Int,3, UInt,&R+16, UInt,&R )
	Menu("Tray","NoIcon")
	Gui, Show
	Return

MinimizeGuiToTray( ByRef R, hGui ) {
	WinGetPos, X0,Y0,W0,H0, % "ahk_id " (Tray:=WinExist("ahk_class Shell_TrayWnd"))
	ControlGetPos, X1,Y1,W1,H1, TrayNotifyWnd1,ahk_id %Tray%
	SW:=A_ScreenWidth,SH:=A_ScreenHeight,X:=SW-W1,Y:=SH-H1,P:=((Y0>(SH/3))?("B"):(X0>(SW/3))
	? ("R"):((X0<(SW/3))&&(H0<(SH/3)))?("T"):("L")),((P="L")?(X:=X1+W0):(P="T")?(Y:=Y1+H0):)
	VarSetCapacity(R,32,0), DllCall( "GetWindowRect",UInt,hGui,UInt,&R)
	NumPut(X,R,16), NumPut(Y,R,20), DllCall("RtlMoveMemory",UInt,&R+24,UInt,&R+16,UInt,8 )
	DllCall("DrawAnimatedRects", UInt,hGui, Int,3, UInt,&R, UInt,&R+16 )
	WinHide, ahk_id %hGui%
}
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Hotkey driven window Tiling scripts

24 Jun 2017, 18:57

New version.

A whole load of improvements, and a name for this new version - "SquAeroSnap".

Changed default grid size from 2x2 to 4x4 - it just feels better to me
Initial placement implemented - it tries to fit the window into the tiling system as best it can.
Correction for Aero "invisible borders" implemented (Configurable)
"Ignore first move" mode added (move / size hotkey on new window just does initial placement and ignores move)
Simple / Advanced hotkeys now available

Advanced hotkey mode is as before
Simple hotkey mode is thus:

Win + Arrows = Move
Win + Ctrl + Arrows = Resize (Always bottom-right corner)

[Edit: Updated with better handling for maximized widows]

Code: Select all

#SingleInstance force
;~ OutputDebug DBGVIEWCLEAR

GoSub, ConfigMinimizeToTray

sqas := new SquAreoSnap()
return

;~ ^Esc::
GuiClose:
ExitApp
; ========================= Concepts used in this code ================================

; == Axes, Vectors and Edges ==
; Moving occurs along an axis
;	If you hit left or right, that's a move along the x axis
;	If you hit up/down, that's that's a move along the y axis
; Movement is in the direction of a vector
;	-1 is towards the origin, so left or up
;	+1 is away from the origin, so right or down
;
; Resizing operates upon an edge, along an axis in the direction of a vector.
; eg the right edge moves to the right, sizing up the window horizontally
; = +1 edge of x axis moves in vector +1
; It is initiated by holding an additional modifier when you press an arrow key

; === Monitor Index (ID) and Order ===
; AHK gives each monitor an Index (Starting with 1, counting up)
; These Indexes however are not guaranteed to be in the same order as they are physically arranged
; Monitor "Order" is the physical order they are arranged in (1 being the left-most)

; === Pos and Span ===
; A given window has Pos and Span attributes for each axis
; Pos is the position of the winow along that axis: 1 is the left/top-most tile
; Span is how many tiles that window covers along that axis

class SquAreoSnap {
    MonitorOrder := []
	MonitorRows := 4
	MonitorCols := 4
	AeroXMod := 7
	AeroYMod := 4
    Monitors := []
    TiledWindows := {}
	IgnoreFirstMove := 1
	CurrentHotkeyMode := 0
	
	Axes := {x: 1, y: 2}
	AxisToWh := {x: "w", y: "h"}
    
    __New(){
		this.IniFile := RegExReplace(A_ScriptName, "\.exe|\.ahk", ".ini")

        Gui, +hwndhwnd
        this.hwnd := hwnd
		
		; === Gui ===
		Gui, Add, GroupBox, w250 h100 Center Section, General Settings

		; -- Rows --
		Gui, Add, Text, xs+20 yp+25 w50, Rows
		Gui, Add, Edit, x+5 yp-3 w40 hwndhRowsEdit
		this.hRowsEdit := hRowsEdit
        
		; -- Columns --
		Gui, Add, Text, x+20 yp+3 w50, Columns
		Gui, Add, Edit, x+5 yp-3 w40 hwndhColsEdit
		this.hColsEdit := hColsEdit

		; -- Ignore first move --
		Gui, Add, CheckBox, % "xs+20 y+10 hwndhIgnoreFirstmove AltSubmit", Ignore first move or size, just snap to tile(s)
		this.hIgnoreFirstmove := hIgnoreFirstmove
		
		; -- Hotkey Mode --
		Gui, Add, CheckBox, % "xs+20 y+10 hwndhHotkeyMode AltSubmit ", Advanced hotkeys
		this.hHotkeyMode := hHotkeyMode

		Gui, Add, GroupBox, xm y+20 w250 h50 Center Section, Aero Settings
		; -- X Offset --
		Gui, Add, Text, xs+20 yp+25 w50, X Offset
		Gui, Add, Edit, x+5 yp-3 w40 hwndhAeroXMod
		this.hAeroXMod := hAeroXMod
        
		; -- Y Offset --
		Gui, Add, Text, x+20 yp+3 w50, Y Offset
		Gui, Add, Edit, x+5 yp-3 w40 hwndhAeroYMod
		this.hAeroYMod := hAeroYMod
	
		
		; -- Instructions --
		Gui, Add, GroupBox, xm y+20 w250 h140 Center, Hotkeys
		Gui, Add, Text, xp+1 yp+25 w245 R8 hwndhHotkeyInstructions Center
		this.hHotkeyInstructions := hHotkeyInstructions
		
		; === Initialize Monitors ===
        SysGet, MonitorCount, MonitorCount
        this.MonitorCount := MonitorCount
        
        Loop % this.MonitorCount {
            this.Monitors.push(new this.CMonitor(A_Index))
        }

        if (this.MonitorOrder.length() != this.MonitorCount){
			this.SetupMonitorLayout()
        }
		this.UpdateMonitorTileConfiguration()
		
		; === Load Settings ===
		settings_loaded := this.LoadSettings()
		Gui, Show, Hide, SquAreoSnap

		; === Minimze to tray if not first run, else show Gui ===
		if (!settings_loaded){
			GoSub, OnMinimizeButton
		} else {
			Gui, Show
		}
		
		; === Enable GuiControl Callbacks ===
		this.SetGuiControlCallbackState(1)
    }
	
	; Enables / Disables callbacks for GuiControls
	SetGuiControlCallbackState(state){
		if (state){
			fn := this.RowColChanged.Bind(this)
			GuiControl, +g, % this.hRowsEdit, % fn
			GuiControl, +g, % this.hColsEdit, % fn

			fn := this.IgnoreFirstMoveChanged.Bind(this)
			GuiControl, +g, % this.hIgnoreFirstmove, % fn
			
			fn := this.HotkeyModeChanged.Bind(this)
			GuiControl, +g, % this.hHotkeyMode, % fn
			
			fn := this.AeroModChanged.Bind(this)
			GuiControl, +g, % this.hAeroXMod, % fn
			GuiControl, +g, % this.hAeroYMod, % fn
		} else {
			GuiControl, -g, % this.hRowsEdit
			GuiControl, -g, % this.hColsEdit
			GuiControl, -g, % hIgnoreFirstmove
			GuiControl, -g, % hHotkeyMode
		}
	}
	
	; --------------------------------- Inital Setup -------------------------------
	LoadSettings(){
		hotkey_mode := 1
		if (FileExist(this.IniFile)){
			first_run := 0
		} else {
			first_run := 1
			FileAppend, % "", % this.IniFile
		}
		if (!first_run){
			IniRead, MonitorRows, % this.IniFile, Settings, MonitorRows
			if (MonitorRows != "ERROR"){
				this.MonitorRows := MonitorRows
			}
			IniRead, MonitorCols, % this.IniFile, Settings, MonitorCols
			if (MonitorCols != "ERROR"){
				this.MonitorCols := MonitorCols
			}
			IniRead, IgnoreFirstMove, % this.IniFile, Settings, IgnoreFirstMove
			if (IgnoreFirstMove != "ERROR"){
				this.IgnoreFirstMove := IgnoreFirstMove
			}
			IniRead, CurrentHotkeyMode, % this.IniFile, Settings, CurrentHotkeyMode
			if (CurrentHotkeyMode != "ERROR"){
				hotkey_mode := CurrentHotkeyMode
			}
		}
		
		; Initialize hotkeys
		this.SetHotkeyState(hotkey_mode)
		
		; Update the GuiControls
		GuiControl, , % this.hRowsEdit, % this.MonitorRows
		GuiControl, , % this.hColsEdit, % this.MonitorCols
		GuiControl, , % this.hIgnoreFirstmove, % this.IgnoreFirstMove
		GuiControl, , % this.hHotkeyMode, % this.CurrentHotkeyMode - 1
		GuiControl, , % this.hAeroXMod, % this.AeroXMod
		GuiControl, , % this.hAeroYMod, % this.AeroYMod
		
		return first_run
	}
	
	; Turns On or Off hotkeys, or sets the mode
    SetHotkeyState(state){
		static AxisKeys := {x: {-1: "Left", 1: "Right"}, y: {-1: "Up", 1: "Down"}}
		
		if (state == this.CurrentHotkeyMode){
			return
		}

		; If we are already in a mode and trying to change to a new mode...
		; ... then loop twice - once to disable the old hotkeys, once to declare new hotkeys
		loop_count := (this.CurrentHotkeyMode != 0 && state) ? 2 : 1
		
		Loop % loop_count {
			if (this.CurrentHotkeyMode == 0 && state){
				; Declare hotkeys
				clearing_hotkeys := 0
				hotkey_set := state
			} else {
				; Remove hotkeys
				clearing_hotkeys := 1
				hotkey_set := this.CurrentHotkeyMode
			}
			
			if (hotkey_set == 1){
				; Simple mode
				basemod := "#"
				EdgeModifiers := {x: {-1: "+", 1: "^"}, y: {-1: "+", 1: "^"}}
				Edges := [1]
			} else {
				; Advanced mode
				basemod := "!"
				EdgeModifiers := {x: {-1: "^", 1: "#"}, y: {-1: "+", 1: "^"}}
				Edges := [-1, 1]
			}
			
			for axis, vectors in AxisKeys {
				for vector, key in vectors {
					fn := clearing_hotkeys ? "Off" : this.MoveWindow.Bind(this, axis, vector)
					hotkey, % basemod key , % fn
					for i, edge in Edges {
						edgemod := EdgeModifiers[axis, edge]
						fn := clearing_hotkeys ? "Off" : this.SizeWindow.Bind(this, axis, edge, vector)
						hotkey, % basemod edgemod key, % fn
					}
				}
			}
			
			if (clearing_hotkeys){
				this.CurrentHotkeyMode := 0
			} else {
				this.CurrentHotkeyMode := state
			}
			this.SetHotkeyInstructions()
		}
    }
	
	; Updates the Gui to show hotkeys
	SetHotkeyInstructions(){
		if (this.CurrentHotkeyMode == 1){
			text =
			(
Base modifier of WIN to Move
Add Ctrl to Resize bottom right corner

WIN + Arrow Keys = Move Window
WIN + CTRL + Up/Down = Resize bottom edge
WIN + CTRL + Left/Right = Resize right edge
			)
		} else if (this.CurrentHotkeyMode == 2) {
			text =
			(
Base modifier of ALT to Move
Add extra modifiers to select edge to Resize

ALT + Arrow Keys = Move Window
ALT + SHIFT + Up/Down = Resize top edge
ALT + CTRL + Up/Down = Resize bottom edge
ALT + CTRL + Left/Right = Resize left edge
ALT + WIN + Left/Right = Resize right edge
			)
		}
		GuiControl, , % this.hHotkeyInstructions, % text
	}
    
	; Called on startup to work out physical layout of monitors
    SetupMonitorLayout(){
		tmp := {}
		for i, mon in this.Monitors {
			tmp[mon.Coords.l] := i
		}
		for i, id in tmp {
			this.MonitorOrder.push(id)
		}
	}
	
	; Instruct all monitors to pre-calculate their tile locations
	UpdateMonitorTileConfiguration(){
		for i, mon in this.Monitors {
			mon.SetRows(this.MonitorRows)
			mon.SetCols(this.MonitorCols)
		}
	}
	
	; ------------------------------- Window placement, movement and sizing ------------------------------
	; Called when a hotkey is hit to detect the current window
    GetWindow(){
        hwnd := WinExist("A")
        if (this.TiledWindows.HasKey(hwnd)){
			win := this.TiledWindows[hwnd]
		} else {
			win := new this.CWindow(hwnd)
        }
        return win
    }

	; Initializes a window if needed.
	; Returns 1 to indicate that the window is new
	InitWindow(win){
        if (this.TiledWindows.HasKey(win.hwnd)){
			return 0
		} else {
            this.TiledWindows[win.hwnd] := win
			win.CurrentMonitor := this.Monitors[this.GetWindowMonitor(win)]
			this.FitWindowToTiles(win)
			return 1
		}
	}
	
	; Works out initial placement for a window
	FitWindowToTiles(win){
		mon := win.CurrentMonitor
		coords := win.GetLocalCoords()

		for axis, unused in this.Axes {
			w_h := this.AxisToWh[axis] ; convert "x" or "y" to "w" or "h"
			; Work out initial position
			tile_pos := floor(coords[axis] / mon.TileSizes[axis]) + 1
			win.Pos[axis] := tile_pos
			
			; Work out how many tiles this window would fill if tiled
			num_tiles := floor(coords[w_h] / mon.TileSizes[axis])
			num_tiles := num_tiles ? num_tiles : 1	; minimum tile size of 1
			win.Span[axis] := num_tiles
			
			; Clamp window to max of full width of the axis
			if (win.Span[axis] > mon.TileCount[axis]){
				win.Span[axis] := mon.TileCount[axis]
			}
			
			; If window would extend off-screen on this axis, move it towards the origin
			sizediff := ((win.Pos[axis] + win.Span[axis]) - mon.TileCount[axis]) - 1
			if (sizediff > 0){
				win.Pos[axis] -= sizediff
			}
		}
		this.TileWindow(win)
	}
	
	; Moves a window along a specified axis in the direction of a specified vector
    MoveWindow(axis, vector){
        win := this.GetWindow()
		
		if (this.InitWindow(win) && this.IgnoreFirstMove){
			return
		}
		mon := win.CurrentMonitor
		
		new_pos := win.Pos[axis] + vector
		
		if ((new_pos + win.Span[axis] - 1) > mon.TileCount[axis]){
			if (axis == "y")
				return
			new_pos := 1
			mon := this.GetNextMonitor(mon.id, vector)
			win.CurrentMonitor := mon
		} else if (new_pos <= 0){
			if (axis == "y")
				return
			mon := this.GetNextMonitor(mon.id, vector)
			win.CurrentMonitor := mon
			new_pos := (mon.TileCount[axis] - win.Span[axis]) + (vector * -1)
		}
        Win.Pos[axis] := new_pos
        this.TileWindow(win)
    }
    
	; Sizes a window by moving and edge along a specific axis in the direction of a specified vector
    SizeWindow(axis, edge, vector){
        win := this.GetWindow()
		
		if (this.InitWindow(win) && this.IgnoreFirstMove){
			return
		}

		mon := win.CurrentMonitor
		
		new_pos := win.Pos[axis], new_span := win.Span[axis]
		
		if (edge == -1){
			; Change in span causes change in pos
			if ((vector == 1 && win.Span[axis] != 1) || (vector == -1 && win.Pos[axis] != 1)){
				new_span += (vector * -1)
				new_pos += vector
			}
		} else {
			new_span += (vector * edge)
		}
		if ((new_span == 0) || ((new_pos + new_span - 1) > mon.TileCount[axis])){
			return
		}
       
		;~ OutputDebug % "AHK| SIZE - Axis: " axis ", Edge: " edge ", Vector: " vector " / New Span: " new_span ", New Pos: " new_pos
		
		win.Span[axis] := new_span, win.Pos[axis] := new_pos
		
        this.TileWindow(win)
    }
	
	; Request a window be placed in it's designated tile
	TileWindow(win){
        mon := win.CurrentMonitor
		
		; Work around Aero invisible borders
		wmod := this.AeroXMod * 2
		hmod := win.Pos.y == 0 ? this.AeroYMod : this.AeroYMod * 2
		x := mon.TileCoords.x[win.Pos.x] - this.AeroXMod
		y := mon.TileCoords.y[win.Pos.y]
		w := (mon.TileSizes.x * win.Span.x) + wmod
		h := (mon.TileSizes.y * win.Span.y) + hmod
		
		; If window is minimized or maximized, restore
		WinGet, MinMax, MinMax, % "ahk_id " win.hwnd
		if (MinMax != 0)
			WinRestore, % "ahk_id " win.hwnd

		WinMove, % "ahk_id " win.hwnd, , x, y, w, h
		;~ OutputDebug % "AHK| Window Tile - PosX: " win.Pos.x ", PosY: " win.Pos.y ", SpanCols: " win.Span.x ", SpanRows: " win.Span.y
		;~ OutputDebug % "AHK| Window Coords - X: " x ", Y: " y ", W: " w ", H: " h
	}

	; -------------------------- Helper Functions ----------------------------
	; Returns a monitor object in a given vector
	; curr = Monitor ID (AHK monitor #)
	; vector = direction to look in
	; Returns monitor Object
	GetNextMonitor(curr, vector){
		found := 0
		for i, monid in this.MonitorOrder {
			if (monid == curr){
				found := 1
				break
			}
		}
		if (!found)
			return curr
		i += vector
		if (i > this.MonitorCount)
			i := 1
		else if (i < 1)
			i := this.MonitorCount
		return this.Monitors[this.MonitorOrder[i]]
	}

	; Takes a Monitor ID (AHK Monitor ID)
	; Returns a Monitor ORDER (Monitor 1 = LeftMost)
	GetMonitorOrder(mon){
		found := 0
		for i, monid in this.MonitorOrder {
			if (monid == mon){
				found := 1
				break
			}
		}
		if (found){
			return i
		} else {
			return mon
		}
	}
	
	; Returns the Monitor Index that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window on this monitor?
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
	
	; ------------------------- GuiControl handling ----------------------------------
	RowColChanged(){
		GuiControlGet, rows, , % this.hRowsEdit
		this.MonitorRows := rows
		GuiControlGet, cols, , % this.hColsEdit
		this.MonitorCols := cols
		IniWrite, % this.MonitorRows, % this.IniFile, Settings, MonitorRows
		IniWrite, % this.MonitorCols, % this.IniFile, Settings, MonitorCols
		this.UpdateMonitorTileConfiguration()
	}
	
	IgnoreFirstMoveChanged(){
		GuiControlGet, setting, , % this.hIgnoreFirstMove
		this.IgnoreFirstMove := setting
		IniWrite, % this.IgnoreFirstMove, % this.IniFile, Settings, IgnoreFirstMove
	}

	HotkeyModeChanged(){
		GuiControlGet, setting, , % this.hHotkeyMode
		this.SetHotkeyState(setting + 1)
		IniWrite, % this.CurrentHotkeyMode, % this.IniFile, Settings, CurrentHotkeyMode
	}
	
	AeroModChanged(){
		GuiControlGet, setting, , % this.hAeroXMod
		this.AeroXMod := setting
		GuiControlGet, setting, , % this.hAeroYMod
		this.AeroYMod := setting
		IniWrite, % this.AeroXMod, % this.IniFile, Settings, AeroXMod
		IniWrite, % this.AeroYMod, % this.IniFile, Settings, AeroYMod
	}
	
	; -------------------------------------------- Monitor Class ---------------------------------
    class CMonitor {
		ID := 0 ;	The Index (AHK Monitor ID) of the monitor
        TileCoords := {x: [], y: []}
        TileSizes := {x: 0, y: 0}
        TileCount := {x: 2, y: 2}
        
        __New(id){
            this.id := id
            this.Coords := this.GetWorkArea()
        }
        
        SetRows(rows){
			this.TileCoords.y := []
            this.TileCount.y := rows
            this.TileSizes.y := round(this.Coords.h / rows)
            o := this.coords.t
            Loop % rows {
                this.TileCoords.y.push(o)
                o += this.TileSizes.y
            }
        }
        
        SetCols(cols){
			this.TileCoords.x := []
            this.TileCount.x := cols
            this.TileSizes.x := round(this.Coords.w / cols)
            o := this.coords.l
            Loop % cols {
                this.TileCoords.x.push(o)
                o += this.TileSizes.x
            }
        }
        
        ; Gets the "Work Area" of a monitor (The coordinates of the desktop on that monitor minus the taskbar)
        ; also pre-calculates a few values derived from the coordinates
        GetWorkArea(){
            SysGet, coords_, MonitorWorkArea, % this.id
            out := {}
            out.l := coords_left
            out.r := coords_right
            out.t := coords_top
            out.b := coords_bottom
            out.w := coords_right - coords_left
            out.h := coords_bottom - coords_top
            out.cx := coords_left + round(out.w / 2)	; center x
            out.cy := coords_top + round(out.h / 2)		; center y
            out.hw := round(out.w / 2)	; half width
            out.hh := round(out.w / 2)	 ; half height
            return out
        }
    }
    
	; ----------------------------------- Window Class ----------------------------------
    class CWindow {
        CurrentMonitor := 0	; Will point to monitor OBJECT when this window is tiled
        Pos := {x: 1, y: 1}
        Span := {x: 1, y: 1}
        
		AxisToOriginEdge := {x: "l", y: "t"}
		Axes := {x: 1, y: 2}

        __New(hwnd){
            this.hwnd := hwnd
        }
        
		GetCoords(){
			WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
			return {x: wx, y: wy, w: ww, h: wh}
		}
		
		GetLocalCoords(){
			coords := this.GetCoords()
			wa := this.CurrentMonitor.GetWorkArea()
			for axis, unused in this.Axes {
				l_t := this.AxisToOriginEdge[axis]
				coords[axis] := abs(wa[l_t] - coords[axis])
			}
			;~ coords.x := mon.coords.x - coords.x, coords.x := mon.coords.y - coords.y, coords.x := mon.coords.w - coords.w, coords.x := mon.coords.h - coords.h
			return coords
		}
		
        ; Gets the coordinates of the center of the window
        GetCenter(){
			w := this.GetCoords()
            cx := w.x + round(w.w / 2)
            cy := w.y + round(w.h / 2)
            return {x: cx, y: cy}
        }
    }
}

; Minimze to tray by SKAN http://www.autohotkey.com/board/topic/32487-simple-minimize-to-tray/

ConfigMinimizeToTray:
	Gui, +hwndGui1
	Menu("Tray","Nostandard"), Menu("Tray","Add","Restore","GuiShow"), Menu("Tray","Add")
	Menu("Tray","Default","Restore"), Menu("Tray","Click",1), Menu("Tray","Standard")
	OnMessage(0x112, "WM_SYSCOMMAND")

WM_SYSCOMMAND(wParam){
	If ( wParam = 61472 ) {
		SetTimer, OnMinimizeButton, -1
		Return 0
	}
}

Menu( MenuName, Cmd, P3="", P4="", P5="" ) {
	Menu, %MenuName%, %Cmd%, %P3%, %P4%, %P5%
	Return errorLevel
}

OnMinimizeButton:
	MinimizeGuiToTray( R, Gui1 )
	Menu("Tray","Icon")
	Return

GuiShow:
	DllCall("DrawAnimatedRects", UInt,Gui1, Int,3, UInt,&R+16, UInt,&R )
	Menu("Tray","NoIcon")
	Gui, Show
	Return

MinimizeGuiToTray( ByRef R, hGui ) {
	WinGetPos, X0,Y0,W0,H0, % "ahk_id " (Tray:=WinExist("ahk_class Shell_TrayWnd"))
	ControlGetPos, X1,Y1,W1,H1, TrayNotifyWnd1,ahk_id %Tray%
	SW:=A_ScreenWidth,SH:=A_ScreenHeight,X:=SW-W1,Y:=SH-H1,P:=((Y0>(SH/3))?("B"):(X0>(SW/3))
	? ("R"):((X0<(SW/3))&&(H0<(SH/3)))?("T"):("L")),((P="L")?(X:=X1+W0):(P="T")?(Y:=Y1+H0):)
	VarSetCapacity(R,32,0), DllCall( "GetWindowRect",UInt,hGui,UInt,&R)
	NumPut(X,R,16), NumPut(Y,R,20), DllCall("RtlMoveMemory",UInt,&R+24,UInt,&R+16,UInt,8 )
	DllCall("DrawAnimatedRects", UInt,hGui, Int,3, UInt,&R, UInt,&R+16 )
	WinHide, ahk_id %hGui%
}
vasili111
Posts: 747
Joined: 21 Jan 2014, 02:04
Location: Georgia

Re: Hotkey driven window tiling scripts

28 Jun 2017, 04:09

Have you looked at https://github.com/fuhsjr00/bug.n ? Maybe it is better to merge these two scripts?
DRAKON-AutoHotkey: Visual programming for AutoHotkey.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: Hotkey driven window tiling scripts

21 Jul 2017, 06:11

Not sure I want to merge it vasili, but it had some useful code - namely a WinMove equivalent that works correctly with Aero invisible borders.
The coding style is pretty horrific though, I had some real trouble working out what parts of the code did.

Whatever, here is a newer version which automatically works around Aero invisible borders.
I also settled on a new hotkey system which I feel is the best balance of simplicity and power:

WIN + Arrows = move
WIN + Shift + Arrows = resize top left corner
WIN + Ctrl + Arrows = resize bottom right corner

Code: Select all

#SingleInstance force
;~ OutputDebug DBGVIEWCLEAR

GoSub, ConfigMinimizeToTray

sqas := new SquAeroSnap()
return

;~ ^Esc::
GuiClose:
ExitApp
; ========================= Concepts used in this code ================================

; == Axes, Vectors and Edges ==
; Moving occurs along an axis
;	If you hit left or right, that's a move along the x axis
;	If you hit up/down, that's that's a move along the y axis
; Movement is in the direction of a vector
;	-1 is towards the origin, so left or up
;	+1 is away from the origin, so right or down
;
; Resizing operates upon an edge, along an axis in the direction of a vector.
; eg the right edge moves to the right, sizing up the window horizontally
; = +1 edge of x axis moves in vector +1
; It is initiated by holding an additional modifier when you press an arrow key

; === Monitor Index (ID) and Order ===
; AHK gives each monitor an Index (Starting with 1, counting up)
; These Indexes however are not guaranteed to be in the same order as they are physically arranged
; Monitor "Order" is the physical order they are arranged in (1 being the left-most)

; === Pos and Span ===
; A given window has Pos and Span attributes for each axis
; Pos is the position of the winow along that axis: 1 is the left/top-most tile
; Span is how many tiles that window covers along that axis

class SquAeroSnap {
    MonitorOrder := []
	MonitorRows := 4
	MonitorCols := 4
    Monitors := []
    TiledWindows := {}
	IgnoreFirstMove := 1
	
	Axes := {x: 1, y: 2}
	AxisToWh := {x: "w", y: "h"}
    
    __New(){
		this.IniFile := RegExReplace(A_ScriptName, "\.exe|\.ahk", ".ini")

        Gui, +hwndhwnd
        this.hwnd := hwnd
		
		; === Gui ===
		Gui, Add, GroupBox, w250 h75 Center Section, General Settings

		; -- Rows --
		Gui, Add, Text, xs+20 yp+25 w50, Rows
		Gui, Add, Edit, x+5 yp-3 w40 hwndhRowsEdit
		this.hRowsEdit := hRowsEdit
        
		; -- Columns --
		Gui, Add, Text, x+20 yp+3 w50, Columns
		Gui, Add, Edit, x+5 yp-3 w40 hwndhColsEdit
		this.hColsEdit := hColsEdit

		; -- Ignore first move --
		Gui, Add, CheckBox, % "xs+20 y+10 hwndhIgnoreFirstmove AltSubmit", Ignore first move or size, just snap to tile(s)
		this.hIgnoreFirstmove := hIgnoreFirstmove
		
		; -- Instructions --
		Gui, Add, GroupBox, xm y+20 w250 h140 Center, Hotkeys
		Gui, Add, Text, xp+1 yp+25 w245 R8 hwndhHotkeyInstructions Center
		this.hHotkeyInstructions := hHotkeyInstructions
		
		; === Initialize Monitors ===
        SysGet, MonitorCount, MonitorCount
        this.MonitorCount := MonitorCount
        
        Loop % this.MonitorCount {
            this.Monitors.push(new this.CMonitor(A_Index))
        }

        if (this.MonitorOrder.length() != this.MonitorCount){
			this.SetupMonitorLayout()
        }
		this.UpdateMonitorTileConfiguration()
		
		; === Load Settings ===
		settings_loaded := this.LoadSettings()
		Gui, Show, Hide, SquAeroSnap

		; === Minimze to tray if not first run, else show Gui ===
		if (!settings_loaded){
			GoSub, OnMinimizeButton
		} else {
			Gui, Show
		}
		
		; === Enable GuiControl Callbacks ===
		this.SetGuiControlCallbackState(1)
    }
	
	; Enables / Disables callbacks for GuiControls
	SetGuiControlCallbackState(state){
		if (state){
			fn := this.RowColChanged.Bind(this)
			GuiControl, +g, % this.hRowsEdit, % fn
			GuiControl, +g, % this.hColsEdit, % fn

			fn := this.IgnoreFirstMoveChanged.Bind(this)
			GuiControl, +g, % this.hIgnoreFirstmove, % fn
		} else {
			GuiControl, -g, % this.hRowsEdit
			GuiControl, -g, % this.hColsEdit
			GuiControl, -g, % hIgnoreFirstmove
		}
	}
	
	; --------------------------------- Inital Setup -------------------------------
	LoadSettings(){
		hotkey_mode := 1
		if (FileExist(this.IniFile)){
			first_run := 0
		} else {
			first_run := 1
			FileAppend, % "", % this.IniFile
		}
		if (!first_run){
			IniRead, MonitorRows, % this.IniFile, Settings, MonitorRows
			if (MonitorRows != "ERROR"){
				this.MonitorRows := MonitorRows
			}
			IniRead, MonitorCols, % this.IniFile, Settings, MonitorCols
			if (MonitorCols != "ERROR"){
				this.MonitorCols := MonitorCols
			}
			IniRead, IgnoreFirstMove, % this.IniFile, Settings, IgnoreFirstMove
			if (IgnoreFirstMove != "ERROR"){
				this.IgnoreFirstMove := IgnoreFirstMove
			}
		}
		
		; Initialize hotkeys
		this.SetHotkeyState()
		
		; Update the GuiControls
		GuiControl, , % this.hRowsEdit, % this.MonitorRows
		GuiControl, , % this.hColsEdit, % this.MonitorCols
		GuiControl, , % this.hIgnoreFirstmove, % this.IgnoreFirstMove
		
		return first_run
	}
	
	; Turns On or Off hotkeys, or sets the mode
    SetHotkeyState(){
		fn := this.MoveWindow.Bind(this, "x", 1)
		hotkey, #Right, % fn
		
		fn := this.MoveWindow.Bind(this, "x", -1)
		hotkey, #Left, % fn
		
		fn := this.MoveWindow.Bind(this, "y", 1)
		hotkey, #Down, % fn
		
		fn := this.MoveWindow.Bind(this, "y", -1)
		hotkey, #Up, % fn
		
		fn := this.SizeWindow.Bind(this, "x", 1, 1)
		hotkey, #^Right, % fn
		
		fn := this.SizeWindow.Bind(this, "x", 1, -1)
		hotkey, #^Left, % fn
		
		fn := this.SizeWindow.Bind(this, "x", -1, 1)
		hotkey, #+Right, % fn
		
		fn := this.SizeWindow.Bind(this, "x", -1, -1)
		hotkey, #+Left, % fn
		
		fn := this.SizeWindow.Bind(this, "y", 1, 1)
		hotkey, #^Down, % fn
		
		fn := this.SizeWindow.Bind(this, "y", 1, -1)
		hotkey, #^Up, % fn
		
		fn := this.SizeWindow.Bind(this, "y", -1, 1)
		hotkey, #+Down, % fn
		
		fn := this.SizeWindow.Bind(this, "y", -1, -1)
		hotkey, #+Up, % fn
		
		this.SetHotkeyInstructions()
    }
	
	; Updates the Gui to show hotkeys
	SetHotkeyInstructions(){
		text =
		(
Base modifier of WIN to Move
Add Ctrl to Resize bottom right corner
Add Shift to Resize top left corner

WIN + Arrow Keys = Move Window
WIN + CTRL + Up/Down = Resize bottom edge
WIN + CTRL + Left/Right = Resize right edge
WIN + SHIFT + Up/Down = Resize top edge
WIN + SHIFT + Left/Right = Resize left edge
		)
		GuiControl, , % this.hHotkeyInstructions, % text
	}
    
	; Called on startup to work out physical layout of monitors
    SetupMonitorLayout(){
		tmp := {}
		for i, mon in this.Monitors {
			tmp[mon.Coords.l] := i
		}
		for i, id in tmp {
			this.MonitorOrder.push(id)
		}
	}
	
	; Instruct all monitors to pre-calculate their tile locations
	UpdateMonitorTileConfiguration(){
		for i, mon in this.Monitors {
			mon.SetRows(this.MonitorRows)
			mon.SetCols(this.MonitorCols)
		}
	}
	
	; ------------------------------- Window placement, movement and sizing ------------------------------
	; Called when a hotkey is hit to detect the current window
    GetWindow(){
        hwnd := WinExist("A")
        if (this.TiledWindows.HasKey(hwnd)){
			win := this.TiledWindows[hwnd]
		} else {
			win := new this.CWindow(hwnd)
        }
        return win
    }

	; Initializes a window if needed.
	; Returns 1 to indicate that the window is new
	InitWindow(win){
        if (this.TiledWindows.HasKey(win.hwnd)){
			return 0
		} else {
            this.TiledWindows[win.hwnd] := win
			win.CurrentMonitor := this.Monitors[this.GetWindowMonitor(win)]
			this.FitWindowToTiles(win)
			return 1
		}
	}
	
	; Works out initial placement for a window
	FitWindowToTiles(win){
		mon := win.CurrentMonitor
		coords := win.GetLocalCoords()

		for axis, unused in this.Axes {
			w_h := this.AxisToWh[axis] ; convert "x" or "y" to "w" or "h"
			; Work out initial position
			tile_pos := floor(coords[axis] / mon.TileSizes[axis]) + 1
			win.Pos[axis] := tile_pos
			
			; Work out how many tiles this window would fill if tiled
			num_tiles := floor(coords[w_h] / mon.TileSizes[axis])
			num_tiles := num_tiles ? num_tiles : 1	; minimum tile size of 1
			win.Span[axis] := num_tiles
			
			; Clamp window to max of full width of the axis
			if (win.Span[axis] > mon.TileCount[axis]){
				win.Span[axis] := mon.TileCount[axis]
			}
			
			; If window would extend off-screen on this axis, move it towards the origin
			sizediff := ((win.Pos[axis] + win.Span[axis]) - mon.TileCount[axis]) - 1
			if (sizediff > 0){
				win.Pos[axis] -= sizediff
			}
		}
		this.TileWindow(win)
	}
	
	; Moves a window along a specified axis in the direction of a specified vector
    MoveWindow(axis, vector){
        win := this.GetWindow()
		
		if (this.InitWindow(win) && this.IgnoreFirstMove){
			return
		}
		mon := win.CurrentMonitor
		
		new_pos := win.Pos[axis] + vector
		
		if ((new_pos + win.Span[axis] - 1) > mon.TileCount[axis]){
			if (axis == "y")
				return
			new_pos := 1
			mon := this.GetNextMonitor(mon.id, vector)
			win.CurrentMonitor := mon
		} else if (new_pos <= 0){
			if (axis == "y")
				return
			mon := this.GetNextMonitor(mon.id, vector)
			win.CurrentMonitor := mon
			new_pos := (mon.TileCount[axis] - win.Span[axis]) + (vector * -1)
		}
        Win.Pos[axis] := new_pos
        this.TileWindow(win)
    }
    
	; Sizes a window by moving and edge along a specific axis in the direction of a specified vector
    SizeWindow(axis, edge, vector){
        win := this.GetWindow()
		
		if (this.InitWindow(win) && this.IgnoreFirstMove){
			return
		}

		mon := win.CurrentMonitor
		
		new_pos := win.Pos[axis], new_span := win.Span[axis]
		
		if (edge == -1){
			; Change in span causes change in pos
			if ((vector == 1 && win.Span[axis] != 1) || (vector == -1 && win.Pos[axis] != 1)){
				new_span += (vector * -1)
				new_pos += vector
			}
		} else {
			new_span += (vector * edge)
		}
		if ((new_span == 0) || ((new_pos + new_span - 1) > mon.TileCount[axis])){
			return
		}
       
		;~ OutputDebug % "AHK| SIZE - Axis: " axis ", Edge: " edge ", Vector: " vector " / New Span: " new_span ", New Pos: " new_pos
		
		win.Span[axis] := new_span, win.Pos[axis] := new_pos
		
        this.TileWindow(win)
    }
	
	; Request a window be placed in it's designated tile
	TileWindow(win){
        mon := win.CurrentMonitor
		
		x := mon.TileCoords.x[win.Pos.x]
		y := mon.TileCoords.y[win.Pos.y]
		w := (mon.TileSizes.x * win.Span.x)
		h := (mon.TileSizes.y * win.Span.y)
		
		; If window is minimized or maximized, restore
		WinGet, MinMax, MinMax, % "ahk_id " win.hwnd
		if (MinMax != 0)
			WinRestore, % "ahk_id " win.hwnd

		;~ WinMove, % "ahk_id " win.hwnd, , x, y, w, h
		Window_move(win.hwnd, x, y, w, h)
		;~ OutputDebug % "AHK| Window Tile - PosX: " win.Pos.x ", PosY: " win.Pos.y ", SpanCols: " win.Span.x ", SpanRows: " win.Span.y
		;~ OutputDebug % "AHK| Window Coords - X: " x ", Y: " y ", W: " w ", H: " h
	}

	; -------------------------- Helper Functions ----------------------------
	; Returns a monitor object in a given vector
	; curr = Monitor ID (AHK monitor #)
	; vector = direction to look in
	; Returns monitor Object
	GetNextMonitor(curr, vector){
		found := 0
		for i, monid in this.MonitorOrder {
			if (monid == curr){
				found := 1
				break
			}
		}
		if (!found)
			return curr
		i += vector
		if (i > this.MonitorCount)
			i := 1
		else if (i < 1)
			i := this.MonitorCount
		return this.Monitors[this.MonitorOrder[i]]
	}

	; Takes a Monitor ID (AHK Monitor ID)
	; Returns a Monitor ORDER (Monitor 1 = LeftMost)
	GetMonitorOrder(mon){
		found := 0
		for i, monid in this.MonitorOrder {
			if (monid == mon){
				found := 1
				break
			}
		}
		if (found){
			return i
		} else {
			return mon
		}
	}
	
	; Returns the Monitor Index that the center of the window is on
	GetWindowMonitor(window){
		c := window.GetCenter()
		Loop % this.monitors.length() {
			m := this.monitors[A_Index].coords
			; Is the top-left corner of the window on this monitor?
			if (c.x >= m.l && c.x <= m.r && c.y >= m.t && c.y <= m.b){
				return A_Index
			}
		}
		return 0
	}
	
	; ------------------------- GuiControl handling ----------------------------------
	RowColChanged(){
		GuiControlGet, rows, , % this.hRowsEdit
		this.MonitorRows := rows
		GuiControlGet, cols, , % this.hColsEdit
		this.MonitorCols := cols
		IniWrite, % this.MonitorRows, % this.IniFile, Settings, MonitorRows
		IniWrite, % this.MonitorCols, % this.IniFile, Settings, MonitorCols
		this.UpdateMonitorTileConfiguration()
	}
	
	IgnoreFirstMoveChanged(){
		GuiControlGet, setting, , % this.hIgnoreFirstMove
		this.IgnoreFirstMove := setting
		IniWrite, % this.IgnoreFirstMove, % this.IniFile, Settings, IgnoreFirstMove
	}

	; -------------------------------------------- Monitor Class ---------------------------------
    class CMonitor {
		ID := 0 ;	The Index (AHK Monitor ID) of the monitor
        TileCoords := {x: [], y: []}
        TileSizes := {x: 0, y: 0}
        TileCount := {x: 2, y: 2}
        
        __New(id){
            this.id := id
            this.Coords := this.GetWorkArea()
        }
        
        SetRows(rows){
			this.TileCoords.y := []
            this.TileCount.y := rows
            this.TileSizes.y := round(this.Coords.h / rows)
            o := this.coords.t
            Loop % rows {
                this.TileCoords.y.push(o)
                o += this.TileSizes.y
            }
        }
        
        SetCols(cols){
			this.TileCoords.x := []
            this.TileCount.x := cols
            this.TileSizes.x := round(this.Coords.w / cols)
            o := this.coords.l
            Loop % cols {
                this.TileCoords.x.push(o)
                o += this.TileSizes.x
            }
        }
        
        ; Gets the "Work Area" of a monitor (The coordinates of the desktop on that monitor minus the taskbar)
        ; also pre-calculates a few values derived from the coordinates
        GetWorkArea(){
            SysGet, coords_, MonitorWorkArea, % this.id
            out := {}
            out.l := coords_left
            out.r := coords_right
            out.t := coords_top
            out.b := coords_bottom
            out.w := coords_right - coords_left
            out.h := coords_bottom - coords_top
            out.cx := coords_left + round(out.w / 2)	; center x
            out.cy := coords_top + round(out.h / 2)		; center y
            out.hw := round(out.w / 2)	; half width
            out.hh := round(out.w / 2)	 ; half height
            return out
        }
    }
    
	; ----------------------------------- Window Class ----------------------------------
    class CWindow {
        CurrentMonitor := 0	; Will point to monitor OBJECT when this window is tiled
        Pos := {x: 1, y: 1}
        Span := {x: 1, y: 1}
        
		AxisToOriginEdge := {x: "l", y: "t"}
		Axes := {x: 1, y: 2}

        __New(hwnd){
            this.hwnd := hwnd
        }
        
		GetCoords(){
			WinGetPos, wx, wy, ww, wh, % "ahk_id " this.hwnd
			return {x: wx, y: wy, w: ww, h: wh}
		}
		
		GetLocalCoords(){
			coords := this.GetCoords()
			wa := this.CurrentMonitor.GetWorkArea()
			for axis, unused in this.Axes {
				l_t := this.AxisToOriginEdge[axis]
				coords[axis] := abs(wa[l_t] - coords[axis])
			}
			;~ coords.x := mon.coords.x - coords.x, coords.x := mon.coords.y - coords.y, coords.x := mon.coords.w - coords.w, coords.x := mon.coords.h - coords.h
			return coords
		}
		
        ; Gets the coordinates of the center of the window
        GetCenter(){
			w := this.GetCoords()
            cx := w.x + round(w.w / 2)
            cy := w.y + round(w.h / 2)
            return {x: cx, y: cy}
        }
    }
}

; Code from Bug.n
; https://github.com/fuhsjr00/bug.n/blob/master/src/Window.ahk#L247

;; 0 - Not hung
;; 1 - Hung
Window_isHung(wndId) {
	static WM_NULL := 0
	detectHidden := A_DetectHiddenWindows
	DetectHiddenWindows, On
	SendMessage, WM_NULL, , , , % "ahk_id " wndId
	result := ErrorLevel
	DetectHiddenWindows, % detectHidden
	
	return result == 1
}

Window_getPosEx(hWindow, ByRef X = "", ByRef Y = "", ByRef Width = "", ByRef Height = "", ByRef Offset_X = "", ByRef Offset_Y = "") {
	Static Dummy5693, RECTPlus, S_OK := 0x0, DWMWA_EXTENDED_FRAME_BOUNDS := 9

	;-- Workaround for AutoHotkey Basic
	PtrType := (A_PtrSize=8) ? "Ptr" : "UInt"

	;-- Get the window's dimensions
	;   Note: Only the first 16 bytes of the RECTPlus structure are used by the
	;   DwmGetWindowAttribute and GetWindowRect functions.
	VarSetCapacity(RECTPlus, 24,0)
	DWMRC := DllCall("dwmapi\DwmGetWindowAttribute"
		,PtrType,hWindow                                ;-- hwnd
		,"UInt",DWMWA_EXTENDED_FRAME_BOUNDS             ;-- dwAttribute
		,PtrType,&RECTPlus                              ;-- pvAttribute
		,"UInt",16)                                     ;-- cbAttribute

	If (DWMRC != S_OK) {
		If ErrorLevel in -3, -4   ;-- Dll or function not found (older than Vista)
		{
			;-- Do nothing else (for now)
		} Else {
			outputdebug,
				(LTrim Join`s
				Function: %A_ThisFunc% -
				Unknown error calling "dwmapi\DwmGetWindowAttribute".
				RC = %DWMRC%,
				ErrorLevel = %ErrorLevel%,
				A_LastError = %A_LastError%.
				"GetWindowRect" used instead.
				)

			;-- Collect the position and size from "GetWindowRect"
			DllCall("GetWindowRect", PtrType, hWindow, PtrType, &RECTPlus)
		}
	}

	;-- Populate the output variables
	X := Left :=NumGet(RECTPlus, 0, "Int")
	Y := Top  :=NumGet(RECTPlus, 4, "Int")
	Right     :=NumGet(RECTPlus, 8, "Int")
	Bottom    :=NumGet(RECTPlus, 12, "Int")
	Width     :=Right-Left
	Height    :=Bottom-Top
	OffSet_X  := 0
	OffSet_Y  := 0

	;-- If DWM is not used (older than Vista or DWM not enabled), we're done
	If (DWMRC <> S_OK)
		Return &RECTPlus

	;-- Collect dimensions via GetWindowRect
	VarSetCapacity(RECT, 16, 0)
	DllCall("GetWindowRect", PtrType, hWindow, PtrType, &RECT)
	GWR_Width := NumGet(RECT, 8, "Int") - NumGet(RECT, 0, "Int")    ;-- Right minus Left
	GWR_Height := NumGet(RECT, 12, "Int") - NumGet(RECT, 4, "Int")  ;-- Bottom minus Top

	;-- Calculate offsets and update output variables
	NumPut(Offset_X := (Width  - GWR_Width)  // 2, RECTPlus, 16, "Int")
	NumPut(Offset_Y := (Height - GWR_Height) // 2, RECTPlus, 20, "Int")
	Return &RECTPlus
}

Window_move(wndId, x, y, width, height) {
	static WM_ENTERSIZEMOVE = 0x0231, WM_EXITSIZEMOVE  = 0x0232

	;~ If Not wndId Window_getPosEx(wndId, wndX, wndY, wndW, wndH) And (Abs(wndX - x) < 2 And Abs(wndY - y) < 2 And Abs(wndW - width) < 2 And Abs(wndH - height) < 2)
		;~ Return, 0
	addr := Window_getPosEx(wndId, wndX, wndY, wndW, wndH)
	if (!(wndId) && !(addr) &&  (Abs(wndX - x) < 2) &&  (Abs(wndY - y) < 2) &&  (Abs(wndW - width) < 2) &&  (Abs(wndH - height) < 2))
		return 0

	If Window_isHung(wndId) {
		OutputDebug % "DEBUG[2] Window_move: Potentially hung window " . wndId
		Return 1
	}
	/* Else {
		WinGet, wndMinMax, MinMax, % "ahk_id " wndId
		If (wndMinMax = -1 And Not Window_#%wndId%_isMinimized)
			WinRestore, ahk_id %wndId%
	}
	*/

	SendMessage, WM_ENTERSIZEMOVE, , , , % "ahk_id " wndId
	If ErrorLevel {
		;~ Debug_logMessage("DEBUG[2] Window_move: Potentially hung window " . wndId, 1)
		Return 1
	} Else {
		WinMove, % "ahk_id " wndId, , % x, % y, % width, % height
	
		;If Not (wndMinMax = 1) Or Not Window_#%wndId%_isDecorated Or Manager_windowNotMaximized(width, height) {
			If (Window_getPosEx(wndId, wndX, wndY, wndW, wndH) && (Abs(wndX - x) > 1 || Abs(wndY - y) > 1 || Abs(wndW - width) > 1 || Abs(wndH - height) > 1)) {
				x -= wndX - x
				y -= wndY - y
				width  += width - wndW - 1
				height += height - wndH - 1
				WinMove, % "ahk_id " wndId, , % x, % y, % width, % height
			}
		;}
	
		SendMessage, WM_EXITSIZEMOVE, , , , % "ahk_id " wndId
		Return, 0
	}
}


; Minimze to tray by SKAN http://www.autohotkey.com/board/topic/32487-simple-minimize-to-tray/

ConfigMinimizeToTray:
	Gui, +hwndGui1
	Menu("Tray","Nostandard"), Menu("Tray","Add","Restore","GuiShow"), Menu("Tray","Add")
	Menu("Tray","Default","Restore"), Menu("Tray","Click",1), Menu("Tray","Standard")
	OnMessage(0x112, "WM_SYSCOMMAND")

WM_SYSCOMMAND(wParam){
	If ( wParam = 61472 ) {
		SetTimer, OnMinimizeButton, -1
		Return 0
	}
}

Menu( MenuName, Cmd, P3="", P4="", P5="" ) {
	Menu, %MenuName%, %Cmd%, %P3%, %P4%, %P5%
	Return errorLevel
}

OnMinimizeButton:
	MinimizeGuiToTray( R, Gui1 )
	Menu("Tray","Icon")
	Return

GuiShow:
	DllCall("DrawAnimatedRects", UInt,Gui1, Int,3, UInt,&R+16, UInt,&R )
	Menu("Tray","NoIcon")
	Gui, Show
	Return

MinimizeGuiToTray( ByRef R, hGui ) {
	WinGetPos, X0,Y0,W0,H0, % "ahk_id " (Tray:=WinExist("ahk_class Shell_TrayWnd"))
	ControlGetPos, X1,Y1,W1,H1, TrayNotifyWnd1,ahk_id %Tray%
	SW:=A_ScreenWidth,SH:=A_ScreenHeight,X:=SW-W1,Y:=SH-H1,P:=((Y0>(SH/3))?("B"):(X0>(SW/3))
	? ("R"):((X0<(SW/3))&&(H0<(SH/3)))?("T"):("L")),((P="L")?(X:=X1+W0):(P="T")?(Y:=Y1+H0):)
	VarSetCapacity(R,32,0), DllCall( "GetWindowRect",UInt,hGui,UInt,&R)
	NumPut(X,R,16), NumPut(Y,R,20), DllCall("RtlMoveMemory",UInt,&R+24,UInt,&R+16,UInt,8 )
	DllCall("DrawAnimatedRects", UInt,hGui, Int,3, UInt,&R, UInt,&R+16 )
	WinHide, ahk_id %hGui%
}

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 109 guests