Beginners OOP with AHK

Helpful script writing tricks and HowTo's
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Beginners OOP with AHK

13 Dec 2017, 09:47

Hello,
in this tutorial I would like to clarify what exactly OOP is, it's advantages and disadvantages and some simple tricks how to create simple OOP in AHK.

General:
OOP is a way of approaching development. Many people think that it is "just using class syntax to program something", but that isn't true in the slightest.
In AHK you are even able to create valid OOP without using the class syntax a single time. However thats rather uncommon and I'm only going to explain the easiest method which delivers the best results.
The term OOP is the short version for Object-oriented-programming. The programming term of the Object has been invented in the mid-late 60s.
Object-oriented programming revolves around those Objects. It has become the main way of programming in the mid 1990s and nowadays there are only few languages without OOP.
Although OOP seems like a very definitive term, there are actually several ways of how to approach OOP, for example in AutoHotkey we have Prototype-based programming, but you don't exactly need to know what that means yet.
-> Wikipedia (We are even mentioned in the article :superhappy: )

Why should I use OOP?:
In Object Oriented Programming and in Procedural Programming ( where you work with functions only ) you divide code into smaller parts in order to reuse it. The difference is that in OOP you divide on several layers.
You have several more options to divide in different ways, going from a very, very rough level to the same level of detail Procedural Programming offers.
This offers a lot more options when designing the project you want to do. In turn you really need that planning stage or you will just get lost in the possibilities.
OOP offers a lot of standard actions and conventions that make communicating code a lot easier. Since it is closely derived from the logic of the real world it becomes really intuitive once you get used to it.
It's also easier to hide complex procedures behind an easy accessible intuitive interface - which makes exchanging code with others a lot easier. You can also avoid having collision problems.
It's also easier to create modular programs - where major chunks of code can be replaced without a problem - a neccessity if you want to maintain code once it has been released.
-> These advantages and disadvantages are very abstract I will summarize what they lead to:
  • OOP is better for larger projects
  • OOP is better when working with multiple people on one project
  • OOP is better for creating code that you can reuse
  • OOP is better for sharing code with others
-> OOP is useful for you when you:
  • Intend to work on medium to large programs
  • like to share code or work with others
  • plan to be a developer in the future - since OOP is the most common programming style on the market

General Objects vs. AutoHotkeys Objects:
Objects as a concept within programming need to fulfill a few rules to be called objects:
  1. An Object is more than simple data and more than functionality - it is always both. An Object the data and the modifications you can make to that data.
  2. An Object is always the instance of a Class. A Class defines the data structure and the modifications you can make upon that data structure
  3. An Object protects it's internal workings from outward access and only allows certain actions and modifications to be taken from the outside
AutoHotkeys Objects do not fulfill the third rule at all - however if we ever only code as if the rule was active, then it doesn't really matter.
Not everything that's called an Object within the context of AutoHotkey fulfills the 1st or 2nd rule. In this tutorial I'm only going to talk about objects that fulfill these rules e. g.:

Code: [Select all] [Download] GeSHi © Codebox Plus

class ObjectClass {
}
object := new ObjectClass() ;where object is an object that fulfills rule 1 and 2 and rule 3 is never broken
;1 it has empty data and the ways to modify this data are nonexistant
;2 it is the instance of the class ObjectClass that defines that this object has neither a data structure nor ways to modify it
;3 since we never access the object in any way it was not intended to - therefore rule 3 is never broken and in turn fulfilled


Practical Work:
After a lot of theoretical facts about Objects I'd like to put these facts to work and explain them using a practical example.
I think that this way, more people could understand this tutorial and it would be more interesting.
So we will just create an example Object or rather an example Class in AutoHotkey that's OOP styled.
And for this example I thought of the file system - which means files, directories and drives.

Planning Stage:
Before we start creating an Object we first need a planning stage. For this planning stage take pen and paper - it's not neccessary but it helps tremendously. ( It helps me and my horrible memory at least )
In OOP the programmer orients himself to objects in order to program. Since you are a programmer now you will have to do exactly that.
So the first step is to define the several types of objects you want to program. The topic we chose is filesystem and the types of objects here are files, directories and drives.
Then you take one object and define what kind of information this type of object contains:
files - have a name
...
which I'm just going to write like:
files:
-path (...and all the other path names like name, extension etc.)
-content
--size ( it's the content's size but the attribute might be important even without accessing the content )
-attributes ( like ReadOnly etc. )
-time ( time last edited )
For the beginners course we are going to jump straight ahead into programming.

Initialization Stage:
When you create a new class for a specific type of Object the first step should be creating a new AHK file named like the class you want to create:
So let's create a File.ahk. Afterwards you simply put the code for a new class inside:

Code: [Select all] [Download] GeSHi © Codebox Plus

class File {
}


Standard terms and actions of Objects:
Now let's take a break and get back to the theory. There are several actions that you can apply to each object.
When these actions are taken a specific method inside the object is called.
A method is basically like a function inside an object that gets called.
Here is a list of things that can happen with an object and what kind of method they call.
It's not important to understand what they all do right now - in fact I think you will barely able to understand what they do at all from this short list.
  1. Creating an Object __New( 0-x creation parameters )
  2. Deleting an Object __Delete()
  3. Getting a Key that doesn't exist __Get( 1-x keynames )
  4. Writing a Key that doesn't exist __Set( 1-x keynames, setValue )
  5. Calling a Method __Call( methodName, parameters )
  6. Using the for loop _NewEnum()
  7. Before __New there is __Init()
Depending on what kind of method you add to your class your object will react differently to these events. Some of these methods are often used, others are almost never ( e. g. __init() should not be used according to the documentation ).
I have ordered these by how often I modify them. __New should be a part of almost any class. __Delete is not everywhere, but it's very common. __Get, __Set and __Call are very powerful but also extremly painful to work with.
Rather than using a new _NewEnum() I will often add a method to get an array that for-loops the way I want since it is easier. I have actually never used __Init() before since I can just use __New instead and it should not be used.

When creating an object you should make sure that whatever Object you created has enough information to be valid. This means that after you created a new File object from our file class you should be able to get it's size, path...
That means for our example we need the information, that makes the difference between any file and a specific file. For files this is easy - you have a file path only this file has.
For other objects finding this information is a little more complex - you might find that there are several of those values or that you can use default values for some etc..
However for our simple example the filePath should be enough. The Object needs this information when it is created, therefore it needs this information when __New is called.
In order to achieve this effect we need to add a parameter to the new method of our object and when creating it with the new operator:

Code: [Select all] [Download] GeSHi © Codebox Plus

class Example {
__New( exampleInfo ) {
Msgbox % exampleInfo
}
}
test := new Example( "Test" )
Bug Warning


Back to our class:
We currently have this code:

Code: [Select all] [Download] GeSHi © Codebox Plus

class File {
}
If we add what we know about our __New method:

Code: [Select all] [Download] GeSHi © Codebox Plus

class File {
__New( fileName ) {
}
}

In order to create a valid file Object we also need to save the fileName inside the object that we create.
For that, methods have a simple feature. The Object they are called with is available inside them - it's stored in the variable this.

Code: [Select all] [Download] GeSHi © Codebox Plus

class File {
__New( fileName ) {
this.name := fileName
}
}
abc := new File( "D:\Test.ahk" ) ;for testing purposes
Msgbox % abc.name ;for testing purposes

For testing purposes I have added some lines that create a test file object. And you should probably already see the problem here.
The file probably doesn't exist on your PC yet we created an object that should be valid - however it isn't since the file doesn't exist.
We should probably check if the file exists, inside the __New method and we should also check if it's a file.

Code: [Select all] [Download] GeSHi © Codebox Plus

class File {
__New( fileName ) {
if ( !fileExist( fileName ) )
Msgbox File doesn't exist
if ( inStr( fileExist( fileName ), "D" ) ) ;if file is a directory or a drive
Msgbox File is not a valid File
this.name := fileName
}
}
This should prevent any invalid object from being created.
What to do when object creation fails

The next step is to add functionality that will retrieve all the information we can get from this file. There are 2 ways to do this. One is to use properties. The other is to use getters.
Properties are special to AutoHotkey Objects where a piece of code is triggered when you try to set or get a specific key of an object.
Getters are just methods, where the name of attribute, you want to get, is prefixed with get e. g.file.getFullPath().
You can use both, either way is good. However I prefer getters since they are common in many languages. I will use getters for this tutorial.

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


Now let's test it once again. We will put a test.ahk inside the same folder as our File.ahk:

Code: [Select all] [Download] GeSHi © Codebox Plus

#Include File.ahk
file1 := new File( A_ScriptFullPath ) ;a file object of our test.ahk
file2 := new File( "File.ahk" ) ;a file object of our "File.ahk" containing our file class
Msgbox % file1.getPathDir() ;get containing folder
Msgbox % file2.getPathDir() ;get containing folder

If you run this code, you should see a problem. When we try to get the containing directory of our second file object we won't get any directory.
This is due to a limitation within SplitPath. SplitPath will only return the directory if it was part of the file path given to it.
The file path that we stored in our second object is "File.ahk" - it doesn't contain any path to the directory, thus making it impossible for SplitPath to return this.
The solution is to either prohibit relative paths or resolve any kind of path given to the object into a complete path.
If you prohibit relative paths, the possible applications of our class are reduced and you need to find out whether the path is relative or not.
Resolving the path to a full one it is rather easy though and makes our class more powerful. You can use the File Loop inside the constructor to achieve that:

Code: [Select all] [Download] GeSHi © Codebox Plus

...
__New( fileName ) {
if ( !fileExist( fileName ) )
Msgbox File doesn't exist
if ( inStr( fileExist( fileName ), "D" ) ) ;if file is a folder or a drive
Msgbox File is not a valid File
Loop, Files, % fileName, F ;since the fileName refers to a single file this loop will only execute once
this.name := A_LoopFileLongPath ;and there it will set the path to the value we need
}
...

If we run our test.ahk, after changing File.ahk, we should get the right results.

Getters, Setters and Delegation:
After creating your first Class and creating 2 instances of that class, we should take a step back and review the rules that our Object needs to fulfill in order to be OOP:
  1. An Object is more than simple data, or functionality because it is always both. An Object is the data, ways to access that data and the modifications you can make to that data.
    -> The data is the file path. And so far we only have ways of accessing that data about the specific file. OK
  2. An Object is always the instance of a Class. A Class defines the data structure and the modifications you can make upon that data structure
    -> We use a class to define this object it's data structure and ways to access it. OK
  3. An Object protects it's internal workings from outward access and only allows certain actions and modifications to be taken from the outside
    -> As we have said before this sort of rule is hard to fulfill in AutoHotkey. As long as you use this object the way it was intended to - meaning only creating it or using getters it's as if there is a border and we fulfill this rule OK
That means that our first OOP File Object is ready for use - it's incomplete though.

The next step we need to take is giving the user of this class the ability to modify the attributes of that file. In the easiest case you will add a setter. A setter is just like a getter except instead of getting an attribute it sets the attribute of an Object.
Setters can be used as long as there a single, defined way to set the attribute and setting the attribute to it's corresponding get value, of the same object will cause no change in the object.
Let's check the attributes we have:
  • Path - the path can either be changed by copying or moving the file, since thats not a single way we can't use a setter here
  • Attributes - if you try to set attributes with FileSetAttrib you will have to write down how you want the attributes to change not how they shoud look like in the end
    Generally it's possible to use a setter here. But we need to wrap around the FileGetAttrib and FileSetAttrib commands in a way that would allow the getter value to be used in the setter method without causing changes for the object.
    That would be bothersome. Or rather it's work and generally we don't want a lot of unneccessary extra work.
  • Time - the way to set the time is singular, defined and inputting the get value into the setter won't change the file - we can use a setter here
  • Size - resizing a file is underdefined, resizing changes the content and editing content is something we haven't discussed yet
  • Content - we haven't talked about this at all for a reason
For Time we will add setters. For Path we will add the methods copy and move which both accept a filePath, For attributes I'm thinking of a simple wrapper method around SetAttrib named changeAttributes.

For our content we will use delegation. On a more general layer, delegation is the action of making someone or something else do your work.
If you program an object and realize that there is already an object that does part of your work, you can use this object and delegate the job to that object.
You could also use delegation when you feel that you have a lot of methods specific to a single type of attribute that clutter your Object - for example our Path with all it's getPath() getPathName()....
However we are going to focus, on delegating, the job of managing the files content, to AutoHotkeys built in File-Object. I mean that is what it's good for or?
That means we need a method that returns a new AHK File-Object in our File-Class. Let's call it open()

We also add another method for the last action we can take on our file ( deleting the file ) and call it delete().
With this we need to add the following methods:
setTimeModified( time ), setTimeAccessed( time ), setTimeCreated( time )
copy( filePath ), move( filePath )
changeAttributes( changeAttributesString )
open( mode, encoding := A_FileEncoding )

We will now once again take out our pen and paper and write down all methods next to our properties:
files: __new( fileName )
path: get*(), move( filePath, overwrite ), copy( filePath )
content: open( mode, encoding )
size: get*( unit )
attributes: get*(), change*( changes )
time: get*() set*()
delete()
The point of doing this is that you now have a complete list, of all the methods of your object that you can use to modify it from the outside.
For me this is enough to competely memorize the object. I keep a list of all the objects and their methods in front of me all the time.
This has helped me through many situations and made my life as developer so much easier.

The last practical work on the File Class:
You should probably be able to implement all the changes that need to be done to the fileObject.
But here is the final result:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus



Summary:
In this part of the tutorial we learned:
  • What Objects are in a OOP sense
  • What classes are used for in OOP
  • What a constructor is and what it's good for
  • What methods and attributes are and how to use them
  • What getters and setters are
  • What delegation is and what it is good for

In this part we did:
  • planned our first class
  • coded it
Recommends AHK Studio
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Basic techniques

13 Dec 2017, 09:47

Basic techniques:
After this first step - the step of creating your first class and your first object - we are going to pick up the pace in this second part.
If you do feel lost after a while I reccomend trying to apply what you learned in the first part a second time - this time on your own.
I reccomend using a GUI-Control ( e. g. a Edit Control ) for this since GUI and OOP is the perfect combination.
Otherwise if you really do feel like you can't manage - or if you have troubles understanding the first part - leave a comment.
I'm not perfect and I like to enhance my Tutorials. So any kind of feedback is appreciated.

In this part of the tutorial I'm going to explain what is probably the most important basic technique of OOP. After leaning it you can create basic OOP.
And depending on the quality it would already be on a level that could be called professional - at least if you gain some experience with it.

Extension and Inheritance:
After what we did so far you probably ask yourself "why the f would anybody go through the pain of writing several different classes like that? You need to write so much stuff and remember so much - it's not better than just using functions, it's worse".
Well if you only look at what we did in the first part, you are certainly right about that. We designed a single class that stands for it's own and is not particulary helpful.


You normally plan out an entire group of objects at once. In our case let's look at all the objects we tried to create in the last part:
File, Folder and Drive
This time we will plan them all out at once, instead of doing double work for each and every class we create.
So far we have planned the file class:
-path (...and all the other path names like name, extension etc.)
-content
--size ( it's the content's size but the attribute might be important even without accessing the content )
-attributes ( like ReadOnly etc. )
-time ( time last edited )

Once again we will plan out what kind of attributes our objects contain.
Since we already have this kind of list with attributes that our file has we can use these already present attributes and check our directory and drive if they contain similar attributes.
file:
-path (...and all the other path names like name, extension etc.)
-content
--size ( it's the content's size but the attribute might be important even without accessing the content )
-attributes ( like ReadOnly etc. )
-time ( time last edited )

folder:
-path (...and all the other path names like ...) unlike a single file it doesn't have any extension
-attributes ( should be the same as file I guess )
-time ( should be the same as file )
-content ( however unlike file content it's the files and folders contained )
--size ( a folder also has a size , however it's the size of all it's contained files and folders )

drive:
-path ( it's basically just %driveLetter%:/ and can't really be edited )
-attributes ( should be the same as file - although I'm not sure if you can edit it )
-content ( should be the same as folder )
--size ( should b the same as folder )

And now after we did this we compare the classes and write down the attributes that are exactly the same for all of them:
file, folder, drive:
-path ( only getting the drive name and getting the full path remains the same )
-attributes ( getting the reamains the same )

And now we do the same for each individual combination of file, folder and drive.
However we will leave out all similarities we have already found for all of them.
file, folder:
-path ( getting and setting remains the same for directory, drive, name and full path )
-attribute setting
-time ( getting and setting )

folder, drive:
-path ( getting remains the same for full path and drive )
-content
--size

Now you might ask yourself why we do this - and there is a good reason for this:

Extension explained:
In order to reuse code the original inventors of OOP thought up a technique called extension or inheritance.
In AutoHotkey you could easily think of it as the class of your class.
What happens is that one child class extends a parent class and the child will gain all the methods and attributes defined by the parent and then adds it's own.
Now you see why defining that list of attributes is so important - it helps us define what belongs inside the parent class and what not. However let's talk about the limitations and possibilities of extension first.

The syntax of extension is one of the easiest:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


In this example the class child extends the class parent.

  • one child class extends a parent class: It is indeed that one child can only have one parent class with this syntax, however with advanced techniques you can basically modify classes and what extends what dynamically - the whole line gets blurry. What you need to know is that for basic OOP this sentence holds truth.
    Additionally one parent class can once again extend to another grand parent class which can extend to a grand grand parent and so on and so on.
  • the child will gain all the methods and attributes defined by the parent and then adds it's own: This is not entirely true but rather simplifies what happens. However if you do not probe to deep you will not find a piece of code that breaks this rule.
    It's worth noting that the child can overwrite parent attributes and methods like:

    Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus

Reusing code is always worth it and as a technique to reuse code extension gains the same advatages.
It reduces the amount of work needed to create the classes - to understand how to use it - to document it - to read it's sourcecode - to understand it's sourcecode and to modify it later on.
There are only advantages to extending. The only real reason no to extend a class is that there are no similar classes that are worth the extension - doing an extension with 2 classes that barely correlate at all rather creates confusion than helping.

Back to the drawing board:
We are now at the stage where we need to decide which parent classes to create. For this stage I often utilize drawing to help visualising the possibilities and alternatives.
Apperently many people thought drawing relations like this is a good idea. There is a whole visual diagram family called UML that describes all the different sorts of happenings and relations inside a program.
This would be our example with child extends parent from above in UML:
Image
I will use UML throughout this tutorial sometimes, not because it's the best tool for planning, but rather because I don't have a better tool at hand.
It's a tool many professionals use - how it looks depends on which program you used to draw it with and it isn't exactly suited for AHK.
The only thing less suited for this tutorial is the scribbles that I draw on my mini block in roughly 30 seconds before I dispose of them once a decision has been made, or I sufficiently visualised what they describe.

Anyways back to our files, directories and disks. We now have to plan the parent classes that we want to create and which child classes should extend these.
It takes a lot experience and there's a lot that can be said about how you should extend. I work mostly by intuition and by imagining how the code would like in the final result.
However by logical thinking we can come up with 2 schemes that would allow for the most code reusage for this case:
Image
and
Image
In the first picture Directory and Drive get a specific parent class that lets them share methods. I called this parent class FileContainer because - unlike Files - both Drives and Directories contain Files and Directories.
In the second diagram, File and Directory get a specific parent class that lets them share methods. I called this parent class FileContained because - unlike Drives - both can be contained by Drives and Directories.
The difference is that in the first one we can avoid retyping the get and setContent Methods of drives and directories and that in the second one we have an advantage when it comes to the getPath methods.
I will always try to avoid writing methods twice that are: complex, change often or not fully defined yet.
Our path methods are not complex, probably won't change and are not only already defined but also completely written in code.
We have no idea yet how our our FileContainer content access methods will look like, but I can tell you that it's going to be more complex than the paths.
The first alternative is therefore the better choice according to my standards - even more so because I will show you a little trick how to avoid having to retype every path method over and over again.

The Skeleton Work:
The next step is something I call the skeleton work. In this step we will first create all the classes and how they relate to one another before filling them with any code.
Afterwards we will immediately copy the code we already have to the corresponding classes. I actually enjoy this stage because we get some decent results without having to do a lot of work, while the plan we thought up slowly takes shape.
1st we will create the Skeleton according to our plan:

Code: [Select all] [Download] GeSHi © Codebox Plus

class FileSystemElement {
}
class File extends FileSystemElement {
}
class FileContainer extends FileSystemElement {
}
class Directory extends FileContainer {
}
class Drive extends FileContainer {
}

Then we take each method, that we have already created for our very first class, and check how it could be put inside the class and if it stays the same.
Since we already planned ahead we can pick out the easiest first and go with:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus

These purely belong to the File class, since the commands used inside purely belong to the file class.
Putting them there results in

Code: [Select all] [Download] GeSHi © Codebox Plus

	getAttributes() { ;flag string see AutoHotkey Help: FileExist for more infos
return FileExist( this.name )
}
changeAttributes( changeAttributeString ) { ;see FileSetAttrib for more infos
FileSetAttrib, % changeAttributeString, % this.name
}
getAttributes works for Files,Drives and Directories. changeAttributes doesn't work with Drives. Splitting them apart is not an option and having the same method in multiple places with the same name is also bad.
Since we need getAttributes for all classes we will put in the grand parent class that is available to all and - since we need to keep the pair together - changeAttributes too:
Putting them there results in

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus

We actually face the same dilemma here. Files has all of these Methods, Directory already looses everything regarding Name - like extension and NameNoExtension. Drive only has Path and Drive.
Yet we can't split them since they belong together. So they go into the grand parent class.
The resulting code

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus

Here it's almost the same again - FileGetTime and FileSetTime works for files and directories but not for drives. Once again we put it in our grand parent class.
The resulting code

The final method that we need to integrate is the constructor:

Code: [Select all] [Download] GeSHi © Codebox Plus

	__New( fileName ) {
if ( !fileExist( fileName ) )
Msgbox File doesn't exist
if ( inStr( fileExist( fileName ), "D" ) ) ;if file is a folder or a drive
Msgbox File is not a valid File
Loop, Files, % fileName, F ;since the fileName refers to a single file this loop will only execute once
this.name := A_LoopFileLongPath ;and there it will set the path to the value we need
}
In AutoHotkey a constructor can be inherited therefore we need to ask ourselves if this constructor will work for other classes.
The simple answer is no. This code specifically checks and only works if the input is a file path.
The complex answer

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


With this we have actually integrated our entire File class into the new skeleton.
This means that our entire File class is already useable - you can even try it if you want - it didn't change at all. However Drive and Directory still need some work.

Now it is time for the trick that I have mentioned before. Let's review what it was about:
Some of the methods that we took from the File class are available for all the classes, though they only work for some of them - and they will fail silently.
In programming you should always avoid letting things fail silently. Since the method is there AHK will not show any sort of "missing method error" - ( though in v1 it never does that anyways )
So we have to throw an error by ourselves:

Code: [Select all] [Download] GeSHi © Codebox Plus

	%methodName%(p*) { ;replace %methodName% and (p*) with the name of the function and the actual parameters thats you want to throw this kind of error
Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
}

We can just overwrite methods, that were defined in parent classes, in child classes and we will overwrite methods that are not available in this specific child class with this method.
By doing so we make the user of our class aware, that there is something wrong happening with his code. ( e. g. he expected a file but got a directory object instead for example. )
After applying these changes our class is ready for testing:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus



Testing our class:
We will now once again test the thing, we have created a few times in order to check if it is useable and correct.
In order to do this we will just create a few test cases.
There are several ways of testing classes - for specific classes it might make sense to just test a few methods - for others you test all cases and every inch of it.
For our class and this test - where we create a class around already existing commands - it might be good to create a few use cases using the old syntax and then changing it to work with our class.

Code: [Select all] [Download] GeSHi © Codebox Plus

File := "test.txt"
LongFilePath := A_ScriptDir . "\" . File
SplitPath, LongFilePath, Name, Directory, Extension, NameNoExtension, Drive
FileOpen( File, "w" ).write( Name "`n" Directory "`n" Extension "`n" NameNoExtension "`n" Drive )

This code will create a file named test.txt and appends its name, the directory it is in, its extension, it's name without extension and the drive it is on to it.
If we roughly translate the same code in order to make it use our class it would be something like:

Code: [Select all] [Download] GeSHi © Codebox Plus

#Include FileSystem.ahk ;I named the file containing our classes FileSystem.ahk
File := "test.txt"
fileObj := new File( File )
fileObj.Open( "w" ).write( fileObj.getPath() "`n" fileObj.getPathName() "`n" fileObj.getPathDirectory() "`n" fileObj.getPathExtension() "`n" fileObj.getPathNameNoExtension() "`n" fileObj.getPathDrive() )

When you try this you will see that nothing happens. When you assign data to a variable with the same name of a class ( like in this case File ) you will actually overwrite the class with the data.
This is due to the fact that classes, are essentially objects stored in super-global variables. Our code wouldn't even work if we put it inside a function. We either have to change the name of this variable or the name of the class.
We already add work by using our include - even in such a simple script.

Modularity:
Things that are stored in super global scope always are a massive cause of troubles. Imagine you had one library that defines the class File and another library like Gdip that uses the variable File in almost every function.
You would have to rename either the class or each variable inside Gdip. But imagine you already used that class inside three or four other projects - would you rename it there too?
Create a specific version just to work with GDIp or creating a specific version of GDIp. And after you prefixed your File class with something ( e.g. renaming it to classFile ) you find that another library you have been using acts up and doesn't work with your File class anymore. So you look for alternatives - maybe different libraries or call the Dlls yourself? Or just create your own library?
You will find that in time you will develop your own specific naming sense, that automatically prevents you from getting collisions ( for example I often use short names inside variables and full names in classes: indirectReference vs. indRef ).
Using other peoples work mostly just creates more problems than it solves. So you will often find that you only use code from specific people that you know are compatible with you.
For AutoHotkey users sticking to the standards set by some part of the community is a must. I will share the standards, that I know of, that are of relevance, for OOP stuff, thats meant to be included:
  • Don't use super globals or globals:
    When you write OOP you always have at least one class at hand and classes are always super global. You can use that class almost like any normal object after it's initialized.
    Additionally you can use static to initiallize data directly in the class when it is loaded:

    Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus



  • Avoid global variable names that sound like they would make good class names:
    If something sounds like a good class name then rename that variable.
    Class Names are generally one or multiple full length words which brings us to the next point.

  • Try to keep your class naming sense consistent with everyone else:
    Class names are generally full length words - often combined together. Sometimes people use a prefix - I don't.

  • Use local in your methods:
    With AutoHotkey version 1.1.27.0 you can use a special mode inside methods and functions that allows you to avoid any collisions with super globals.
    If you put local somewhere in your function or method it becomes "force-local" meaning that all super globals will be ignored and only local variables will be read and written to.
    You can still use super globals or globals by defining them with global though.

    Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus



  • Avoid byref to output data:
    AutoHotkey with OOP doesn't need byRef - byRef only requires the user to create a new variable - that variable often then gets stored inside an object.
    You waste a line and force the user of your class to polute his scope with another variable - use arrays or objects instead.

  • Put your classes inside a container class and name your file after that container class:
    This is one of the most important conventions that I had discovered more recently. Essentially you put all of your stray classes inside a container class.
    Additionally you name the file name it contains exactly like that containing class. The classes inside then can be accessed by writing Container.Class, a new instance can be created like new Container.Class()

    Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


    This way you reduce the amounts of numbers of super globals to one and even share the name of that super global in the file name
    Additionally you also gain a new class that you can use to store methods or other functionality that you need to make your classes work but doesn't belong to any of the classes directly.

We will apply these rules step by step to our class:
  • The variables are mostly OK - but I renamed TimeStamp several times as that doesn't sound like an impossible class name - I don't think you even have to be that careful here.
  • We don't use super globals
  • We don't use byRef anywhere
  • I think we should wrap our classes in a class named FileSystem.
When applying these changes we get this final result:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


Notice how the "extends" changed after I wrapped the classes inside the container.


Summary:
In this chapter we learned:
About extension:
  • what extension is
  • how to use it
  • where to use it and how to handle it
  • how to deal with collisions and method distribution
About modularity:
  • what it is
  • why it is neccessary
  • many techniques and conventions to achieve it
Clean coding:
Although I never specifically mention the topic in my tutorial I talk about many conventions that allow for clean coding.

Additional notes:
Even I don't always stick methods together so perfectly like that - it's just to show you what perfectly clean code looks like.

Of course ByRef is never neccessary for output variables - but you could always use it to enhance the speed of input variables.
However that doesn't offer a lot of speed gain - if you want to work with fast strings in OOP, you will have to write a String class.

I'm not sure I ever mentioned this, but .__Class contains the name of the class the Object is from:

Code: [Select all] [Download] GeSHi © Codebox Plus

testInstance := new TestClass()
Msgbox % testInstance.__Class
class TestClass {
}
Recommends AHK Studio
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Tips & Tricks and Ticks & Trips

13 Dec 2017, 09:47

Tips & Tricks and Ticks & Trips

This part will be present in any tutorial that I'm going to write in the OOP series.
It will contain Hints, some standard code techniques, common bugs and some practice classes you could write yourself.

Tips:
The KISS principle: Keep It Stupid Simple - the easier your class is to understand and the less documentation you need to create.
Fewer things will go wrong and working with your class is less frustrating.

Convention over configuration: ( I can't seem to find an easy description for this )

Write as if you write for others: If your code is something that you might use across quite a few projects ( e.g. you plan to write quite a few games and you write a library for video output ) then write as if you wrote for other people.
That way you will probably be able to reuse your class the longest time and you might being able to use this class a lot longer and make following projects a lot easier.

Review your code and look for patterns: If you reuse code you will start noticing that you repeat specific sequences of code or even specific classes that are similar to one another.
This happens because you reuse your code - these patterns can hint that it might be neccessary to add a new method to your class for the sake of convenience or might even lead to a new class.
Classes like these are often extremly useful and further help expanding the advantage of reusing code - you should reuse every code that you write at least for a little while to find some patterns and problems.
You could even find out a solution to a problem that you constantly face ( like I did with my indirectReference to face circular references )

Objective evaluation and tweak, rewrite or abandon: After you are done with your class you will often feel like it's a big pile of *fish* - that's perfectly normal.
When you finish your class you are already a better developer with more insight into the field of your class than the you that designed and wrote your class.
I immediately feel the urge to completely rewrite my class. However it is important to resist these immediate urges.
You need to take the time and opbjectively have a look at your class:
  • Can you use this class to do what you initially planned?
  • Is the task easier using your class - Is it actually easy to use?
  • Can you modify and maintain the class easily - is it possible to adjust your class to new use cases that you have found while developing it?
Depending on the answer to these questions you will have to act: Rewrite the class if it doesn't help you at all - and you have found a better way to write it.
If it's not easy to maintain or modify then use it until you really need to modify it, then rewrite it.
For all the other things just tweak it a little.
If your class is not useable and you have no idea how to fix it and you think you wouldn't do better after a rewrite then you probably need to take an easier project.

Tricks:

Easy Collections:
Collections are a datatype which contains multiple objects - however unlike arrays each object can only be inside the collection or not. Order also doesn't matter within collections.
If you were to implement this in AutoHotkey you would probably use an array. Then when you add an object you would have to check if it's there already before adding it. You also have to search before removing it. You even have to search just to find if it's inside the array.
This is rather slow and expensive.
But there is an easy way to solve this problem - each object has a unique and specific address that won't change as long as the object exists.
If you use the objects address, as the key, for the object, in an array, you can easily recreate the behaviour of collections:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus



Including inside classes:
In AutoHotkey acts as if the code from another file would be at this exact position.
That means you can also include a class inside a class. Using this trick our FileSystem class would look like:

Code: [Select all] [Download] GeSHi © Codebox Plus

class FileSystem {
#Include FileSystemElement.ahk
#Include File.ahk
#Include FileContainer.ahk
#Include Directory.ahk
#Include Drive.ahk
}

And we moved all the individual classes into seperate files.
Doing this is a matter of preference - I don't do it.

Move methods of another class:
It is possible to move single methods from one class to one another in the source code by using:

Code: [Select all] [Download] GeSHi © Codebox Plus

instance := new gotMethod()
instance.theMethod()
class gotMethod {
static theMethod := hasMethod.theMethod
}
class hasMethod {
theMethod() {
Msgbox This is not any method - this is THE method
}
}
This works due to the fact that all methods are in fact function objects stored inside the class object.
The gotMethod class uses static to initialize a new key, that will be stored inside itself and assigns the func object stored inside the class hasMethod.
We could have used this to clone our "method not available method" or other methods, but I forgot about this until recently.

Using bind to use methods in callbacks:
If you have commands like SetTimer or have to use a gLabel and you might find it difficult to call an object method with them.
The object needs to be supplied to the method however SetTimer and gLabels only accept functions.
One solution is to use .bind to create a boundfunc object which SetTimer and gLabels can also use:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


Generally speaking are bound funcs very important to turn object methods into simple callback functions.
For more advanced things you will still probably have to use a wrapper function.

Ticks:
In this section I will list the most common bugs or oversights that can cause bugs.
I will not explain every bug here since some of the bugs do not matter in the field of basic OOP.
It's supposed to be a checklist that can help you finding your bug more easily.
  • Typos
    Well this one is pretty basic, but it's still the most common cause of bugs.
    Using a proper Editor and using the right plugins for it is going to help ( see my signature for my reccomendation ).
  • for-loops, enumerators and changing the array while enumerating
    For loops use an object - an enumerator - to extract the contents of any object. This enumerator will simply get the first key-value pair, then the second, the third...
    However when you add or remove a key-value pair, e. g. the first key value pair, then the second key-value pair will become the first etc.
    Our enumerator however will go on getting the next in line completely ignoring the fact that the line just changed.
    In practice that means that our for loop might miss ( .delete or remove ) or repeat ( .insert ) a key-value pair:

    Code: [Select all] [Download] GeSHi © Codebox Plus

    testArray := [ "first", "second", "third", "fourth" ] ;our example
    for key, value in testArray ;our for loop
    testArray.removeAt( key ) ;seems like it would delete everything
    Msgbox % testArray.length() ;still has 2 elements left since we skipped 2 elements in the iteration above
    Msgbox % testArray.1 . "`n" . testArray.2

    There are only 2 methods around this:
    Either cloning the array that is being enumerated - at the cost of memory, a bad idea if you have a lot of large strings in the object, or in situations where things simply can't be cloned
    or simply storing what needs to be changed and doing it after you're done enumerating.
  • Naming an attribute just like a method:
    This does happen sometimes when you are not careful. As I have said before it is possible to overwrite methods from parent classes inside child classes.
    The same is also possible for objects and their classes - you can overwrite methods defined inside the objects class inside an object.
    So if you have a method inside a class, named just like an attribute of the object, the attribute will actually overwrite the method:

    Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


    The way around this is simple don't name your attributes like methods.
  • Circular references - no explination given
  • Bad call to - no explination given
  • __Get, __Set, __Call and the "callback hell" - no explination given

Trips:
In the following section I would like to present you some ideas for projects, either by showing you projects that can be done or code that was already created that you can use to create some pretty fancy stuff.

A function to display the contents or the state of any object or array that you create is a must have. You need it for debugging, something you will be doing a lot - at least I seem to do it a lot.
However since we have no built in standards for this each and every single developer prefers his own function and we will probably never agree on any standard.
You will also need a function like that. Consider this very basic example as your starting point:

Code: [Select all] [Download] GeSHi © Codebox Plus

array := [ "first", "second", "third" ]
string := "[ "
for key, value in array
string .= ", " . value
Msgbox % string . " ]"


ObjRegisterActive() is a function created by Lexikos which allows you to share your objects with other processes using COM.
Working with multiple processes has never been easier.

In a similar fashion GeekDudes [url?https://github.com/G33kDude/RemoteObj.ahk]RemoteObj()[/url] lets you share your object using a socket - which means LAN or the internet.

GDIp is available in OOP under https://github.com/tariqporter/Gdip2 if you know GDIp it might be the perfect thing for you to play around with a new OOP library.

GUIs generally work very very well with OOP - try making a OOP picture control and add it to an OOP GUI, or do you also think it would be easier if ListViews were OOP?

Finally
Don't be afraid to share your creations even if you think it's bad. I will have a look at it and give my opinion on it and ways to potentially enhance it.
Also if you have any suggestions for libraries that can be used with basic OOP easily, then I will add them.

I hope I see all of you guys in the next part of this tutorial series.

Have Fun Coding :rainbow:
Recommends AHK Studio
Helgef
Posts: 3151
Joined: 17 Jul 2016, 01:02
Contact:

Re: OOP with AHK

13 Dec 2017, 10:22

Nice write up, thanks for sharing, I look forward to the continuation :thumbup:
if you pass the wrong count of parameters to an object method AutoHotkey will say nothing.
v2 throws on too few parameters, not surplus :thumbup:

Code: [Select all] [Download] GeSHi © Codebox Plus

Loop, Files, % fileName, F ;since the fileName refers to a single file this loop will only execute once
this.name := A_LoopFileLongPath ;and there it will set the path to the value we need

Neat :thumbup:

Cheers.
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: OOP with AHK

13 Dec 2017, 12:21

I have added more to thw first part.
It will be finished soon - it's almost finished it just needs some pieces of information.
It's a large topic though so I can add more parts later and discuss the a bit more advanced stuff and some tips and tricks for routine work.

Helgef wrote:Nice write up, thanks for sharing, I look forward to the continuation :thumbup:
Thanks
Helgef wrote:
if you pass the wrong count of parameters to an object method AutoHotkey will say nothing.
v2 throws on too few parameters, not surplus :thumbup:
Thats almost a reason for me to switch to AHK v2.
Helgef wrote:

Code: [Select all] [Download] GeSHi © Codebox Plus

Loop, Files, % fileName, F ;since the fileName refers to a single file this loop will only execute once
this.name := A_LoopFileLongPath ;and there it will set the path to the value we need

Neat :thumbup:

Cheers.
I had to look for that one quite a bit - I'm glad you like it.
Recommends AHK Studio
User avatar
boiler
Posts: 2372
Joined: 21 Dec 2014, 02:44

Re: OOP with AHK

13 Dec 2017, 13:58

nnnik wrote:An Object is always the instance of a Class.

Does this mean that array objects are considered classes in AHK, even though they are not defined using the "Class" keyword?
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: OOP with AHK

13 Dec 2017, 14:59

boiler wrote:
nnnik wrote:An Object is always the instance of a Class.

Does this mean that array objects are considered classes in AHK, even though they are not defined using the "Class" keyword?

Not everything is either a class or an object. I don't actually know what makes a class to a class since you can abuse AutoHotkeys class keyword in order to define things that are not classes.
For simplicity I simply think that everything that is used like a class. And in prototype based languages this is a lot more that just classes:

Code: [Select all] [Download] GeSHi © Codebox Plus

abc := { __New: func( "newTest" ) test:func( "testMsgbox" ) }
test1 := new abc( "He", "ll", "o ", "Wo", "rld!" )
test.test()
newTest( this, values* ) {
this.s := ""
for each, value in values
this.s .= value
}
testMsgbox( this ) {
Msgbox % this.s
}
Is the same as:

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus

Recommends AHK Studio
coffee
Posts: 65
Joined: 01 Apr 2017, 07:55

Re: OOP with AHK

13 Dec 2017, 20:15

boiler wrote:
nnnik wrote:An Object is always the instance of a Class.

Does this mean that array objects are considered classes in AHK, even though they are not defined using the "Class" keyword?

The "An Object is always the instance of a Class" applies to languages like java and other class-based languages. Autohotkey uses prototypes, the template is an object itself that you can manipulate directly, instantiating it is optional.
From what I've found out through time, some mechanical differences when creating an object using class vs manually in Autohotkey (there may be more):
Class keyword makes the variable superglobal
Adds a __class field containing the name of the class, which is used by the destructor to prevent it from running in non-instances (as per doc). Although __delete doesnt run either on non-class created objects if you dont instantiate.
Uses __init() to process instance variables, and well, it allows you to define instance variables (outside of any method in the class), which i dont think you can when building the object using {}. Since:
As far as im aware, these two are more or less the same

Code: [Select all] [Download] GeSHi © Codebox Plus

class myobject {
static thekey:="thevalue"
}
; or
myobject:={thekey: "thevalue"}

But my object could be an instance of the default base object, I dont know.
Both will allow you to

Code: [Select all] [Download] GeSHi © Codebox Plus

newObject:=new myobject()
; and
msgbox(newObject.thekey) ; shows thevalue
; will pull the key from the base object "myobject"
newObject.anotherkey:="hello"
; and then
anotherNewObject:=new newObject()
; for which
msgbox(anotherNewObject.anotherkey) ; shows hello


Instance variables are also accessible to instances of an instance.

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


Between static and instance variables, once they are created, they don't make much difference in terms of access and inheritance, only in the way they are "defined/declared" and when, everything will be available by searching on the base object, then the base of the base and so on until it reaches a black hole.

So you could say, that an array object is "not identical" to using the class keyword. But they are both objects that you can access directly and instantiate if you want.

When creating an empty object,

Is the same as (but may be changed in the future as per what the doc says)

is the same as

which ends up being the same as

Code: [Select all] [Download] GeSHi © Codebox Plus

class myobject {
}

aside from what the class keyword provides. But if make the first 3 superglobal, then you can create instances anywhere.

And you can do

Code: [Select all] [Download] GeSHi © Codebox Plus

myobject.member:="value"
; creating it with [] doesn't prevent you from using non-integer keys

on all four of the above. Or give them function references, or use the object functions from the doc.

But [] and {} behave differently when pushing values on creation. [] uses integer keys only (linear array), while {} and object() allows you to set custom keys (associative array).


In autohotkey,the class keyword allows you to create an object with better visualization/organization. Like nnnik posted above, you can either build an object (template/prototype) using {} or object() using references to external functions, or using the class keyword and typing the fields/methods yourself, or also referencing external functions if that's how you roll. That is one of the "argued advantages" of prototype languages, you don't need to extend an entire class with stuff you don't need, you can just build it with what you need, then instantiate as needed. Create some functions that will be used by multiple objects, build the objects, then instantiate, instead of extending class after class carrying everything along.

You can also change the superglobal aspect by just reassigning it to another variable and removing the reference on the original value. You can also delete the __class key.

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus



On nnnik's example, you can also manually set the base object using .base, or for a class, using extends. Or if you don't use extends, you can later use .base on it.

Code: [Select all] [Download] GeSHi © Codebox Plus

myobject.base:=anotherobject
; or
class myobject extends anotherobject {
}


In short, creating an object using class makes it easier if you need that object to have a structure (methods, keys etc) before you use it either directly or instantiated or if it will be self contained. Or if you just want to use class to create objects because you don't like {} or object(), you can, there doesn't seem to be any rules with autohotkey objects, it's a free for all.
You can run an entire script inside a class if you want, without creating an instance, "simulating" java, or run it inside a function like c++, or just not.
User avatar
boiler
Posts: 2372
Joined: 21 Dec 2014, 02:44

Re: OOP with AHK

13 Dec 2017, 21:05

Thank you for the detailed insight.
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: OOP with AHK

14 Dec 2017, 01:05

Well this is information I want to compile in the second part of the tutorial with deeper details on how exactly bases work.
Recommends AHK Studio
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

02 Jan 2018, 12:43

I think I finished the first part - I want to hear your thoughts on this and mostly if you had trouble understanding anything or if you had trouble following it at times.
I also added the first few lines of the second part.
Recommends AHK Studio
euras
Posts: 335
Joined: 05 Nov 2015, 12:56

Re: Beginners OOP with AHK

04 Jan 2018, 14:26

it's great, I'm waiting for the second part :) thank you nnnik!
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

04 Jan 2018, 16:47

Thanks I'm glad to hear someone liked it.
I want to thank yawik ( yawikflame here in the forums ) who read my tutorial and suggested some enhancements and corrected typos.
Recommends AHK Studio
User avatar
Gerdi
Posts: 102
Joined: 03 Aug 2015, 18:48
GitHub: grrdi
Location: Germany

Re: Beginners OOP with AHK

05 Jan 2018, 09:28

Many Thanks nnnik, :thumbup:

by the way,
see FileExit for more infos
i think this means "see (AutoHotKey-help-item) FileExist for more Infos"

:clap:
Win 10 Home (x64) or Win 7 Enterprise
https://github.com/Grrdi/ZackZackOrdner/archive/master.zip --> get folders on the quick
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

05 Jan 2018, 12:19

Thanks Corrected
Recommends AHK Studio
Helgef
Posts: 3151
Joined: 17 Jul 2016, 01:02
Contact:

Re: Beginners OOP with AHK

07 Jan 2018, 06:50

Hello again :wave:.
It is a good read, well done :thumbup: . I like that you encourage planning, eg, with paper and pen, it really is helpful. I will add a little planning tips: Start by writing the documentation. This is not easy to do when one works on a project alone, because most often one is to eager to start coding, but for shared projects, it is key, imo. Another approach (sort of a digital version of the paper and pen suggestion or a good way to start after you have your plan on paper) is to write an empty class, that is a class which only contains methods and properties but no actual implementation. For the file example, one could begin to write this, and add comments about input/output/algorithms etc,...

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus


This gives a good overview and is a good base for the documentation. You keep the sketch and then implement a copy of it, updateing the sketch if needed, when done with the implementation, much of the documentation is done too.


__init() is not even documented

It is mentioned under instance variables.
The method name __Init is reserved for this purpose, and should not be used by the script.


Thanks for your efforts.
Cheers.
User avatar
nnnik
Posts: 3201
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

09 Jan 2018, 16:52

Thanks for the comments Helgef - I will add them at a later date.
I have added another part of the tutorial - and I will leave you with a cliffhanger this time I guess.
Recommends AHK Studio
wolf_II
Posts: 2103
Joined: 08 Feb 2015, 20:55

Re: Beginners OOP with AHK

11 Jan 2018, 10:46

Great tutorial, thank you very much!
User avatar
derz00
Posts: 495
Joined: 02 Feb 2016, 17:54
GitHub: derz00
Location: Middle of the round cube

Re: Beginners OOP with AHK

12 Jan 2018, 12:41

I must read this sometime. Am also in a edX course, learning Python and the science in general. Coming back to AHK it seems really loose, etc. But just the other day I wrote my first class. So I am wondering if you can give any tips? It works well, and does what I want. But it seems like a lot of this. :P Is that just the way it is? Or there would be better ways to design it?

(It is to replace SplashText (gone from v2) in one of my applications.)

Code: [Select all] [Expand] [Download] GeSHi © Codebox Plus

Last edited by derz00 on 12 Jan 2018, 14:51, edited 1 time in total.
try it and see
...
stealzy
Posts: 74
Joined: 01 Nov 2015, 13:43

Re: Beginners OOP with AHK

12 Jan 2018, 14:50

Code: [Select all] [Download] GeSHi © Codebox Plus

this.v:=GuiCreate("-Caption")
g:=this.v
g.SetFont("s16")

Remember only limitation — you can't change this.v by assigning new object to g.
Last edited by stealzy on 12 Jan 2018, 15:02, edited 2 times in total.

Return to “Tutorials”

Who is online

Users browsing this forum: No registered users and 3 guests