EventGhost/EventGhost

View on GitHub
plugins/Window/FindWindow.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-
#
# This file is a plugin for EventGhost.
# Copyright © 2005-2020 EventGhost Project <http://www.eventghost.net/>
#
# EventGhost is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 2 of the License, or (at your option)
# any later version.
#
# EventGhost is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with EventGhost. If not, see <http://www.gnu.org/licenses/>.

import wx
from os.path import basename

# Local imports
import eg
from eg.WinApi import (
    GetClassName,
    GetProcessName,
    GetTopLevelWindowList,
    GetWindowChildsList,
    GetWindowText,
    GetWindowThreadProcessId,
)
from eg.WinApi.Dynamic import GA_ROOT, GetAncestor

STOP_IF_NOT_FOUND = 0
STOP_IF_FOUND = 1
STOP_NEVER = 2

class Config(eg.PersistentData):
    hideOnDrag = True


class FindWindow(eg.ActionBase):
    name = "Find a Window"
    description = (
        'Searches for a window, which is afterwards used as a target for '
        'further window actions in the macro.\n\n<p>'
        'If a macro has no "Find a window" actions, all window actions will '
        'target the frontmost window.<p>'
        'In the edit boxes you can use the curly brace wildcards {*} to match '
        'any string sequence and {?} to match a single letter.'
    )
    iconFile = "icons/FindWindow"

    class text:
        label = "Find Window: %s"
        label2 = "Find frontmost window"
        drag1 = "Drag me to\na window."
        drag2 = "Now move me\nto a window."
        refresh_btn = "&Refresh"
        onlyFrontmost = "Only match frontmost window"
        invisible_box = "Search invisible items also"
        hide_box = "Hide EventGhost while dragging"
        stopMacro = [
            "Stop macro if target is not found",
            "Stop macro if target is found",
            "Never stop macro"
        ]
        matchNum1 = "Only return"
        matchNum2 = "'th match"
        wait1 = "Wait up to"
        wait2 = "seconds for the window to appear."
        testButton = "Test"
        options = (
            "Program:",
            "Window Name:",
            "Window Class:",
            "Child Name:",
            "Child Class:"
        )

    if eg.debugLevel:
        @eg.LogIt
        def __del__(self):
            pass

    def Compile(
        self,
        program=None,
        winName=None,
        winClass=None,
        childName=None,
        childClass=None,
        matchNum=1,
        includeInvisible=False,
        timeout=0,
        stopMacro=STOP_IF_NOT_FOUND
    ):
        if stopMacro is None:
            return UseForegroundWindowOnly

        matcher = eg.WindowMatcher(
            program,
            winName,
            winClass,
            childName,
            childClass,
            matchNum,
            includeInvisible,
            timeout,
        )

        def Do():
            hwnds = matcher()
            if (
                (stopMacro == STOP_IF_NOT_FOUND and not hwnds) or
                (stopMacro == STOP_IF_FOUND and hwnds)
            ):
                eg.programCounter = None
            eg.lastFoundWindows[:] = hwnds
            return hwnds
        return Do

    def Configure(
        self,
        program="",
        winName=None,
        winClass=None,
        childName=None,
        childClass=None,
        matchNum=1,
        includeInvisible=False,
        timeout=0,
        stop=STOP_IF_NOT_FOUND
    ):
        panel = eg.ConfigPanel(resizable=True)
        text = self.text
        searchOnlyFrontmost = False
        if stop is None:
            program = ""
            matchNum = 1
            includeInvisible = False
            timeout = 0
            stop = 0
            searchOnlyFrontmost = True
        self.dialog = panel.dialog
        self.lastHwnd = None
        self.lastPid = None
        self.hideOnDrag = True

        # the "only search for the frontmost" checkbox
        cbOnlyFrontmost = wx.CheckBox(panel, -1, text.onlyFrontmost)

        def OnSearchOnlyFrontmostCheckbox(event):
            flag = not cbOnlyFrontmost.IsChecked()
            cbIncludeInvisible.Enable(flag)
            if not flag:
                stopMacroCtrl_1.SetValue(False)
                stopMacroCtrl_2.SetValue(False)
            stopMacroCtrl_1.Enable(flag)
            stopMacroCtrl_2.Enable(flag)
            waitCtrl.Enable(flag)
            for checkBox, textCtrl in self.options[:-1]:
                checkBox.Enable(flag)
                textCtrl.Enable(flag and checkBox.GetValue())
            self.options[-1][0].Enable(flag)
            self.options[-1][1].Enable(flag)
            event.Skip()
        cbOnlyFrontmost.Bind(wx.EVT_CHECKBOX, OnSearchOnlyFrontmostCheckbox)

        # the IncludeInvisible checkbox
        cbIncludeInvisible = wx.CheckBox(panel, -1, text.invisible_box)

        def OnIncludeInvisibleCheckbox(event):
            hwnd = self.lastHwnd
            self.tree.includeInvisible = cbIncludeInvisible.IsChecked()
            self.tree.Refresh()
            self.tree.SelectHwnd(hwnd)
            event.Skip()
        cbIncludeInvisible.Bind(wx.EVT_CHECKBOX, OnIncludeInvisibleCheckbox)

        # the stop-macro choice
        id_1 = wx.NewId()
        stopMacroCtrl_1 = wx.CheckBox(panel, id_1, text.stopMacro[0])
        id_2 = wx.NewId()
        stopMacroCtrl_2 = wx.CheckBox(panel, id_2, text.stopMacro[1])

        if stop == STOP_IF_NOT_FOUND:
            stopMacroCtrl_1.SetValue(True)
            stopMacroCtrl_2.Enable(False)
        elif stop == STOP_IF_FOUND:
            stopMacroCtrl_2.SetValue(True)
            stopMacroCtrl_1.Enable(False)

        def OnStopMacroCtrl(event):
           # if cbOnlyFrontmost.IsChecked():
            id = event.GetId()
            flag = event.IsChecked()
            if id == id_1:
                stopMacroCtrl_2.Enable(not flag)
                if flag:
                    stopMacroCtrl_2.SetValue(False)
            else:
                stopMacroCtrl_1.Enable(not flag)
                if flag:
                    stopMacroCtrl_1.SetValue(False)
            event.Skip()
        stopMacroCtrl_1.Bind(wx.EVT_CHECKBOX, OnStopMacroCtrl)
        stopMacroCtrl_2.Bind(wx.EVT_CHECKBOX, OnStopMacroCtrl)

        finderTool = eg.WindowDragFinder(
            panel,
            self.OnFinderToolLeftClick,
            self.OnFinderTool
        )
        self.finderTool = finderTool

        # the HideOnDrag checkbox
        cbHideOnDrag = wx.CheckBox(panel, -1, text.hide_box)
        cbHideOnDrag.SetValue(Config.hideOnDrag)

        def OnHideOnDragCheckbox(dummyEvent):
            Config.hideOnDrag = cbHideOnDrag.IsChecked()
        cbHideOnDrag.Bind(wx.EVT_CHECKBOX, OnHideOnDragCheckbox)

        # the tree to display processes and windows
        self.tree = eg.WindowTree(panel, includeInvisible=includeInvisible)
        cbIncludeInvisible.SetValue(includeInvisible)

        # the refresh button
        refreshButton = wx.Button(panel, -1, text.refresh_btn)

        def OnButton(dummyEvent):
            hwnd = self.lastHwnd
            self.tree.Refresh()
            self.tree.SelectHwnd(hwnd)
        refreshButton.Bind(wx.EVT_BUTTON, OnButton)

        # construction of the layout with sizers
        dragSizer = wx.StaticBoxSizer(
            wx.StaticBox(panel, -1, "Drag Finder"),
            wx.VERTICAL
        )
        dragSizer.Add(finderTool, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 4)
        dragSizer.Add(cbHideOnDrag)

        leftTopSizer = wx.BoxSizer(wx.VERTICAL)
        leftTopSizer.Add(cbOnlyFrontmost, 0, wx.BOTTOM, 5)
        leftTopSizer.Add(cbIncludeInvisible, 0, wx.BOTTOM, 5)
        leftTopSizer.Add(stopMacroCtrl_1, 0, wx.BOTTOM, 5)
        leftTopSizer.Add(stopMacroCtrl_2, 0, wx.BOTTOM, 5)

        topSizer = wx.BoxSizer(wx.HORIZONTAL)
        topSizer.Add(leftTopSizer, 1, wx.EXPAND)
        topSizer.Add(dragSizer)

        sizer1 = wx.GridBagSizer(vgap=4, hgap=4)
        sizer1.SetEmptyCellSize((0, 0))
        sizer1.Add(self.tree, (0, 0), (1, 5), wx.EXPAND)
        sizer1.Add(refreshButton, (1, 4), (2, 1), wx.ALIGN_TOP | wx.ALIGN_RIGHT)

        self.options = options = []

        def Wrapper(textCtrl, checkBox):
            def OnCheckBox(event):
                textCtrl.Enable(checkBox.GetValue())
                event.Skip()
            return OnCheckBox

        def MakeLine(line, checkBoxText, value):
            checkBox = wx.CheckBox(panel, -1, checkBoxText)
            sizer1.Add(checkBox, (line, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL)
            textCtrl = wx.TextCtrl(panel, -1, size=(20, -1))
            if value is not None:
                checkBox.SetValue(True)
                textCtrl.SetValue(value)
            else:
                textCtrl.Enable(False)
            sizer1.Add(textCtrl, (line, 1), (1, 3), wx.EXPAND)
            checkBox.Bind(wx.EVT_CHECKBOX, Wrapper(textCtrl, checkBox))
            options.append((checkBox, textCtrl))
            line += 1

        MakeLine(1, text.options[0], program)
        MakeLine(2, text.options[1], winName)
        MakeLine(3, text.options[2], winClass)
        MakeLine(4, text.options[3], childName)
        MakeLine(5, text.options[4], childClass)
        line = 6
        numMatchCB = wx.CheckBox(panel, -1, text.matchNum1)
        numMatchCB.SetValue(bool(matchNum))
        sizer1.Add(numMatchCB, (line, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL)
        numMatchCtrl = eg.SpinIntCtrl(panel, -1, matchNum or 1, 1)
        sizer1.Add(numMatchCtrl, (line, 1), (1, 1), wx.EXPAND)
        sizer1.Add(
            wx.StaticText(panel, -1, text.matchNum2),
            (line, 2),
            (1, 3),
            wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT
        )
        numMatchCB.Bind(wx.EVT_CHECKBOX, Wrapper(numMatchCtrl, numMatchCB))
        options.append((numMatchCB, numMatchCtrl))
        line += 1

        # the wait parameter
        waitCtrl = eg.SpinNumCtrl(panel)
        waitCtrl.SetValue(timeout)

        sizer1.Add(
            panel.StaticText(text.wait1),
            (line, 0),
            (1, 1),
            wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
        )
        sizer1.Add(
            waitCtrl,
            (line, 1),
            (1, 1),
            wx.ALIGN_CENTER_VERTICAL | wx.EXPAND
        )
        sizer1.Add(
            panel.StaticText(text.wait2),
            (line, 2),
            (1, 3),
            wx.ALIGN_CENTER_VERTICAL
        )
        line += 1
        sizer1.Add((1, 1), (line, 0))
        sizer1.AddGrowableCol(2, 100)
        sizer1.AddGrowableRow(0, 100)
        #sizer1.SetItemMinSize(options[0][1], 300, -1)

        # group the main lines together
        panel.sizer.AddMany([
            (topSizer, 0, wx.EXPAND),
            ((5, 5), ),
            (sizer1, 1, wx.EXPAND),
        ])

        # re-assign the test button
        def OnTestButton(dummyEvent):
            args = GetResult()[:-2]  # we don't need timeout and stopMacro
            hwnds = eg.WindowMatcher(*args)()
            dialog = TestDialog(panel.dialog, hwnds)
            dialog.ShowModal()
            dialog.Destroy()
        panel.dialog.buttonRow.testButton.Bind(wx.EVT_BUTTON, OnTestButton)

        @eg.LogIt
        def GetResult():
            resultList = []
            for checkBox, textCtrl in options:
                if not checkBox.IsChecked():
                    resultList.append(None)
                else:
                    resultList.append(textCtrl.GetValue())
            resultList.append(self.tree.includeInvisible)
            resultList.append(waitCtrl.GetValue())
            if stopMacroCtrl_1.IsChecked():
                resultList.append(STOP_IF_NOT_FOUND)
            elif stopMacroCtrl_2.IsChecked():
                resultList.append(STOP_IF_FOUND)
            else:
                resultList.append(STOP_NEVER)
            return resultList

        hwnds = eg.WindowMatcher(
            program,
            winName,
            winClass,
            childName,
            childClass,
            matchNum,
            includeInvisible,
        )()
        if matchNum is not None and len(hwnds):
            self.lastHwnd = hwnds[0]
            self.tree.SelectHwnd(self.lastHwnd)
        if searchOnlyFrontmost:
            cbOnlyFrontmost.SetValue(True)
            OnSearchOnlyFrontmostCheckbox(wx.CommandEvent())
        while True:
            self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelectionChanged)
            affirmed = panel.Affirmed()
            self.tree.Unbind(wx.EVT_TREE_SEL_CHANGED)
            if not affirmed:
                break
            if cbOnlyFrontmost.IsChecked():
                panel.SetResult(
                    None, None, None, None, None, None, None, None, None
                )
            else:
                panel.SetResult(*GetResult())

    def GetLabel(self, program, *args):
        if args[7] is None:
            return self.text.label2
        else:
            return self.text.label % basename(program or '')

    @eg.LogIt
    def OnFinderTool(self, dummyEvent=None):
        if Config.hideOnDrag:
            eg.document.frame.SetPosition(self.oldFramePosition)
            self.dialog.SetPosition(self.oldDialogPosition)
        lastTarget = self.finderTool.GetValue()
        if lastTarget is not None:
            self.tree.Unselect()
            self.tree.Refresh()
            self.tree.SelectHwnd(lastTarget)

    @eg.LogIt
    def OnFinderToolLeftClick(self, dummyEvent=None):
        self.oldFramePosition = eg.document.frame.GetPosition()
        self.oldDialogPosition = self.dialog.GetPosition()
        if Config.hideOnDrag:
            eg.document.frame.SetPosition((-32000, -32000))
            self.dialog.SetPosition((-32000, -32000))
        #event.Skip()

    @eg.LogIt
    def OnSelectionChanged(self, event):
        event.Skip()
        tree = self.tree
        item = tree.GetSelection()
        if not item.IsOk():
            return
        hwnd = tree.GetPyData(item)
        eg.PrintDebugNotice("HWND:", hwnd)
        if tree.GetItemParent(item) == tree.root:
            # only selected a program
            pid = hwnd
            hwnd = None
        else:
            pid = GetWindowThreadProcessId(hwnd)[1]

        if pid == self.lastPid and hwnd == self.lastHwnd:
            return
        self.lastPid = pid
        self.lastHwnd = hwnd
        exe = GetProcessName(pid)

        def SetOption(flag, option, value):
            checkBox, textCtrl = option
            checkBox.SetValue(flag)
            textCtrl.SetValue(value)
            textCtrl.Enable(flag)

        rootHwnd = None
        options = self.options
        SetOption(bool(exe), options[0], exe)
        if hwnd is not None:
            rootHwnd = GetAncestor(hwnd, GA_ROOT)
            targetWinName = GetWindowText(rootHwnd)
            targetWinClass = GetClassName(rootHwnd)
            SetOption(True, options[1], targetWinName)
            SetOption(True, options[2], targetWinClass)
        else:
            SetOption(False, options[1], "")
            SetOption(False, options[2], "")
        if rootHwnd is not None and rootHwnd != hwnd:
            targetWinName = GetWindowText(hwnd)
            targetWinClass = GetClassName(hwnd)
            SetOption(True, options[3], targetWinName)
            SetOption(True, options[4], targetWinClass)
            searchHwnd = hwnd
            data = [
                child
                for child in GetWindowChildsList(
                    rootHwnd,
                    tree.includeInvisible
                ) if (
                    GetClassName(child) == targetWinClass and
                    GetWindowText(child) == targetWinName
                )
            ]
            try:
                count = data.index(searchHwnd) + 1
            except:
                count = 0
        else:
            SetOption(False, options[3], "")
            SetOption(False, options[4], "")
            if rootHwnd is not None:
                data = [
                    hwnd
                    for hwnd in GetTopLevelWindowList(tree.includeInvisible)  # NOQA
                    if (
                        GetClassName(hwnd) == targetWinClass and
                        GetWindowText(hwnd) != targetWinName and
                        GetWindowThreadProcessId(hwnd)[1] == pid
                    )
                ]
                count = len(data)
            else:
                count = 0
        SetOption(count > 0, options[5], count or 1)


class TestDialog(eg.Dialog):
    def __init__(self, parent, hwnds):
        eg.Dialog.__init__(
            self,
            parent,
            title="Found Windows",
            size=(500, 350),
            style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
        )
        windowList = eg.WindowList(self, hwnds)
        okButton = wx.Button(self, wx.ID_OK)
        btnSizer = eg.HBoxSizer(
            ((0, 0), 1, wx.EXPAND),
            (okButton, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.EXPAND | wx.ALL, 5),
            ((0, 0), 1, wx.EXPAND),
            (eg.SizeGrip(self), 0, wx.ALIGN_BOTTOM | wx.ALIGN_RIGHT),
        )
        mainSizer = eg.VBoxSizer(
            (windowList, 1, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, 5),
            (btnSizer, 0, wx.EXPAND),
        )
        self.SetSizer(mainSizer)


def UseForegroundWindowOnly():
    """ Instruct EventGhost to use the active desktop window only, by
        clearing eg.lastFoundWindows
    """
    del eg.lastFoundWindows[:]
    return []