[Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids (now for v2!)

Post your working scripts, libraries and tools for AHK v1.1 and older
gregster
Posts: 8886
Joined: 30 Sep 2013, 06:48

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by gregster » 24 Aug 2020, 10:43

This might be related:
Instead, Microsoft Edge’s Internet Explorer Legacy mode means that users can stay on one browser – to “seamlessly experience the best of the modern web in one tab while accessing a business-critical legacy IE 11 app in another tab”, the company says.

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by kczx3 » 24 Aug 2020, 11:21

Technically, yes. However, no version prior to IE11 is supported. Nor is it recommended by Microsoft to my knowledge. I wouldn't personally put any work in to supporting anything other than IE11. And even then, soon the WebView2 control will be GA and you can ship the Edge (Chromium) RunTime installer with your application which installs the runtime in the background for your app to use. And Edge is supposed to be usable all the way back to Windows 7.

KiddoV
Posts: 13
Joined: 11 May 2020, 20:29

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by KiddoV » 31 Aug 2020, 07:25

Hi, all.
No body mentions about how to create an additional gui using neutron. Let say I want to create a custom messagebox that popup as a neutron webapp. Is anyone know how to do that. The code seem to crash when I try to create a new Neutron object.
Thanks!

User avatar
Chunjee
Posts: 1397
Joined: 18 Apr 2014, 19:05
Contact:

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by Chunjee » 31 Aug 2020, 22:26

KiddoV wrote:
31 Aug 2020, 07:25
No body mentions about how to create an additional gui using neutron.

This seems to work:

Code: Select all

neutron := new NeutronWindow()
; neutron.load("html\index.html")
neutron.Show("w2080 h1000")
neutron.Maximize()

neutron2 := new NeutronWindow()
; neutron2.load("html\index.html")
neutron2.Show("w2080 h1000")
neutron2.Maximize()
I commented out the load line only because it's unlikely you have a \html\index.html file exactly like me. You just need to make a new instance of the class and interact with each window through its associated object.

KiddoV
Posts: 13
Joined: 11 May 2020, 20:29

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by KiddoV » 01 Sep 2020, 06:19

Thanks @Chunjee for the reply!
I put it in a function:

Code: Select all

HtmlMsgBox(Options := "", Title := "", Text := "", Timeout := 0) {
    NeutronMsgBox := new NeutronWindow()
    NeutronMsgBox.Load("html_msgbox.html")
    NeutronMsgBox.Gui("-Resize +LabelHtmlMsgBox")
    
    NeutronMsgBox.Show("")
    
    Return
    
    NeutronMsgBoxClose:
        NeutronMsgBox.Destroy()     ;Free memory
    Return
}

Code: Select all

TestBttn(neutron, event) {
    HtmlMsgBox("", "Test Msgbox")
}
...and call it in HTML

Code: Select all

<button id="test-btn" type="button" class="btn btn-primary btn-xs" onclick="ahk.TestBttn(event)">Test MsgBox</button>
it seem to crashes my program right away after clicking the button?
Any idea why?
Thanks!

KiddoV
Posts: 13
Joined: 11 May 2020, 20:29

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by KiddoV » 14 Oct 2020, 06:59

Hi all,
Anyone know how to stop the mouse cursor to change to "resize-icon" when hover the edges of the window? I made an app that do not resize window, so it doesn't make sense when hover the edges of the window and cursor changed to resize-icon.
Thanks!

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by kczx3 » 14 Oct 2020, 08:43

@KiddoV
It is probably possible but why? There's nothing wrong with letting it resize since the browser should handle resizing any UI elements for you.

geek
Posts: 1051
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by geek » 14 Oct 2020, 21:27

kczx3 wrote:
24 Aug 2020, 11:21
Technically, yes. However, no version prior to IE11 is supported. Nor is it recommended by Microsoft to my knowledge. I wouldn't personally put any work in to supporting anything other than IE11. And even then, soon the WebView2 control will be GA and you can ship the Edge (Chromium) RunTime installer with your application which installs the runtime in the background for your app to use. And Edge is supposed to be usable all the way back to Windows 7.
I've picked up on your work with WebView and managed to backport it to AHKv1, as well as solved a few of the problems that you were facing with, for example, AddHostObjectToScript and reading object properties (AHK's IDispatch implementation and WebView2's IDispatch implementation are both quirky, and in a way that conflicts. I had to rebuild the IDispatch support from the ground up in AHK). I don't have the code posted anywhere outside of Discord, but it might make its way online soon so keep an eye out. It looks like the WebView2 control is more usable in v2 and v1 thanks to anonymous functions, but the unique way in which Microsoft implement things like thread locking and callbacks mean that it will likely never be as easy to use as IE11/ActiveX/Neutron.

Also, to anyone who is concerned about IE disappearing from the OS--don't be too concerned. The component that Edge will use to display IE content in Edge is, as far as I am aware, the same component that is used by Neutron. As long as Edge retains IE backward compatibility Neutron should still work fine.
KiddoV wrote: it seem to crashes my program right away after clicking the button?
Any idea why?
Thanks!
One of the first things that was brought to my attention after releasing Neutron was the fact that multiple-window support was broken. It turned out to be an easy fix ( https://github.com/G33kDude/Neutron.ahk/commit/63a0785d140202eb013226afd84da65bc19a56ca ) but it hasn't yet made its way to a release. Time constraints and other commitments have kept me from doing it justice. Either implement that small patch yourself, or grab the latest development copy instead of the release build: https://github.com/G33kDude/Neutron.ahk/blob/master/Neutron.ahk
KiddoV wrote: Hi all,
Anyone know how to stop the mouse cursor to change to "resize-icon" when hover the edges of the window? I made an app that do not resize window, so it doesn't make sense when hover the edges of the window and cursor changed to resize-icon.
Thanks!
This was a feature that I neglected for release 1.0.0. Right now, my best recommendation would be to set a minimum size and, if you really need to, a maximum size that the window should be. It's 2020, windows should be resizable. That said, if you really do need to get rid of resizing altogether you can comment out the contents of the if statement where it says if (Msg == this.WM_NCHITTEST) in the _WindowProc(Msg, wParam, lParam) method.

KiddoV
Posts: 13
Joined: 11 May 2020, 20:29

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by KiddoV » 15 Oct 2020, 08:47

Thank you @GeekDude. Really love your work!
Do you think if it is possible to make a Neutron library (maybe in the future) that won't need IE in the window system?. I mean to make a stand-alone Neutron lib that does not depend on IE browser.

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by kczx3 » 15 Oct 2020, 12:22

@GeekDude
Glad that my efforts (that were only possible due to the efforts by @Flipeador) helped you to pick up and continue with it. I find most tasks are easier with AHK v2 due to GUI objects and fat-arrow functions (scope inheritance!). That said, I agree that WebView2 will never be as simple as the Web Browser control because WebView2 doesn't expose the DOM at all. Everything must be handled by messaging to my knowledge. I think its worth it though considering how much better of a browser and JavaScript environment that WebView2 gives.

I didn't explicitly catch this but are you backporting it to v1 to then use with Neutron?

geek
Posts: 1051
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by geek » 16 Oct 2020, 18:13

KiddoV wrote:
15 Oct 2020, 08:47
Thank you @GeekDude. Really love your work!
Do you think if it is possible to make a Neutron library (maybe in the future) that won't need IE in the window system?. I mean to make a stand-alone Neutron lib that does not depend on IE browser.
Not Neutron, but later libraries that take advantage of Edge's upcoming desktop app integration API won't need IE (they'll need Edge :P )
kczx3 wrote: @GeekDude
Glad that my efforts (that were only possible due to the efforts by @Flipeador) helped you to pick up and continue with it. I find most tasks are easier with AHK v2 due to GUI objects and fat-arrow functions (scope inheritance!). That said, I agree that WebView2 will never be as simple as the Web Browser control because WebView2 doesn't expose the DOM at all. Everything must be handled by messaging to my knowledge. I think its worth it though considering how much better of a browser and JavaScript environment that WebView2 gives.

I didn't explicitly catch this but are you backporting it to v1 to then use with Neutron?
I haven't used v2 at all, and I think more people find my libraries more useful as v1 compatible than as v2 compatible. I started with WebView2 in v1 before I saw any of your/@Flipeador/others' work on it, but wasn't able to get it to work until I started comparing my implementation against the v2 code all of you had provided. The v2 implementation is much simpler than v1, so long-term I think it will be more attractive to move over to v2.

WebView2 won't ever make it to Neutron simply because its core behavior is fundamentally different, but I've been thinking about how I could best go about creating a similar library for WebView2. It will probably be called Positron (if it ever sees daylight).

Also, it does kind of expose the DOM through the CallDevToolsProtocolMethod (DOM endpoints are available for that) but it's much more complicated than just invoking JavaScript and, as all calls use serialized IPC, not significantly different than invoking JS. Maybe I can wrap that in a way that makes sense, but I'm not sure the effort is warranted.

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by kczx3 » 16 Oct 2020, 19:38

@GeekDude be careful! Once you start using v2 you might not go back!

KiddoV
Posts: 13
Joined: 11 May 2020, 20:29

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by KiddoV » 20 Oct 2020, 09:40

@GeekDude ,
@ I am looking forward to see the future of Neutron on Edge. In my opinion tho, I would rather implement it into Chrome (If that possible) . What do you think?
@ Maybe it just me who cares so much about the UI. But, do you know how to change the scroll bar outlook? It looks so ugly and cannot customize on IE11. Is there a way to customize it either on HTML/CSS side or AHK side?

jly
Posts: 89
Joined: 30 Sep 2020, 06:06

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by jly » 10 Nov 2020, 01:25

@GeekDude
Hi GeekDude, I like Neutron very much.
I am trying to use Neutron to create a simple project.
I have encountered some problems, I would like to ask you for advice.


1
I created two files: test.ahk, test.html
There is only one input box in test.html, and there is a piece of js code to capture the keyup event.

Capture keyup events, pop-up prompt:
test1.png
test1.png (6.57 KiB) Viewed 6742 times

If I open test.html with a browser, the keyup event can be captured in Chrome, IE, and Edge browsers.

But when test.html is loaded with Neutron in test.ahk, the keyup event cannot be captured.
If I replace keyup with focus, mouseup, mousedown events, they can be captured normally.

Further testing found that when the input method is switched to non-English, the a-z keys can be captured,
but the number keys cannot be captured.

If I change keyup to keydown, the number keys can be captured, but the a-z keys cannot be captured.
When the input method is switched to non-English, the a-z keys can be captured.

Add onkeydown="ahk.keyup(event)" directly to <input/>, the result is the same.

test.ahk:

Code: Select all

#SingleInstance, Force
#NoEnv
SetBatchLines, -1
#Include Neutron.ahk

neutron := new NeutronWindow()
neutron.Load("test.html")
neutron.Gui("+LabelNeutron")
neutron.Show("w500 h300")
return

NeutronClose:
ExitApp
return

keyup(neutron, event)
{
	MsgBox,,,111,1
}
test.html:

Code: Select all


<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<script type="text/javascript" charset="utf-8" src="https cdn.bootcss.com /jquery/2.1.0/jquery.min.js"></script>  Broken Link for safety
		<style type="text/css">
			input.input_text{
			width: 100%;
			}
		</style>
	</head>

	<body class="d-flex flex-column">
		<div class="box">
			<input type="text" class="input_text" onmousedown="neutron.DragInputBox()"  id="searchInput" placeholder="">
		</div>
	</body>
</html>

<script>
	$(document).ready(function(){
		$("#searchInput").on('keyup', function() {
			alert("hello world");
		});
	});
</script>


2
In addition, I want to drag the entire window when dragging the input box with the mouse,
so I added the DragInputBox function under the Neutron.ahk: DragTitleBar function.
and set "onmousedown" to neutron.DragInputBox()

After releasing the input box, I can immediately press the keyboard to enter characters.
If I do not enter the characters immediately, but enter it after one second or two,
the edit box does not respond. I have to input only after moving the mouse slightly.

If I use PostMessage to move the window,
the problem is more serious: After dragging and releasing, I have to move the mouse to input characters.


DragInputBox:

Code: Select all


	DragInputBox()
	{
		CoordMode, Mouse, Window
		MouseGetPos, mX_win, mY_win
		if(mX_win>A_CaretX+10)
		{
			;PostMessage, this.WM_NCLBUTTONDOWN, 2, 0,, % "ahk_id" this.hWnd

			SetWinDelay,10
			CoordMode,Mouse
			MouseGetPos,KDE_X1,KDE_Y1,KDE_id
			WinGet,KDE_Win,MinMax,ahk_id %KDE_id%
			If KDE_Win
				return
			; Get the initial window position.
			WinGetPos,KDE_WinX1,KDE_WinY1,,,ahk_id %KDE_id%
			Loop
			{
				GetKeyState,KDE_Button,LButton,P ; If the button has been released, exit.
				If KDE_Button = U
					break
				MouseGetPos,KDE_X2,KDE_Y2 ; Get the current mouse position.
				KDE_X2 -= KDE_X1 ; Get the offset from the original mouse position.
				KDE_Y2 -= KDE_Y1
				KDE_WinX2 := (KDE_WinX1 + KDE_X2) ; Apply this offset to the window position.
				KDE_WinY2 := (KDE_WinY1 + KDE_Y2)
				WinMove,ahk_id %KDE_id%,,%KDE_WinX2%,%KDE_WinY2% ; Move the window to a new position.
			}
		}
	}
	

Neutron.ahk:

Code: Select all

;
; Neutron.ahk v1.0.0
; Copyright (c) 2020 Philip Taylor (known also as GeekDude, G33kDude)
; https://github.com/G33kDude/Neutron.ahk
;
; MIT License
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in all
; copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
; SOFTWARE.
;

class NeutronWindow
{
	static TEMPLATE := "
( ; html
<!DOCTYPE html><html>
<head>

<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<style>
	html, body {
		width: 100%; height: 100%;
		margin: 0; padding: 0;
		font-family: sans-serif;
	}

	body {
		display: flex;
		flex-direction: column;
	}

	header {
		width: 100%;
		display: flex;
		background: silver;
		font-family: Segoe UI;
		font-size: 9pt;
	}

	.title-bar {
		padding: 0.35em 0.5em;
		flex-grow: 1;
	}

	.title-btn {
		padding: 0.35em 1.0em;
		cursor: pointer;
		vertical-align: bottom;
		font-family: Webdings;
		font-size: 11pt;
	}

	.title-btn:hover {
		background: rgba(0, 0, 0, .2);
	}

	.title-btn-close:hover {
		background: #dc3545;
	}

	.main {
		flex-grow: 1;
		padding: 0.5em;
		overflow: auto;
	}
</style>
<style>{}</style>

</head>
<body>

<header>
	<span class='title-bar' onmousedown='neutron.DragTitleBar()'>{}</span>
	<span class='title-btn' onclick='neutron.Minimize()'>0</span>
	<span class='title-btn' onclick='neutron.Maximize()'>1</span>
	<span class='title-btn title-btn-close' onclick='neutron.Close()'>r</span>
</header>

<div class='main'>{}</div>

<script>{}</script>

</body>
</html>
)"


	
	; --- Constants ---
	
	static VERSION := "1.0.0"
	
	; Windows Messages
	, WM_DESTROY := 0x02
	, WM_SIZE := 0x05
	, WM_NCCALCSIZE := 0x83
	, WM_NCHITTEST := 0x84
	, WM_NCLBUTTONDOWN := 0xA1
	, WM_KEYDOWN := 0x100
	, WM_MOUSEMOVE := 0x200
	, WM_LBUTTONDOWN := 0x201
	
	; Non-client hit test values (WM_NCHITTEST)
	, HT_VALUES := [[13, 12, 14], [10, 1, 11], [16, 15, 17]]
	
	; Registry keys
	, KEY_FBE := "HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MAIN"
	. "\FeatureControl\FEATURE_BROWSER_EMULATION"
	
	; Undoucmented Accent API constants
	; https withinrafael.com /2018/02/02/adding-acrylic-blur-to-your-windows-10-apps-redstone-4-desktop-apps/  Broken Link for safety
	, ACCENT_ENABLE_BLURBEHIND := 3
	, WCA_ACCENT_POLICY := 19
	
	; Other constants
	, EXE_NAME := A_IsCompiled ? A_ScriptName : StrSplit(A_AhkPath, "\").Pop()
	
	
	; --- Instance Variables ---
	
	; Maximum pixel inset for sizing handles to appear
	border_size := 6
	
	; The window size
	w := 800
	h := 600
	
	
	; --- Properties ---
	
	; Get the JS DOM object
	doc[]
	{
		get
		{
			return this.wb.Document
		}
	}
	
	; Get the JS Window object
	wnd[]
	{
		get
		{
			return this.wb.Document.parentWindow
		}
	}
	
	
	; --- Construction, Destruction, Meta-Functions ---
	
	__New(html:="", css:="", js:="", title:="Neutron")
	{
		static wb
		this.LISTENERS := [this.WM_DESTROY, this.WM_SIZE, this.WM_NCCALCSIZE
		, this.WM_KEYDOWN, this.WM_LBUTTONDOWN]
		
		; Create necessary circular references
		this.bound := {}
		this.bound._OnMessage := this._OnMessage.Bind(this)
		
		; Bind message handlers
		for i, message in this.LISTENERS
			OnMessage(message, this.bound._OnMessage)
		
		; Create and save the GUI
		; TODO: Restore previous default GUI
		Gui, New, +hWndhWnd +Resize -DPIScale
		this.hWnd := hWnd
		
		; Enable shadow
		VarSetCapacity(margins, 16, 0)
		NumPut(1, &margins, 0, "Int")
		DllCall("Dwmapi\DwmExtendFrameIntoClientArea"
		, "UPtr", hWnd      ; HWND hWnd
		, "UPtr", &margins) ; MARGINS *pMarInset
		
		; When manually resizing a window, the contents of the window often "lag
		; behind" the new window boundaries. Until they catch up, Windows will
		; render the border and default window color to fill that area. On most
		; windows this will cause no issue, but for borderless windows this can
		; cause rendering artifacts such as thin borders or unwanted colors to
		; appear in that area until the rest of the window catches up.
		;
		; When creating a dark-themed application, these artifacts can cause
		; jarringly visible bright areas. This can be mitigated some by changing
		; the window settings to cause dark/black artifacts, but it's not a
		; generalizable approach, so if I were to do that here it could cause
		; issues with light-themed apps.
		;
		; Some borderless window libraries, such as rossy's C implementation
		; (https://github.com/rossy/borderless-window) hide these artifacts by
		; playing with the window transparency settings which make them go away
		; but also makes it impossible to show certain colors (in rossy's case,
		; Fuchsia/FF00FF).
		;
		; Luckly, there's an undocumented Windows API function in user32.dll
		; called SetWindowCompositionAttribute, which allows you to change the
		; window accenting policies. This tells the DWM compositor how to fill
		; in areas that aren't covered by controls. By enabling the "blurbehind"
		; accent policy, Windows will render a blurred version of the screen
		; contents behind your window in that area, which will not be visually
		; jarring regardless of the colors of your application or those behind
		; it.
		;
		; Because this API is undocumented (and unavailable in Windows versions
		; below 10) it's not a one-size-fits-all solution, and could break with
		; future system updates. Hopefully a better soultion for the problem
		; this hack addresses can be found for future releases of this library.
		;
		; https withinrafael.com /2018/02/02/adding-acrylic-blur-to-your-windows-10-apps-redstone-4-desktop-apps/  Broken Link for safety
		; https://github.com/melak47/BorderlessWindow/issues/13#issuecomment-309154142
		; http undoc.airesoft.co.uk /user32.dll/SetWindowCompositionAttribute.php  Broken Link for safety
		; https gist.github.com /riverar/fd6525579d6bbafc6e48  Broken Link for safety
		; https vhanla.codigobit.info /2015/07/enable-windows-10-aero-glass-aka-blur.html  Broken Link for safety
		
		Gui, Color, 0, 0
		VarSetCapacity(wcad, A_PtrSize+A_PtrSize+4, 0)
		NumPut(this.WCA_ACCENT_POLICY, &wcad, 0, "Int")
		VarSetCapacity(accent, 16, 0)
		NumPut(this.ACCENT_ENABLE_BLURBEHIND, &accent, 0, "Int")
		NumPut(&accent, &wcad, A_PtrSize, "Ptr")
		NumPut(16, &wcad, A_PtrSize+A_PtrSize, "Int")
		DllCall("SetWindowCompositionAttribute", "UPtr", hWnd, "UPtr", &wcad)
		
		; Creating an ActiveX control with a valid URL instantiates a
		; WebBrowser, saving its object to the associated variable. The "about"
		; URL scheme allows us to start the control on either a blank page, or a
		; page with some HTML content pre-loaded by passing HTML after the
		; colon: "about:<!DOCTYPE html><body>...</body>"
		
		; Read more about the WebBrowser control here:
		; http msdn.microsoft.com /en-us/library/aa752085  Broken Link for safety
		
		; For backwards compatibility reasons, the WebBrowser control defaults
		; to IE7 emulation mode. The standard method of mitigating this is to
		; include a compatibility meta tag in the HTML, but this requires
		; tampering to the HTML and does not solve all compatibility issues.
		; By tweaking the registry before and after creation of the control we
		; can opt-out of the browser emulation feature altogether with minimal
		; impact on the rest of the system.
		
		; Read more about browser compatibility modes here:
		; https://docs.microsoft.com/en-us/archive/blogs/patricka/controlling-webbrowser-control-compatibility
		
		RegRead, fbe, % this.KEY_FBE, % this.EXE_NAME
		RegWrite, REG_DWORD, % this.KEY_FBE, % this.EXE_NAME, 0
		Gui, Add, ActiveX, vwb hWndhWB x0 y0 w800 h600, about:blank
		if (fbe = "")
			RegDelete, % this.KEY_FBE, % this.EXE_NAME
		else
			RegWrite, REG_DWORD, % this.KEY_FBE, % this.EXE_NAME, % fbe
		
		; Save the WebBrowser control to reference later
		this.wb := wb
		this.hWB := hWB
		
		; Connect the web browser's event stream to a new event handler object
		ComObjConnect(this.wb, new this.WBEvents(this))
		
		; Compute the HTML template if necessary
		if !(html ~= "i)^<!DOCTYPE")
			html := Format(this.TEMPLATE, css, title, html, js)
		
		; Write the given content to the page
		this.doc.write(html)
		this.doc.close()
		
		; Inject the AHK objects into the JS scope
		this.wnd.neutron := this
		this.wnd.ahk := new this.Dispatch(this)
		
		; Wait for the page to finish loading
		while wb.readyState < 4
			Sleep, 50
		
		; Subclass the rendered Internet Explorer_Server control to intercept
		; its events, including WM_NCHITTEST and WM_NCLBUTTONDOWN.
		; Read more here: https forum.juce.com /t/_/27937  Broken Link for safety
		; And in the AutoHotkey documentation for RegisterCallback (Example 2)
		
		dhw := A_DetectHiddenWindows
		DetectHiddenWindows, On
		ControlGet, hWnd, hWnd,, Internet Explorer_Server1, % "ahk_id" this.hWnd
		this.hIES := hWnd
		DetectHiddenWindows, %dhw%
		
		this.pWndProc := RegisterCallback(this._WindowProc, "", 4, &this)
		this.pWndProcOld := DllCall("SetWindowLong" (A_PtrSize == 8 ? "Ptr" : "")
		, "Ptr", hWnd          ; HWND     hWnd
		, "Int", -4            ; int      nIndex (GWLP_WNDPROC)
		, "Ptr", this.pWndProc ; LONG_PTR dwNewLong
		, "Ptr") ; LONG_PTR
		
		; Stop the WebBrowser control from consuming file drag and drop events
		this.wb.RegisterAsDropTarget := False
		DllCall("ole32\RevokeDragDrop", "UPtr", this.hIES)
	}
	
	; Show an alert for debugging purposes when the class gets garbage collected
	; __Delete()
	; {
	; 	MsgBox, __Delete
	; }
	
	
	; --- Event Handlers ---
	
	_OnMessage(wParam, lParam, Msg, hWnd)
	{
		if (hWnd == this.hWnd)
		{
			; Handle messages for the main window
			
			if (Msg == this.WM_NCCALCSIZE)
			{
				; Size the client area to fill the entire window.
				; See this project for more information:
				; https://github.com/rossy/borderless-window
				
				; Fill client area when not maximized
				if !DllCall("IsZoomed", "UPtr", hWnd)
					return 0
				; else crop borders to prevent screen overhang
				
				; Query for the window's border size
				VarSetCapacity(windowinfo, 60, 0)
				NumPut(60, windowinfo, 0, "UInt")
				DllCall("GetWindowInfo", "UPtr", hWnd, "UPtr", &windowinfo)
				cxWindowBorders := NumGet(windowinfo, 48, "Int")
				cyWindowBorders := NumGet(windowinfo, 52, "Int")
				
				; Inset the client rect by the border size
				NumPut(NumGet(lParam+0, "Int") + cxWindowBorders, lParam+0, "Int")
				NumPut(NumGet(lParam+4, "Int") + cyWindowBorders, lParam+4, "Int")
				NumPut(NumGet(lParam+8, "Int") - cxWindowBorders, lParam+8, "Int")
				NumPut(NumGet(lParam+12, "Int") - cyWindowBorders, lParam+12, "Int")
				
				return 0
			}
			else if (Msg == this.WM_SIZE)
			{
				; Extract size from LOWORD and HIWORD (preserving sign)
				this.w := w := lParam<<48>>48
				this.h := h := lParam<<32>>48
				
				DllCall("MoveWindow", "UPtr", this.hWB, "Int", 0, "Int", 0, "Int", w, "Int", h, "UInt", 0)
				
				return 0
			}
			else if (Msg == this.WM_DESTROY)
			{
				; Clean up all our circular references so that the object may be
				; garbage collected.
				
				for i, message in this.LISTENERS
					OnMessage(message, this.bound._OnMessage, 0)
				this.bound := []
			}
		}
		else if (hWnd == this.hIES)
		{
			; Handle messages for the rendered Internet Explorer_Server
			
			if (Msg == this.WM_KEYDOWN)
			{
				; Accelerator handling code from AutoHotkey Installer
				
				if (Chr(wParam) ~= "[A-Z]" || wParam = 0x74) ; Disable Ctrl+O/L/F/N and F5.
					return
				Gui +OwnDialogs ; For threadless callbacks which interrupt this.
				pipa := ComObjQuery(this.wb, "{00000117-0000-0000-C000-000000000046}")
				VarSetCapacity(kMsg, 48), NumPut(A_GuiY, NumPut(A_GuiX
				, NumPut(A_EventInfo, NumPut(lParam, NumPut(wParam
				, NumPut(Msg, NumPut(hWnd, kMsg)))), "uint"), "int"), "int")
				Loop 2
					r := DllCall(NumGet(NumGet(1*pipa)+5*A_PtrSize), "ptr", pipa, "ptr", &kMsg)
				; Loop to work around an odd tabbing issue (it's as if there
				; is a non-existent element at the end of the tab order).
				until wParam != 9 || this.wb.document.activeElement != ""
				ObjRelease(pipa)
				if r = 0 ; S_OK: the message was translated to an accelerator.
					return 0
				return
			}
		}
	}
	
	_WindowProc(Msg, wParam, lParam)
	{
		Critical
		hWnd := this
		this := Object(A_EventInfo)
		
		if (Msg == this.WM_NCHITTEST)
		{
			; Check to see if the cursor is near the window border, which
			; should be treated as the "non-client" drag-to-resize area.
			; https://autohotkey.com/board/topic/23969-/#entry155480
			
			; Extract coordinates from LOWORD and HIWORD (preserving sign)
			x := lParam<<48>>48, y := lParam<<32>>48
			
			; Get the window position for comparison
			WinGetPos, wX, wY, wW, wH, % "ahk_id" this.hWnd
			
			; Calculate positions in the lookup tables
			row := (x < wX + this.BORDER_SIZE) ? 1 : (x >= wX + wW - this.BORDER_SIZE) ? 3 : 2
			col := (y < wY + this.BORDER_SIZE) ? 1 : (y >= wY + wH - this.BORDER_SIZE) ? 3 : 2
			
			return this.HT_VALUES[col, row]
		}
		else if (Msg == this.WM_NCLBUTTONDOWN)
		{
			; Hoist nonclient clicks to main window
			return DllCall("SendMessage", "Ptr", this.hWnd, "UInt", Msg, "UPtr", wParam, "Ptr", lParam, "Ptr")
		}
		
		; Otherwise (since above didn't return), pass all unhandled events to the original WindowProc.
		return DllCall("CallWindowProc"
		, "Ptr", this.pWndProcOld ; WNDPROC lpPrevWndFunc
		, "Ptr", hWnd             ; HWND    hWnd
		, "UInt", Msg             ; UINT    Msg
		, "UPtr", wParam          ; WPARAM  wParam
		, "Ptr", lParam           ; LPARAM  lParam
		, "Ptr") ; LRESULT
	}
	
	
	; --- Instance Methods ---
	
	; Triggers window dragging. Call this on mouse click down. Best used as your
	; title bar's onmousedown attribute.
	DragTitleBar()
	{
		
		PostMessage, this.WM_NCLBUTTONDOWN, 2, 0,, % "ahk_id" this.hWnd
	}

	DragInputBox()
	{
		CoordMode, Mouse, Window
		MouseGetPos, mX_win, mY_win
		if(mX_win>A_CaretX+10)
		{
			;PostMessage, this.WM_NCLBUTTONDOWN, 2, 0,, % "ahk_id" this.hWnd

			SetWinDelay,10
			CoordMode,Mouse
			MouseGetPos,KDE_X1,KDE_Y1,KDE_id
			WinGet,KDE_Win,MinMax,ahk_id %KDE_id%
			If KDE_Win
				return
			; Get the initial window position.
			WinGetPos,KDE_WinX1,KDE_WinY1,,,ahk_id %KDE_id%
			Loop
			{
				GetKeyState,KDE_Button,LButton,P ; If the button has been released, exit.
				If KDE_Button = U
					break
				MouseGetPos,KDE_X2,KDE_Y2 ; Get the current mouse position.
				KDE_X2 -= KDE_X1 ; Get the offset from the original mouse position.
				KDE_Y2 -= KDE_Y1
				KDE_WinX2 := (KDE_WinX1 + KDE_X2) ; Apply this offset to the window position.
				KDE_WinY2 := (KDE_WinY1 + KDE_Y2)
				WinMove,ahk_id %KDE_id%,,%KDE_WinX2%,%KDE_WinY2% ; Move the window to a new position.
			}
		}
	}
	
	
	; Minimizes the Neutron window. Best used in your title bar's minimize
	; button's onclick attribute.
	Minimize()
	{
		Gui, % this.hWnd ":Minimize"
	}
	
	; Maximize the Neutron window. Best used in your title bar's maximize
	; button's onclick attribute.
	Maximize()
	{
		if DllCall("IsZoomed", "UPtr", this.hWnd)
			Gui, % this.hWnd ":Restore"
		else
			Gui, % this.hWnd ":Maximize"
	}
	
	; Closes the Neutron window. Best used in your title bar's close
	; button's onclick attribute.
	Close()
	{
		WinClose, % "ahk_id" this.hWnd
	}
	
	; Hides the Nuetron window.
	Hide()
	{
		Gui, % this.hWnd ":Hide"
	}
	
	; Destroys the Neutron window. Do this when you would no longer want to
	; re-show the window, as it will free the memory taken up by the GUI and
	; ActiveX control. This method is best used either as your title bar's close
	; button's onclick attribute, or in a custom window close routine.
	Destroy()
	{
		Gui, % this.hWnd ":Destroy"
	}
	
	; Shows a hidden Neutron window.
	Show(options:="")
	{
		w := RegExMatch(options, "w\s*\K\d+", match) ? match : this.w
		h := RegExMatch(options, "h\s*\K\d+", match) ? match : this.h
		
		; AutoHotkey sizes the window incorrectly, trying to account for borders
		; that aren't actually there. Call the function AHK uses to offset and
		; apply the change in reverse to get the actual wanted size.
		VarSetCapacity(rect, 16, 0)
		DllCall("AdjustWindowRectEx"
		, "Ptr", &rect ;  LPRECT lpRect
		, "UInt", 0x80CE0000 ;  DWORD  dwStyle
		, "UInt", 0 ;  BOOL   bMenu
		, "UInt", 0 ;  DWORD  dwExStyle
		, "UInt") ; BOOL
		w += NumGet(&rect, 0, "Int")-NumGet(&rect, 8, "Int")
		h += NumGet(&rect, 4, "Int")-NumGet(&rect, 12, "Int")
		
		Gui, % this.hWnd ":Show", %options% w%w% h%h%
	}
	
	; Loads an HTML file by name (not path). When running the script uncompiled,
	; looks for the file in the local directory. When running the script
	; compiled, looks for the file in the EXE's RCDATA. Files included in your
	; compiled EXE by FileInstall are stored in RCDATA whether they get
	; extracted or not. An easy way to get your Neutron resources into a
	; compiled script, then, is to put FileInstall commands for them right below
	; the return at the bottom of your AutoExecute section.
	;
	; Parameters:
	;   fileName - The name of the HTML file to load into the Neutron window.
	;              Make sure to give just the file name, not the full path.
	;
	; Returns: nothing
	;
	; Example:
	;
	; ; AutoExecute Section
	; neutron := new NeutronWindow()
	; neutron.Load("index.html")
	; neutron.Show()
	; return
	; FileInstall, index.html, index.html
	; FileInstall, index.css, index.css
	;
	Load(fileName)
	{
		; Complete the path based on compiled state
		if A_IsCompiled
			url := "res://" this.wnd.encodeURIComponent(A_ScriptFullPath) "/10/" fileName
		else
			url := A_WorkingDir "/" fileName
		
		; Navigate to the calculated file URL
		this.wb.Navigate(url)
		
		; Wait for the page to finish loading
		while this.wb.readyState < 3
			Sleep, 50
		
		; Inject the AHK objects into the JS scope
		this.wnd.neutron := this
		this.wnd.ahk := new this.Dispatch(this)
		
		; Wait for the page to finish loading
		while this.wb.readyState < 4
			Sleep, 50
	}
	
	; Shorthand method for document.querySelector
	qs(selector)
	{
		return this.doc.querySelector(selector)
	}
	
	; Shorthand method for document.querySelectorAll
	qsa(selector)
	{
		return this.doc.querySelectorAll(selector)
	}
	
	; Passthrough method for the Gui command, targeted at the Neutron Window
	; instance
	Gui(subCommand, value1:="", value2:="", value3:="")
	{
		Gui, % this.hWnd ":" subCommand, %value1%, %value2%, %value3%
	}
	
	
	; --- Static Methods ---
	
	; Given an HTML Collection (or other JavaScript array), return an enumerator
	; that will iterate over its items.
	;
	; Parameters:
	;     htmlCollection - The JavaScript array to be iterated over
	;
	; Returns: An Enumerable object
	;
	; Example:
	;
	; neutron := new NeutronWindow("<body><p>A</p><p>B</p><p>C</p></body>")
	; neutron.Show()
	; for i, element in neutron.Each(neutron.body.children)
	;     MsgBox, % i ": " element.innerText
	;
	Each(htmlCollection)
	{
		return new this.Enumerable(htmlCollection)
	}
	
	; Given an HTML Form Element, construct a FormData object
	;
	; Parameters:
	;   formElement - The HTML Form Element
	;   useIdAsName - When a field's name is blank, use it's ID instead
	;
	; Returns: A FormData object
	;
	; Example:
	;
	; neutron := new NeutronWindow("<form>"
	; . "<input type='text' name='field1' value='One'>"
	; . "<input type='text' name='field2' value='Two'>"
	; . "<input type='text' name='field3' value='Three'>"
	; . "</form>")
	; neutron.Show()
	; formElement := neutron.doc.querySelector("form") ; Grab 1st form on page
	; formData := neutron.GetFormData(formElement) ; Get form data
	; MsgBox, % formData.field2 ; Pull a single field
	; for name, element in formData ; Iterate all fields
	;     MsgBox, %name%: %element%
	;
	GetFormData(formElement, useIdAsName:=True)
	{
		formData := new this.FormData()
		
		for i, field in this.Each(formElement.elements)
		{
			; Discover the field's name
			name := ""
			try ; fieldset elements error when reading the name field
				name := field.name
			if (name == "" && useIdAsName)
				name := field.id
			
			; Filter against fields which should be omitted
			if (name == "" || field.disabled
				|| field.type ~= "^file|reset|submit|button$")
				continue
			
			; Handle select-multiple variants
			if (field.type == "select-multiple")
			{
				for j, option in this.Each(field.options)
					if (option.selected)
						formData.add(name, option.value)
				continue
			}
			
			; Filter against unchecked checkboxes and radios
			if (field.type ~= "^checkbox|radio$" && !field.checked)
				continue
			
			; Return the field values
			formData.add(name, field.value)
		}
		
		return formData
	}
	
	; Given a potentially HTML-unsafe string, return an HTML safe string
	; https://stackoverflow.com/a/6234804
	EscapeHTML(unsafe)
	{
		unsafe := StrReplace(unsafe, "&", "&amp;")
		unsafe := StrReplace(unsafe, "<", "&lt;")
		unsafe := StrReplace(unsafe, ">", "&gt;")
		unsafe := StrReplace(unsafe, """", "&quot;")
		unsafe := StrReplace(unsafe, "''", "&#039;")
		return unsafe
	}
	
	; Wrapper for Format that applies EscapeHTML to each value before passing
	; them on. Useful for dynamic HTML generation.
	FormatHTML(formatStr, values*)
	{
		for i, value in values
			values[i] := this.EscapeHTML(value)
		return Format(formatStr, values*)
	}
	
	
	; --- Nested Classes ---
	
	; Proxies method calls to AHK function calls, binding a given value to the
	; first parameter of the target function.
	;
	; For internal use only.
	;
	; Parameters:
	;   parent - The value to bind
	;
	class Dispatch
	{
		__New(parent)
		{
			this.parent := parent
		}
		
		__Call(params*)
		{
			; Make sure the given name is a function
			if !(fn := Func(params[1]))
				throw Exception("Unknown function: " params[1])
			
			; Make sure enough parameters were given
			if (params.length() < fn.MinParams)
				throw Exception("Too few parameters given to " fn.Name ": " params.length())
			
			; Make sure too many parameters weren't given
			if (params.length() > fn.MaxParams && !fn.IsVariadic)
				throw Exception("Too many parameters given to " fn.Name ": " params.length())
			
			; Change first parameter from the function name to the neutron instance
			params[1] := this.parent
			
			; Call the function
			return fn.Call(params*)
		}
	}
	
	; Handles Web Browser events
	; https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa768283%28v%3dvs.85%29
	;
	; For internal use only
	;
	; Parameters:
	;   parent - An instance of the Neutron class
	;
	class WBEvents
	{
		__New(parent)
		{
			this.parent := parent
		}
		
		DocumentComplete(wb)
		{
			; Inject the AHK objects into the JS scope
			wb.document.parentWindow.neutron := this.parent
			wb.document.parentWindow.ahk := new this.parent.Dispatch(this.parent)
		}
	}
	
	; Enumerator class that enumerates the items of an HTMLCollection (or other
	; JavaScript array).
	;
	; Best accessed through the .Each() helper method.
	;
	; Parameters:
	;   htmlCollection - The HTMLCollection to be enumerated.
	;
	class Enumerable
	{
		i := 0
		
		__New(htmlCollection)
		{
			this.collection := htmlCollection
		}
		
		_NewEnum()
		{
			return this
		}
		
		Next(ByRef i, ByRef elem)
		{
			if (this.i >= this.collection.length)
				return False
			i := this.i
			elem := this.collection.item(this.i++)
			return True
		}
	}
	
	; A collection similar to an OrderedDict designed for holding form data.
	; This collection allows duplicate keys and enumerates key value pairs in
	; the order they were added.
	class FormData
	{
		names := []
		values := []
		
		; Add a field to the FormData structure.
		;
		; Parameters:
		;   name - The form field name associated with the value
		;   value - The value of the form field
		;
		; Returns: Nothing
		;
		Add(name, value)
		{
			this.names.Push(name)
			this.values.Push(value)
		}
		
		; Get an array of all values associated with a name.
		;
		; Parameters:
		;   name - The form field name associated with the values
		;
		; Returns: An array of values
		;
		; Example:
		;
		; fd := new NeutronWindow.FormData()
		; fd.Add("foods", "hamburgers")
		; fd.Add("foods", "hotdogs")
		; fd.Add("foods", "pizza")
		; fd.Add("colors", "red")
		; fd.Add("colors", "green")
		; fd.Add("colors", "blue")
		; for i, food in fd.All("foods")
		;     out .= i ": " food "`n"
		; MsgBox, %out%
		;
		All(name)
		{
			values := []
			for i, v in this.names
				if (v == name)
					values.Push(this.values[i])
			return values
		}
		
		; Meta-function to allow direct access of field values using either dot
		; or bracket notation. Can retrieve the nth item associated with a given
		; name by passing more than one value in when bracket notation.
		;
		; Example:
		;
		; fd := new NeutronWindow.FormData()
		; fd.Add("foods", "hamburgers")
		; fd.Add("foods", "hotdogs")
		; MsgBox, % fd.foods ; hamburgers
		; MsgBox, % fd["foods", 2] ; hotdogs
		;
		__Get(name, n := 1)
		{
			for i, v in this.names
				if (v == name && !--n)
					return this.values[i]
		}
		
		; Allow iteration in the order fields were added, instead of a normal
		; object's alphanumeric order of iteration.
		;
		; Example:
		;
		; fd := new NeutronWindow.FormData()
		; fd.Add("z", "3")
		; fd.Add("y", "2")
		; fd.Add("x", "1")
		; for name, field in fd
		;     out .= name ": " field ","
		; MsgBox, %out% ; z: 3, y: 2, x: 1
		;
		_NewEnum()
		{
			return {"i": 0, "base": this}
		}
		Next(ByRef name, ByRef value)
		{
			if (++this.i > this.names.length())
				return False
			name := this.names[this.i]
			value := this.values[this.i]
			return True
		}
	}
}

3
I want to make buttons or other elements in the window can be dragged and sorted automatically.
I searched for some js and css scripts, but when the corresponding webpage is loaded with Neutron,
these elements cannot be dragged freely.

Browsing these web pages with a browser alone,
chrome and Microsoft Edge can access normally, and elements can be dragged.
Using Internet Explorer 11, elements cannot be dragged.
https ksylvest.github.io /jquery-gridly/ Broken Link for safety
https shopify.github.io /draggable/examples/flexbox.html Broken Link for safety

Internet Explorer 11 and Microsoft Edge:
t4.png
t4.png (176.63 KiB) Viewed 6742 times


My test steps:
Use chrome to browse https ksylvest.github.io /jquery-gridly/ Broken Link for safety , then ctrl+s to save the page.
Then use Neutron to load the saved webpage, js reports an error, and dragging has no effect.
In the webpage, I deleted the useless code, only kept the core code, and still can't drag.

What can I do to make Neutron load these pages successfully?


Browser used:
Chrome : 86.0.4240.183
Internet Explorer11 : 11.572.19041.0
Microsoft Edge : 86.0.622.63

rhinox202
Posts: 11
Joined: 30 Sep 2015, 10:22

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by rhinox202 » 13 Nov 2020, 20:35

@GeekDude
Any chance we can get a "sneak peak" at the V1 backport of WebView2? I'd love to check it out, not quite ready to go AHKv2.

User avatar
TheDewd
Posts: 1503
Joined: 19 Dec 2013, 11:16
Location: USA

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by TheDewd » 18 Nov 2020, 01:04

How can I show the GUI controls (Minimize, Maximize, Close) without using HTML "fake" controls? Instead of loading custom HTML, I'm redirecting to an external website, and it removes the titlebar controls.

EDIT: Nevermind. Found it. OnMessage
Last edited by TheDewd on 18 Nov 2020, 13:50, edited 8 times in total.

User avatar
tank
Posts: 3122
Joined: 28 Sep 2013, 22:15
Location: CarrolltonTX
Contact:

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by tank » 18 Nov 2020, 01:12

superb. hats off to lazy to do it myself
We are troubled on every side‚ yet not distressed; we are perplexed‚
but not in despair; Persecuted‚ but not forsaken; cast down‚ but not destroyed;
Telegram is the best way to reach me
https://t.me/ttnnkkrr
If you have forum suggestions please submit a
Check Out WebWriter

el_pablo
Posts: 1
Joined: 27 Nov 2020, 14:55

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by el_pablo » 27 Nov 2020, 15:02

Can I use Neutron to control Discord which is an Electron app?

I would like to have a quick way to switch from Push-to-talk to Voice Activity.

nemezisx
Posts: 2
Joined: 27 Nov 2014, 11:33

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by nemezisx » 31 Dec 2020, 08:34

why neutron does not display scroll bars on my web page, even though when the web page is opened using ActiveX, chrome and IE there is no scroll bar problem. however, when using the neutron, the scroll bar does not appear.
Attachments
003.png
Neutron
003.png (85.89 KiB) Viewed 6176 times
002.png
IE or ActiveX
002.png (108.21 KiB) Viewed 6176 times
001.png
Chrome
001.png (106.26 KiB) Viewed 6176 times

KiddoV
Posts: 13
Joined: 11 May 2020, 20:29

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by KiddoV » 07 Jan 2021, 10:56

Hi, everyone!
I am currently using CodeMirror plugin (codemirror.net/index.html) to make a simple text editor with Neutron (of course). All of the default shortcut keys like: Ctrl+A, Ctrl+C, Ctrl+Z... were working well when I opened html file via IE11, but when I used AHK (Neutron) to load html files, I was not be able to press all the shortcut keys. Any idea why this is happened?
Thanks all!

Post Reply

Return to “Scripts and Functions (v1)”