talonvoice / talon

Issue Tracker for the main Talon app
85 stars 0 forks source link

[Feature Request] ability to dock imgui #233

Open knausj85 opened 3 years ago

knausj85 commented 3 years ago

I don't know if this would work on other OSs besides Windows, but I think it would be very useful down the line to be able to dock an imgui, something like OptiKey.

this has many potential use cases, from OptiKey-like functionality to context aware buttons exposed in the UI in a consistent location that doesn't block other things on screen.

lunixbochs commented 3 years ago

Can you go into more detail? I'm not familiar with optikey docking.

knausj85 commented 3 years ago

image like the start menu, it is able to reserve space on some side. window operations like maximizing and such respect the reserved space

lunixbochs commented 3 years ago

Any idea which api they use for that?

knausj85 commented 3 years ago

I'm afraid not, I will look into it when I have some downtime

knausj85 commented 3 years ago

an example of creating an application bar from c# at least

            //Register a new app bar with Windows - this adds it to a list of app bars
            var abd = new APPBARDATA();
            abd.cbSize = Marshal.SizeOf(abd);
            abd.hWnd = windowHandle;
            appBarCallBackId = PInvoke.RegisterWindowMessage("AppBarMessage"); //Get a system wide unique window message (id)
            abd.uCallbackMessage = appBarCallBackId;
            var result = PInvoke.SHAppBarMessage((int)AppBarMessages.New, ref abd);

from https://github.com/OptiKey/OptiKey/blob/eafb2147f524e37ae317593fcfb91feedb579f43/src/JuliusSweetland.OptiKey.Core/Services/WindowManipulationService.cs#L1599

updating an application bar:

 var barData = new APPBARDATA();
            barData.cbSize = Marshal.SizeOf(barData);
            barData.hWnd = windowHandle;
            barData.uEdge = dockPosition.ToAppBarEdge();
            barData.rc.Left = (int)Math.Round(sizeAndPosition.Left);
            barData.rc.Top = (int)Math.Round(sizeAndPosition.Top);
            barData.rc.Right = (int)Math.Round(sizeAndPosition.Right);
            barData.rc.Bottom = (int)Math.Round(sizeAndPosition.Bottom);

            //Submit a query for the proposed dock size and position, which might be updated
            PInvoke.SHAppBarMessage(AppBarMessages.QueryPos, ref barData);

            ...
            //Then set the dock size and position, using the potentially updated barData
            PInvoke.SHAppBarMessage(AppBarMessages.SetPos, ref barData);

from https://github.com/OptiKey/OptiKey/blob/eafb2147f524e37ae317593fcfb91feedb579f43/src/JuliusSweetland.OptiKey.Core/Services/WindowManipulationService.cs#L1664

knausj85 commented 3 years ago

I haven't tried this yet, but this looks promising for Python https://stackoverflow.com/questions/60567141/how-make-application-desktop-toolbar-ui-with-pyqt5

knausj85 commented 3 years ago

here is a working example; requires pywin32, PyQt5 for the moment

"""
Registering an Application Desktop Toolbar in Windows for PyQt5 window.

Taken from https://gist.github.com/swdevbali/495f902162446b30cd567b2a44d86d79 and 
           https://github.com/sabren/ceomatic/blob/master/wxappbars.py, 
thanks for swdevbali and sabren
Adapted by Elendiar.
"""
import ctypes, sys
from ctypes import wintypes
from ctypes import *
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QDesktopWidget, QWidget
from PyQt5.QtCore import *
from copy import deepcopy

shell32 = windll.shell32
user32 = windll.user32
import win32api, win32gui
import win32.lib.win32con as win32con

class APPBARDATA(Structure):
    _fields_ = [
        ("cbSize", wintypes.DWORD),
        ("hWnd", wintypes.HWND),
        ("uCallbackMessage", ctypes.c_ulong),
        ("uEdge", c_ulong),
        ("rc", wintypes.RECT),
        ("lParam", wintypes.LPARAM),
    ]

PAPPBARDATA = POINTER(APPBARDATA)

class ABEdge:
    Left = 0
    Top = 1
    Right = 2
    Bottom = 3
    Float = 4

class ABMsg:
    ABM_NEW = 0
    ABM_REMOVE = 1
    ABM_QUERYPOS = 2
    ABM_SETPOS = 3
    ABM_GETSTATE = 4
    ABM_GETTASKBARPOS = 5
    ABM_ACTIVATE = 6
    ABM_GETAUTOHIDEBAR = 7
    ABM_SETAUTOHIDEBAR = 8
    ABM_WINDOWPOSCHANGED = 9
    ABM_SETSTATE = 10

class ABNotify:
    ABN_STATECHANGE = 0
    ABN_POSCHANGED = 1
    ABN_FULLSCREENAPP = 2
    ABN_WINDOWARRANGE = 3

class RegisterInfo(object):
    def __init__(self):
        self._window = None

        self.callbackId = 0
        self.isRegistered = False
        self.edge = ABEdge.Float
        self.originalStyle = None
        self.originalPosition = None
        self.originalSize = None
        self.originalResizeMode = None

    @property
    def window(self):
        return self._window

    @window.setter
    def window(self, window):
        self._window = window
        self._hWnd = window.winId().__int__()
        self._oldWndProc = win32gui.SetWindowLong(
            self._hWnd, win32con.GWL_WNDPROC, self.WndProc
        )

    # http://wiki.wxpython.org/HookingTheWndProc
    def WndProc(self, hWnd, msg, wParam, lParam):
        if msg == win32con.WM_DESTROY:
            self._restoreOldWndProc()
        elif msg == self.callbackId:
            if wParam == ABNotify.ABN_POSCHANGED:
                _ABSetPos(self.edge, self.window)
        else:
            return win32gui.CallWindowProc(self._oldWndProc, hWnd, msg, wParam, lParam)

    def _restoreOldWndProc(self):
        win32api.SetWindowLong(self._hWnd, win32con.GWL_WNDPROC, self._oldWndProc)

_registeredWindowInfo = {}

def _GetRegisterInfo(appbarWindow):
    geometry = appbarWindow.geometry()
    reg = RegisterInfo()
    reg.callBackId = 0
    reg.window = appbarWindow
    reg.isRegistered = False
    reg.edge = ABEdge.Top
    reg.originalStyle = appbarWindow.windowFlags()
    reg.originalPosition = QPoint(geometry.x(), geometry.y())
    reg.originalSize = QSize(geometry.width(), geometry.height())
    _registeredWindowInfo[appbarWindow] = reg
    return _registeredWindowInfo[appbarWindow]

def _RestoreWindow(appbarWindow):
    info = _GetRegisterInfo(appbarWindow)
    appbarWindow.setWindowFlags(info.originalStyle)
    appbarWindow.move(info.originalPosition)
    appbarWindow.resize(info.originalSize)

def SetAppBar(appbarWindow, edge, barSize):

    info = _GetRegisterInfo(appbarWindow)
    info.edge = edge
    abd = APPBARDATA()
    abd.cbSize = wintypes.DWORD(sizeof(abd))
    abd.hWnd = wintypes.HWND(appbarWindow.winId().__int__())

    if (edge == ABEdge.Float) and info.isRegistered:
        shell32.SHAppBarMessage(ABMsg.ABM_REMOVE, PAPPBARDATA(abd))
        info.isRegistered = False
        _RestoreWindow(appbarWindow)

    elif not info.isRegistered:
        info.isRegistered = True
        info.callbackId = win32api.RegisterWindowMessage("AppBarMessage")
        shell32.SHAppBarMessage(ABMsg.ABM_NEW, PAPPBARDATA(abd))

    appbarWindow.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
    _ABSetPos(info.edge, appbarWindow, barSize)

def RemoveAppBar(appbarWindow):
    print("REMOVING")
    info = _GetRegisterInfo(appbarWindow)
    abd = APPBARDATA()
    shell32.SHAppBarMessage(ABMsg.ABM_REMOVE, PAPPBARDATA(abd))
    info.isRegistered = False
    # _RestoreWindow(appbarWindow)

def _DoResize(appBarWindow, rect):
    appBarWindow.resize(rect.Width, rect.Height)
    appBarWindow.move(rect.Left, rect.Top)

def _ABSetPos(edge, appbarWindow, barSize):
    barData = APPBARDATA()
    barData.cbSize = wintypes.DWORD(sizeof(barData))
    barData.hWnd = appbarWindow.winId().__int__()
    barData.uEdge = edge
    screen = QDesktopWidget().screenGeometry()

    deskW = screen.width()
    deskH = screen.height()

    if barData.uEdge == ABEdge.Left or barData.uEdge == ABEdge.Right:
        winW = barSize
        winH = deskH
        barData.rc.top = 0
        barData.rc.bottom = deskH
        if barData.uEdge == ABEdge.Left:
            barData.rc.left = 0
            barData.rc.right = winW
        else:
            barData.rc.right = deskW
            barData.rc.left = deskW - winW
    else:
        winW = deskW
        winH = barSize
        barData.rc.left = 0
        barData.rc.right = deskW
        if barData.uEdge == ABEdge.Top:
            barData.rc.top = 0
            barData.rc.bottom = winH
        else:
            barData.rc.bottom = deskH
            barData.rc.top = deskH - winH

    oldLeft = deepcopy(barData.rc.left)  # deepcopy because after reopen x from 0
    # become w -> 222 and window isnt visible.
    shell32.SHAppBarMessage(ABMsg.ABM_QUERYPOS, PAPPBARDATA(barData))
    shell32.SHAppBarMessage(ABMsg.ABM_SETPOS, PAPPBARDATA(barData))
    barData.rc.left = deepcopy(oldLeft)

    def _resize():
        x = barData.rc.left
        y = barData.rc.top
        w = barData.rc.right - barData.rc.left
        h = barData.rc.bottom - barData.rc.top
        appbarWindow.resize(w, h)
        appbarWindow.move(x, y)

    # This is done async, because windows will send a resize after a new appbar is added.
    # if we size right away, the windows resize comes last and overrides us.
    QTimer.singleShot(300, _resize)

if __name__ == "__main__":
    # DPI scaling
    # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    # QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    app = QtWidgets.QApplication(sys.argv)
    win = QWidget()
    win.setWindowFlags(
        Qt.FramelessWindowHint
        | Qt.WindowStaysOnTopHint
        | Qt.CustomizeWindowHint
        | Qt.Tool
    )
    btn = QtWidgets.QPushButton("Click-to-close", win)
    btn.clicked.connect(QtWidgets.qApp.quit)
    barSize = 222
    SetAppBar(win, ABEdge.Left, barSize)
    win.show()
    sys.exit(app.exec_())
knausj85 commented 3 years ago

sorry for the spam, there's some good documentation here https://docs.microsoft.com/en-us/windows/win32/shell/application-desktop-toolbars