Kalmat / PyWinCtl

Cross-Platform module to get info on and control windows on screen
Other
191 stars 20 forks source link

Create a function to return if a window is flashing or not #35

Closed Helcity closed 1 year ago

Helcity commented 1 year ago

It would be very nice if there was a function or method with the following functionality:

IsWindowFlashing(WinName): If (WinIsFlashing): return True Else: Return False

My objective is to check if a particular window is flashing on the taskbar, asking for the user attention and focus. Any similar method that scans all windows with this functionality and returns the names of windows that satisfy such criteria would be welcomed too.

Is it possible to create such a thing?

Kalmat commented 1 year ago

Hi! Thank you for your interest!

Right now, I'm not sure. Let me check if I am able to find anything about it (I guess you mean on MS-Windows, right?)

MestreLion commented 1 year ago

On Linux this might be inferred from _NET_WM_STATE_DEMANDS_ATTENTION

By the way... I'm having a hard time getting a window to get from Minimized/whatever to Focused / Active, using Ubuntu's Unity DE (Compiz). All related methods I try simply set this property, but it does not bring the window to front

Helcity commented 1 year ago

Yeah, MS-Windows, Win10 would be enough

Kalmat commented 1 year ago

Hi again!

I've been googling around and I found that it's not possible to do that using Python: https://stackoverflow.com/questions/67048076/wh-shell-using-python-winapi-setwindowshookexa https://stackoverflow.com/questions/964564/how-to-add-a-system-windows-hook-so-as-to-be-notified-of-windows-being-created/964643#964643

The solution seems to be to hook to specific general events (WH_SHELL), but this can only be done on C++ from a separate (and unmanaged) DLL. Unfortunately, I'm totally unskilled in C...

Nevertheless I am thinking on a workaround, based on analyzing the color of the icons in the taskbar. This looks feasible, but I need to find the way to identify which icon belongs to which app. pywinauto looks promissing, but still not sure if it will work or not. I will keep on investigating and will let you know any progress.

Best regards!

Kalmat commented 1 year ago

Hi again!

Though still very experimental, hackish and tricky, I think I managed to get it working, at least on my system, that's the reason why I'd need you to try this on your own system before keeping investigating. Could you please try this? (you may need to install some modules on your system, please let me know if you need help)

import pywinctl as pwc
import pywinauto
from pywinauto import Application
import win32api
import win32gui
import win32con
import win32process

def isFlashing(name) -> bool:

    def _getFileDescription(handle):
        # https://stackoverflow.com/questions/31118877/get-application-name-from-exe-file-in-python

        _, pid = win32process.GetWindowThreadProcessId(handle)
        hProc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ, 0, pid)
        exeName = win32process.GetModuleFileNameEx(hProc, 0)

        try:
            language, codepage = win32api.GetFileVersionInfo(exeName, '\\VarFileInfo\\Translation')[0]
            stringFileInfo = u'\\StringFileInfo\\%04X%04X\\%s' % (language, codepage, "FileDescription")
            description = win32api.GetFileVersionInfo(exeName, stringFileInfo)
        except:
            description = "unknown"

        return description

    def _find_taskbar_icon(hWnd):

        exStyle = win32api.GetWindowLong(hWnd, win32con.GWL_EXSTYLE)
        owner = win32gui.GetWindow(hWnd, win32con.GW_OWNER)
        if exStyle & win32con.WS_EX_APPWINDOW != 0 or owner != 0:
            return None

        name = _getFileDescription(hWnd)

        try:
            app: pywinauto.Application = Application(backend="uia").connect(path="explorer.exe")
            sysTray: pywinauto.WindowSpecification = app.window(class_name="Shell_TrayWnd")
            w: pywinauto.WindowSpecification = sysTray.child_window(title_re=name, found_index=0)
            rect = w.rectangle()
        except:
            rect = None
        return rect

    w = pwc.getWindowsWithTitle(name, condition=pwc.Re.CONTAINS)
    hWnd = w[0].getHandle()

    iconRect = _find_taskbar_icon(hWnd)
    if iconRect:

        xPos = iconRect.left + int((iconRect.right - iconRect.left) / 2)
        color = 0
        desktop = win32gui.GetDesktopWindow()
        dc = win32gui.GetWindowDC(desktop)
        for i in range(50, 54):
            color += win32gui.GetPixel(dc, xPos, iconRect.top + i)
        win32gui.ReleaseDC(desktop, dc)
        flashColor = 10787327  # This value is totally empirical. Find a way to retrieve it!!!!
        if color / 4 == flashColor:
            return True
        else:
            return False
    else:
        return False

print(isFlashing("Spotify"))  # change this name for the one you'd like to target

Don't forget to change "Spotify" for whatever application you want to check!!!

Captura de pantalla 2023-01-18 154610 Captura de pantalla 2023-01-18 154640

MestreLion commented 1 year ago

I believe the end goal of this will be a Window(...).isFlashing() instance method, and the name argument is just a convenience for your tests/research and not an actual argument of the end method, correct?

A few comments:

Kalmat commented 1 year ago

I believe the end goal of this will be a Window(...).isFlashing() instance method, and the name argument is just a convenience for your tests/research and not an actual argument of the end method, correct?

Correct!

  • In both Windows and most Linux DEs, "flashing" is a 2-stage status: a brief one where the taskbar icon actually "flashes" (in Windows) or "wiggle"/"vibrate" (Unity) for a few seconds, and then a more permanent status where it remains "highlighted" in some way (orange background in Windows, blue marker in Unity, etc), usually not flashing or animating, until the user intervenes and make the window active.

Just for information. In Windows, it's not possible without a C++ DLL, which is far beyond my knowledge. This is why I am trying this "ugly" workaround. In Linux I have not investigated yet, I think that the state you were pointing (_NET_WM_STATE_DEMANDS_ATTENTION) is very promissing! Finally, it's quite probable that this will not be feasible in macOS, for which I use AppleScript that has its... limitations (we will see).

  • About the naming: in Windows world, it is called flashing (although it only actually flashes for a brief time, and then remain statically "colored"). In macOS, it bounces. In X, it is demanding attention. While looking for cross-platform references, Qt calls the action to set this alert() (and there's no associated status).

Didn't think much about a cross-platform name, to be honest. isAlerting may be a good candidate, or requiresUserAction, or maybe isFlashing, isBouncing and isDemandingAttention (all three as alias).

MestreLion commented 1 year ago

In Linux I have not investigated yet, I think that the state you were pointing (_NET_WM_STATE_DEMANDS_ATTENTION) is very promissing!

Not only promising, but trivial:

def isAlerting(self):
    return STATE_ATTENTION in EWMH.getWmState(self._hWnd, str=True)

Didn't think much about a cross-platform name, to be honest. isAlerting may be a good candidate, or requiresUserAction, or maybe isFlashing, isBouncing and isDemandingAttention (all three as alias).

I like your suggestion of isAlerting, looks quite platform neutral, is similar to what Qt chose (always good to back up a decision with a reference), and most importantly it does refer to both the initial, transient animation and the permanent state

Kalmat commented 1 year ago

Hi @Helcity! sorry to bother you, I'd just like to know if you had the time to test the code I was attaching in my previous comment. Please, let me know any progress you make or if you need help or similar.

Thank you!