[a128] reassigning Gui.Control prototypes

Report problems with documented functionality
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

[a128] reassigning Gui.Control prototypes

10 Mar 2021, 09:56

here i want to assign a different object to the Gui.Text.Prototype so that when an instance of the Gui.Text control is instantiated it identifies as something other than Gui.Text and also has its own custom methods

Code: Select all

#Requires AutoHotkey v2.0-a128-f63b0eb4

Gui.Text.Prototype := {
	__Class: 'custom text ctrl', 
	customMethod: this => MsgBox(A_ThisFunc)
}

G := Gui()
textCtrl := G.Add('Text')
MsgBox textCtrl.__Class ; 'Gui.Text', NOT 'custom text ctrl'
textCtrl.customMethod() ; "Gui.Text: no such method found"
however, it seems this isnt possible because when ahk instantiates controls it fetches the prototypes-to-be-assigned NOT based on what the prototype chains look like at runtime, but based on the original prototypes that it internally keeps a reference to:
https://github.com/Lexikos/AutoHotkey_L/blob/alpha/source/script_gui.cpp#L700-L706

Code: Select all

Object *GuiControlType::GetPrototype(GuiControls aType)
{
	ASSERT(aType <= _countof(sPrototypes));
	if (aType == GUI_CONTROL_TAB2 || aType == GUI_CONTROL_TAB3)
		aType = GUI_CONTROL_TAB; // Just make them all Gui.Tab.
	return sPrototypes[aType];
}
and https://github.com/Lexikos/AutoHotkey_L/blob/alpha/source/script_gui.cpp#L2660

Code: Select all

control.SetBase(GuiControlType::GetPrototype(aControlType));
is that by design?
for contrast, if i try the same thing with regular classes, it works - the prototypes are swapped and the new instance has inherited the swapped prototype:

Code: Select all

class Dog
{
	speak() => MsgBox('woof')
}

Dog.Prototype := {speak: this => MsgBox('meow')}

d := Dog()
d.speak() ; 'meow', this is Ok
lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

Re: [a128] reassigning Gui.Control prototypes

11 Mar 2021, 04:25

It is by design.

Replacing the native prototypes is not supported because:
  • Supporting it (especially for all types) would add overhead, increase complexity and have a non-trivial cost in terms of my time.
  • The native prototypes are used to identify which native methods or properties can be safely applied to the object. For instance, (Gui.Control.Prototype.Opt)(textCtrl, options) would fail because textCtrl is not derived from the prototype object originally associated with Opt. Adding Opt: Gui.Control.Prototype.Opt to your new prototype and calling textCtrl.Opt(options) would have the same result.
  • The restrictions for setting Base (which are in place because of the point above) would in most cases prevent you from assigning a native prototype as the base of your new prototype. Instead, you would have to construct an instance of the original native type, and then use that as a prototype. In this case, you would have to create a Gui with a Text control, and use the Text control's object as your prototype.
  • I see little reason to replace a built-in prototype. Other means of extending the built-in classes are available, and are less error-prone. For instance, if you just define each property you want, you are unlikely to end up with a control which has none of the original methods (as in your example) or that doesn't identify as a Gui.Control; and it is more likely that your script will work with other scripts that define their own properties.

Built-in types which are not intended to be instantiated directly (by calling the class) do not necessarily have __New or __Init called when they are "constructed" (even if you define them). The classes are defined only to support type checking and extension by altering the prototype's properties.


I considered making Prototype like Base, a built-in property of the Class, and might yet. I haven't because it's more work and more code (requiring not just the implementation of the property but also implementation of Class as a distinct C++ type, which is trivial to do but will increase binary size), with relatively trivial impact on scripts.
  • Some operations should be faster, like instantiating classes.
  • Some additional restrictions might be enforced: no setting Prototype, x is y requiring a true Class and not just any object with a Prototype, maybe others.
  • Class() would create a Prototype for itself, instead of requiring the script to set one; Class(BaseClass) would probably be added to allow the construction of a Class and Prototype based on a native class other than Object.

For custom GUI controls, one idea was for G.Add(Ctrl) to do the equivalent of (Gui.%Ctrl%)() (which would require changing construction of controls to work more like you expect). In that case, what you should do (just hypothetically, since you probably shouldn't do it) is replace Gui.Text itself, not its Prototype, if you want G.Add('Text') to return a new type of control. (The proper equivalent would hypothetically be to subclass Gui and define Text in your subclass, perhaps as a nested class.)


As for classifying this as a bug or not, setting an object as Prototype does in fact cause that object to be used as the base of all future instances of the class:

Code: Select all

Gui.Text.Prototype := {__Class: 'Fubar'}
MsgBox type((Gui.Text)()) ; Fubar
If an object is not derived from Gui.Text.Prototype, it is not an instance of the Gui.Text class. Therefore, G.Add('Text') does not create an instance of this Gui.Text class in your case. ;)

The documentation says the Call method (ClassObj()) sets the base of the new object to ClassObj.Prototype. However, who says the class is being called?

The documentation also says Prototype retrieves or sets the object on which all instances of the class are based. However, I think it should be obvious that any new value you set isn't automatically used by all existing instances, so which instances? And what's an instance of the class anyway? GUI controls are not constructed from the Gui.Text class. If an instance is defined as any object derived from Gui.Text.Prototype, they are instances of that class by default, but not after you reassign Prototype. Certainly, textCtrl is Gui.Text is false in your case.
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: [a128] reassigning Gui.Control prototypes

11 Mar 2021, 05:44

thanks for the writeup

the reason why id want to do this is because of an idea i was toying with(in light of recent guicontrol extension topics) that goes like something like this:
  • i want the code to be a library
  • besides adding an #Include statement, i dont want clients to have to do anything else to enable the extensions
  • the extensions have to retroactively apply to all controls that still identify as native ahk controls, without the having to change client code(no special constructors, no new functions, no manual .base/.prototype reassignment after the fact)
  • applying the extensions may override default control methods, but it definitively is not allowed to overwrite them
  • also, its not allowed to mix the extensions with the default methods(which implicitly follows from the above rule)
  • the controls still need to behave and identify as native ahk controls(in addition to identifying as whatever extension archetype theyre deemed to be)
  • multiple extension libraries targeting the same control type need to be interoperable
so what i came up with is:
  • ill have a UDF class with a static __New() handle the automatic application of the extensions
  • ill slice and store the prototype chain of say Gui.Text, ill assign Gui.Text my own extension prototype, then ill hang the original saved prototype chain back onto my extension prototype. This takes care of:
    • the client not having to change their code
    • of the extensions being applied universally retroactively
    • of not mixing and overwriting methods
    • of keeping type information intact(what used to be a Gui.Text is now a Gui.TextEx > Gui.Text > Gui.Control > Object > Any)
    • of the interoperability issue(a prototype chain could look like Gui.TextExMyImplementation > Gui.TextExPossiblyByAnotherAuthorIfTheyCompliedWithThisProposedPattern > Gui.Text > Gui.Control > Object > Any)
the only problem with this approach is it can break client code if it relies on a particular prototype existing in a particular place in the chain.
also, the order of the #Includes dictates the precedence

if u have other ideas about how to achieve this while satisfying these requirements, share it pls

Return to “Bug Reports”

Who is online

Users browsing this forum: No registered users and 38 guests