Advice: Abstracting a Program GUI as a Class?
Posted: 22 Jun 2020, 09:39
I'm an instructional designer on a team of 14 designers. Most of the team uses Adobe Captivate all day, every day. We've made some real productivity gains using AHK with Captivate, but I'm always looking for improvements to the process. Sadly, Captivate isn't easy to automate. It doesn't use COM or Microsoft UI. And the control names change every time the program starts. So we're using a ton of ImageSearch and MouseClick statements, and lately it seems like we're solving the same problems over and over. I feel like I'm spending too much time looking for a snippet of code I wrote two years ago, or updating an old process in one script because I now have a more reliable way to code the problem. Then of course I have to consider every other script that might benefit from that tweak . . .
So I'm thinking about abstracting the Captivate GUI as a class. I'm curious if anyone has any experience in this, and any thoughts to share. Is this a complete waste of time? Is there a reason it's a bad idea? Are there any major roadblocks you can think of? I've posted my starting code below. Right now I'm thinking every panel and every dialog will be a subclass, but I'm open to suggestions.
A few cravats:
Thank you!
So I'm thinking about abstracting the Captivate GUI as a class. I'm curious if anyone has any experience in this, and any thoughts to share. Is this a complete waste of time? Is there a reason it's a bad idea? Are there any major roadblocks you can think of? I've posted my starting code below. Right now I'm thinking every panel and every dialog will be a subclass, but I'm open to suggestions.
A few cravats:
- Everyone on the team uses an identical Captivate workspace, so all buttons and menu options are in the same location. We also have a script to reset the workspace, and almost all code starts with a reset to give us a blank slate.
- Our team has me, an intermediate programmer, as the primary developer. Several beginning programmers help out occasionally, so it's more important for code to be readable than it is for code to be compact.
Thank you!
Code: Select all
Class CpClass
{
class Properties
{
static x:=1665
static y:=160
select()
{
MouseClick, left, this.x, this.y
Sleep, 1200
}
; The Properties panel has two states that determine which controls show.
; If an object on the stage is selected the panel shows one set of buttons.
; We'll call this the primary state since it's used the most.
; If no object is selected, the panel shows a different set of buttons.
; This is the secondary state.
; A few objects (mainly the hamburger menu in the upper corner, and the Style/Options/Actions buttons) are available in both states.
; Methods for these objects are omni-state methods.
; Utility methods are used by both states for things like error checking and state detection.
; -------------------------- PRIMARY STATE METHODS --------------------------
txtfieldObjectName(newName)
{
; won't be available unless an object is selected
this.objectSelected()
MouseClick, left, 1695, 195
spamDelete() ; this is a function in a library. It removes the existing value from the control.
Send %newName%
Sleep, 300
Send {tab}
}
; -------------------------- SECONDARY STATE METHODS --------------------------
menuMasterSlide(masterName)
{
objectNotSelected()
; OK, this one is going to be a pain. The menu is not mouse-navigable.
; Will have to get all the text on the control, and click whatever
; matches the slide type the user wants to select.
}
btnResetMasterSlide()
{
objectNotSelected()
MouseClick, left, 1780, 390
}
menuTheme(option)
{
objectNotSelected()
MouseClick, left, 1735, 235
; INCOMPLETE
}
btnActions()
{
objectNotSelected()
MouseClick, left, 1775, 425
}
menuOnEnter(option)
{
; This will be a pain because unavailable objects are greyed out, and are skipped over when pressing the down key.
; We can't reliably navigate via kbd.
; Instead we need to get the text on the controls.
objectNotSelected()
MouseClick, left, 1655, 475
; INCOMPLETE
}
menuOnExit(option)
{
objectNotSelected()
MouseClick, left, 1655, 530
; INCOMPLETE
}
; -------------------------- OMNI STATE METHODS --------------------------
menuAccessibility(option:=1)
{
; This menu has 2 options if the user DOES NOT have an object selected.
; It has ONE option is an object is selected.
; In the no-obj state, it's also counter-intuitive:
; one down arrow opens the 2nd menu option.
; two down arrows open the 1st option.
MouseClick, left, 1905, 195
objectSelected := superSearch("properties_objectSelected.png", 15, false, false) ; superSearch is a custom ImageSearch function.
if (objectSelected[1] = 0)
{
Send {down}{enter}
} else
{
if (option = 1)
{
Send {down 2}{enter}
} else
{
if (option = 2)
{
Send {down}{enter}
}
else
{
msg := simpleGui("Error", "This hamburger menu only has two options. Reloading.", 250, 100) ; simpleGui is a custom message-box like function that centers the message on the screen where the mouse is located.
}
}
}
}
btnStyle()
{
objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
if (objectSelected[1] = 0)
{
MouseClick, left, 1730, 465
} else
{
MouseClick, left, 1680, 425
}
}
btnOptions()
{
objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
if (objectSelected[1] = 0)
{
MouseClick, left, 1730, 465
} else
{
MouseClick, left, 1680, 425
}
}
; -------------------------- UTILITY METHODS --------------------------
objectSelected()
{
; Make sure the user has an object selected.
; Selected/not selected state radically changes the panel
objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
if (objectSelected[1] != 0)
{
msg := simpleGui("Error", "You don't have an object selected. Reloading.", 250, 100)
WinWaitClose, ahk_id %msg%
Reload
}
}
objectNotSelected()
{
objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
if (objectSelected[1] = 0) {
msg := simpleGui("Error", "You have an object selected. This control isn't available when an object is selected. Reloading.", 250, 100)
WinWaitClose, ahk_id %msg%
Reload
}
}
}
class ItemAccessibility
{
btnCancel()
{
MouseClick, left, 340, 300
}
btnOK()
{
MouseClick, left, 235, 300
}
autoLabel(state)
{
WinWaitActive, Item Accessibility
Sleep, 1500
if (state = "on")
{
; iterate through all possible states. If one if found, click it.
objectSelected := superSearch("accessability_autoLabelOff1.png", 15, offset:=[7,7], false)
if (objectSelected[1] != 0)
{
objectSelected := superSearch("accessability_autoLabelOff2.png", 15, offset:=[7,7], false)
if (objectSelected[1] != 0)
{
objectSelected := superSearch("accessability_autoLabelOff3.png", 15, offset:=[7,7], false)
if (objectSelected[1] != 0)
{
objectSelected := superSearch("accessability_autoLabelOff4.png", 15, offset:=[7,7], true)
}
}
}
} else
{
; iterate through all possible states. If one if found, click it.
objectSelected := superSearch("accessability_autoLabelOn1.png", 15, offset:=[7,7], false)
if (objectSelected[1] != 0)
{
objectSelected := superSearch("accessability_autoLabelOn2.png", 15, offset:=[7,7], false)
if (objectSelected[1] != 0)
{
objectSelected := superSearch("accessability_autoLabelOn3.png", 15, offset:=[7,7], false)
if (objectSelected[1] != 0)
{
objectSelected := superSearch("accessability_autoLabelOn4.png", 15, offset:=[7,7], true)
}
}
}
}
}
}
}