EventGhost/EventGhost

View on GitHub
plugins/EventGhost/ShowOSD.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-
#
# This file is part of 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 threading
from os import listdir
from os.path import abspath, dirname, join

import wx

import eg
from eg.WinApi.Dynamic import (
    CreateEvent,
    SetEvent,
    SetWindowPos,
    SWP_FRAMECHANGED,
    SWP_HIDEWINDOW,
    SWP_NOACTIVATE,
    SWP_NOOWNERZORDER,
    SWP_SHOWWINDOW,
)
from eg.WinApi.Utils import GetMonitorDimensions

HWND_FLAGS = SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED

SKIN_DIR = join(
    abspath(dirname(__file__.decode('mbcs'))),
    "OsdSkins"
)
SKINS = [name[:-3] for name in listdir(SKIN_DIR) if name.endswith(".py")]
SKINS.sort()

DEFAULT_FONT_INFO = wx.Font(
    18,
    wx.SWISS,
    wx.NORMAL,
    wx.BOLD
).GetNativeFontInfoDesc()


class ShowOSD(eg.ActionBase):
    name = "Show OSD"
    description = "Shows a simple On Screen Display."
    iconFile = "icons/ShowOSD"

    class text:
        label = "Show OSD: %s"
        editText = "Text to display:"
        osdFont = "Text Font:"
        osdColour = "Text Colour:"
        outlineFont = "Outline OSD"
        alignment = "Alignment:"
        alignmentChoices = [
            "Top Left",
            "Top Right",
            "Bottom Left",
            "Bottom Right",
            "Screen Center",
            "Bottom Center",
            "Top Center",
            "Left Center",
            "Right Center",
        ]
        display = "Show on display:"
        xOffset = "Horizontal offset X:"
        yOffset = "Vertical offset Y:"
        wait1 = "Autohide OSD after"
        wait2 = "seconds (0 = never)"
        skin = "Use skin"

    def __call__(
            self,
            osdText="",
            fontInfo=None,
            foregroundColour=(255, 255, 255),
            backgroundColour=(0, 0, 0),
            alignment=0,
            offset=(0, 0),
            displayNumber=0,
            timeout=3.0,
            skin=None
    ):
        if isinstance(skin, bool):
            skin = SKINS[0] if skin else None
        self.osdFrame.timer.cancel()
        osdText = eg.ParseString(osdText)
        event = CreateEvent(None, 0, 0, None)
        wx.CallAfter(
            self.osdFrame.ShowOSD,
            osdText,
            fontInfo,
            foregroundColour,
            backgroundColour,
            alignment,
            offset,
            displayNumber,
            timeout,
            event,
            skin
        )
        eg.actionThread.WaitOnEvent(event)

    def Configure(
            self,
            osdText="",
            fontInfo=None,
            foregroundColour=(255, 255, 255),
            backgroundColour=(0, 0, 0),
            alignment=0,
            offset=(0, 0),
            displayNumber=0,
            timeout=3.0,
            skin=None,
    ):
        if isinstance(skin, bool):
            skin = SKINS[0] if skin else None
        if fontInfo is None:
            fontInfo = DEFAULT_FONT_INFO
        panel = eg.ConfigPanel()
        text = self.text
        editTextCtrl = panel.TextCtrl("\n\n", style=wx.TE_MULTILINE)
        height = editTextCtrl.GetBestSize()[1]
        editTextCtrl.ChangeValue(osdText)
        editTextCtrl.SetMinSize((-1, height))
        alignmentChoice = panel.Choice(
            alignment, choices=text.alignmentChoices
        )
        displayChoice = eg.DisplayChoice(panel, displayNumber)
        xOffsetCtrl = panel.SpinIntCtrl(offset[0], -32000, 32000)
        yOffsetCtrl = panel.SpinIntCtrl(offset[1], -32000, 32000)
        timeCtrl = panel.SpinNumCtrl(timeout)

        fontButton = panel.FontSelectButton(fontInfo)
        foregroundColourButton = panel.ColourSelectButton(foregroundColour)

        if backgroundColour is None:
            tmpColour = (0, 0, 0)
        else:
            tmpColour = backgroundColour
        outlineCheckBox = panel.CheckBox(
            backgroundColour is not None, text.outlineFont
        )

        backgroundColourButton = panel.ColourSelectButton(tmpColour)
        backgroundColourButton.Enable(backgroundColour is not None)

        useSkin = skin is not None
        skinCtrl = panel.CheckBox(useSkin, text.skin)
        skinCtrl.SetValue(useSkin)
        skinChc = panel.Choice(SKINS.index(skin) if skin else 0, SKINS)
        skinChc.Enable(useSkin)

        sizer = wx.GridBagSizer(5, 5)
        expand = wx.EXPAND
        align = wx.ALIGN_CENTER_VERTICAL
        sizer.AddMany([
            (panel.StaticText(text.editText), (0, 0), (1, 1), align),
            (editTextCtrl, (0, 1), (1, 4), expand),
            (panel.StaticText(text.osdFont), (1, 3), (1, 1), align),
            (fontButton, (1, 4)),
            (panel.StaticText(text.osdColour), (2, 3), (1, 1), align),
            (foregroundColourButton, (2, 4)),
            (outlineCheckBox, (3, 3), (1, 1), expand),
            (backgroundColourButton, (3, 4)),
            (skinCtrl, (4, 3)),
            (skinChc, (4, 4), (1, 1), expand),
            (panel.StaticText(text.alignment), (1, 0), (1, 1), align),
            (alignmentChoice, (1, 1), (1, 1), expand),
            (panel.StaticText(text.display), (2, 0), (1, 1), align),
            (displayChoice, (2, 1), (1, 1), expand),
            (panel.StaticText(text.xOffset), (3, 0), (1, 1), align),
            (xOffsetCtrl, (3, 1), (1, 1), expand),
            (panel.StaticText(text.yOffset), (4, 0), (1, 1), align),
            (yOffsetCtrl, (4, 1), (1, 1), expand),
            (panel.StaticText(text.wait1), (5, 0), (1, 1), align),
            (timeCtrl, (5, 1), (1, 1), expand),
            (panel.StaticText(text.wait2), (5, 2), (1, 3), align),
        ])

        sizer.AddGrowableCol(2)
        panel.sizer.Add(sizer, 1, wx.EXPAND)

        def OnCheckBoxBGColour(event):
            backgroundColourButton.Enable(outlineCheckBox.IsChecked())
            event.Skip()
        outlineCheckBox.Bind(wx.EVT_CHECKBOX, OnCheckBoxBGColour)

        def OnCheckBoxSkin(event):
            skinChc.Enable(skinCtrl.IsChecked())
            event.Skip()
        skinCtrl.Bind(wx.EVT_CHECKBOX, OnCheckBoxSkin)

        while panel.Affirmed():
            if outlineCheckBox.IsChecked():
                outlineColour = backgroundColourButton.GetValue()
            else:
                outlineColour = None
            if skinCtrl.IsChecked():
                skin = skinChc.GetStringSelection()
            else:
                skin = None

            panel.SetResult(
                editTextCtrl.GetValue(),
                fontButton.GetValue(),
                foregroundColourButton.GetValue(),
                outlineColour,
                alignmentChoice.GetValue(),
                (xOffsetCtrl.GetValue(), yOffsetCtrl.GetValue()),
                displayChoice.GetValue(),
                timeCtrl.GetValue(),
                skin
            )

    def GetLabel(self, osdText, *dummyArgs):
        return self.text.label % osdText.replace("\n", r"\n")

    @classmethod
    def OnAddAction(cls):
        def MakeOSD():
            cls.osdFrame = OSDFrame(None)

            def CloseOSD():
                cls.osdFrame.timer.cancel()
                cls.osdFrame.Close()

            eg.app.onExitFuncs.append(CloseOSD)

        wx.CallAfter(MakeOSD)

    @eg.LogIt
    def OnClose(self):
        # self.osdFrame.timer.cancel()
        # wx.CallAfter(self.osdFrame.Close)
        self.osdFrame = None


class OSDFrame(wx.Frame):
    """
    A shaped frame to display the OSD.
    """

    @eg.LogIt
    def __init__(self, parent):
        wx.Frame.__init__(
            self,
            parent,
            -1,
            "OSD Window",
            size=(0, 0),
            style=(
                wx.FRAME_SHAPED |
                wx.NO_BORDER |
                wx.FRAME_NO_TASKBAR |
                wx.STAY_ON_TOP
            )
        )
        self.hwnd = self.GetHandle()
        self.bitmap = wx.EmptyBitmap(0, 0)
        # we need a timer to possibly cancel it
        self.timer = threading.Timer(0.0, eg.DummyFunc)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

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

    @staticmethod
    def GetSkinnedBitmap(
            textLines,
            textWidths,
            textHeights,
            textWidth,
            textHeight,
            memoryDC,
            textColour,
            skinName
    ):
        image = wx.Image(join(SKIN_DIR, skinName + ".png"))
        option = eg.Bunch()

        def Setup(minWidth, minHeight, xMargin, yMargin,
                  transparentColour=None):
            width = textWidth + 2 * xMargin
            if width < minWidth:
                width = minWidth
            height = textHeight + 2 * yMargin
            if height < minHeight:
                height = minHeight
            option.xMargin = xMargin
            option.yMargin = yMargin
            option.transparentColour = transparentColour
            bitmap = wx.EmptyBitmap(width, height)
            option.bitmap = bitmap
            memoryDC.SelectObject(bitmap)
            return width, height

        def Copy(x, y, width, height, toX, toY):
            bmp = wx.BitmapFromImage(image.GetSubImage((x, y, width, height)))
            memoryDC.DrawBitmap(bmp, toX, toY)

        def Scale(x, y, width, height, toX, toY, toWidth, toHeight):
            subImage = image.GetSubImage((x, y, width, height))
            subImage.Rescale(toWidth, toHeight, wx.IMAGE_QUALITY_HIGH)
            bmp = wx.BitmapFromImage(subImage)
            memoryDC.DrawBitmap(bmp, toX, toY)

        scriptGlobals = dict(Setup=Setup, Copy=Copy, Scale=Scale)
        eg.ExecFile(join(SKIN_DIR, skinName + ".py"), scriptGlobals)

        bitmap = option.bitmap
        memoryDC.SelectObject(wx.NullBitmap)
        bitmap.SetMask(wx.Mask(bitmap, option.transparentColour))
        memoryDC.SelectObject(bitmap)
        memoryDC.SetTextForeground(textColour)
        memoryDC.SetTextBackground(textColour)
        DrawTextLines(
            memoryDC, textLines, textHeights, option.xMargin, option.yMargin
        )
        memoryDC.SelectObject(wx.NullBitmap)
        return bitmap

    def OnClose(self, dummyEvent=None):
        # BUGFIX: Just hooking this event makes sure that nothing happens
        # when this OSD window is closed
        pass

    @eg.LogIt
    def OnPaint(self, dummyEvent=None):
        wx.BufferedPaintDC(self, self.bitmap)

    @eg.LogIt
    def OnTimeout(self):
        wx.CallAfter(
            SetWindowPos, self.hwnd, 0, 0, 0, 0, 0, HWND_FLAGS | SWP_HIDEWINDOW
        )

    @eg.LogIt
    def ShowOSD(
            self,
            osdText="",
            fontInfo=None,
            textColour=(255, 255, 255),
            outlineColour=(0, 0, 0),
            alignment=0,
            offset=(0, 0),
            displayNumber=0,
            timeout=3.0,
            event=None,
            skin=None,
    ):
        self.timer.cancel()
        if osdText.strip() == "":
            self.bitmap = wx.EmptyBitmap(0, 0)
            SetWindowPos(self.hwnd, 0, 0, 0, 0, 0, HWND_FLAGS | SWP_HIDEWINDOW)
            SetEvent(event)
            return

        # self.Freeze()
        memoryDC = wx.MemoryDC()

        # make sure the mask colour is not used by foreground or
        # background colour
        forbiddenColours = (textColour, outlineColour)
        maskColour = (255, 0, 255)
        if maskColour in forbiddenColours:
            maskColour = (0, 0, 2)
            if maskColour in forbiddenColours:
                maskColour = (0, 0, 3)
        maskBrush = wx.Brush(maskColour, wx.SOLID)
        memoryDC.SetBackground(maskBrush)

        if fontInfo is None:
            fontInfo = DEFAULT_FONT_INFO
        font = wx.FontFromNativeInfoString(fontInfo)
        memoryDC.SetFont(font)

        textLines = osdText.splitlines()
        sizes = [memoryDC.GetTextExtent(line or " ") for line in textLines]
        textWidths, textHeights = zip(*sizes)
        textWidth = max(textWidths)
        textHeight = sum(textHeights)

        if skin:
            bitmap = self.GetSkinnedBitmap(
                textLines,
                textWidths,
                textHeights,
                textWidth,
                textHeight,
                memoryDC,
                textColour,
                skin
            )
            width, height = bitmap.GetSize()
        elif outlineColour is None:
            width, height = textWidth, textHeight
            bitmap = wx.EmptyBitmap(width, height)
            memoryDC.SelectObject(bitmap)

            # fill the DC background with the maskColour
            memoryDC.Clear()

            # draw the text with the foreground colour
            memoryDC.SetTextForeground(textColour)
            DrawTextLines(memoryDC, textLines, textHeights)

            # mask the bitmap, so we can use it to get the needed
            # region of the window
            memoryDC.SelectObject(wx.NullBitmap)
            bitmap.SetMask(wx.Mask(bitmap, maskColour))

            # fill the anti-aliased pixels of the text with the foreground
            # colour, because the region of the window will add these
            # half filled pixels also. Otherwise we would get an ugly
            # border with mask-coloured pixels.
            memoryDC.SetBackground(wx.Brush(textColour, wx.SOLID))
            memoryDC.SelectObject(bitmap)
            memoryDC.Clear()
            memoryDC.SelectObject(wx.NullBitmap)
        else:
            width, height = textWidth + 5, textHeight + 5
            outlineBitmap = wx.EmptyBitmap(width, height, 1)
            outlineDC = wx.MemoryDC()
            outlineDC.SetFont(font)
            outlineDC.SelectObject(outlineBitmap)
            outlineDC.Clear()
            outlineDC.SetBackgroundMode(wx.SOLID)
            DrawTextLines(outlineDC, textLines, textHeights)
            outlineDC.SelectObject(wx.NullBitmap)
            outlineBitmap.SetMask(wx.Mask(outlineBitmap))
            outlineDC.SelectObject(outlineBitmap)

            bitmap = wx.EmptyBitmap(width, height)
            memoryDC.SetTextForeground(outlineColour)
            memoryDC.SelectObject(bitmap)
            memoryDC.Clear()

            Blit = memoryDC.Blit
            logicalFunc = wx.COPY
            for x in xrange(5):
                for y in xrange(5):
                    Blit(
                        x, y, width, height, outlineDC, 0, 0, logicalFunc, True
                    )
            outlineDC.SelectObject(wx.NullBitmap)
            memoryDC.SetTextForeground(textColour)
            DrawTextLines(memoryDC, textLines, textHeights, 2, 2)
            memoryDC.SelectObject(wx.NullBitmap)
            bitmap.SetMask(wx.Mask(bitmap, maskColour))

        region = wx.RegionFromBitmap(bitmap)
        self.SetShape(region)
        self.bitmap = bitmap
        monitorDimensions = GetMonitorDimensions()
        try:
            displayRect = monitorDimensions[displayNumber]
        except IndexError:
            displayRect = monitorDimensions[0]
        xOffset, yOffset = offset
        xFunc, yFunc = ALIGNMENT_FUNCS[alignment]
        x = displayRect.x + xFunc((displayRect.width - width), xOffset)
        y = displayRect.y + yFunc((displayRect.height - height), yOffset)
        deviceContext = wx.ClientDC(self)
        deviceContext.DrawBitmap(self.bitmap, 0, 0, False)
        SetWindowPos(
            self.hwnd, 0, x, y, width, height, HWND_FLAGS | SWP_SHOWWINDOW
        )

        if timeout > 0.0:
            self.timer = threading.Timer(timeout, self.OnTimeout)
            self.timer.start()
        eg.app.Yield(True)
        SetEvent(event)


def AlignLeft(width, offset):
    return offset


def AlignCenter(width, offset):
    return (width / 2) + offset


def AlignRight(width, offset):
    return width - offset


ALIGNMENT_FUNCS = (
    (AlignLeft, AlignLeft),  # Top Left
    (AlignRight, AlignLeft),  # Top Right
    (AlignLeft, AlignRight),  # Bottom Left
    (AlignRight, AlignRight),  # Bottom Right
    (AlignCenter, AlignCenter),  # Screen Center
    (AlignCenter, AlignRight),  # Bottom Center
    (AlignCenter, AlignLeft),  # Top Center
    (AlignLeft, AlignCenter),  # Left Center
    (AlignRight, AlignCenter),  # Right Center
)


def DrawTextLines(deviceContext, textLines, textHeights, xOffset=0, yOffset=0):
    for i, textLine in enumerate(textLines):
        deviceContext.DrawText(textLine, xOffset, yOffset)
        yOffset += textHeights[i]