Fix Mac launcher comments to work when uncommented.
[gpodder.git] / share / gpodder / extensions / notification-win32.py
blob52703874a5feb681ab992aff67d2c9c92f4323c9
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2018 The gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # Notification implementation for Windows
21 # Sean Munkel; 2012-12-29
22 """
23 Current state (2018/07/29 ELL):
24 - I can't get pywin32 to work in msys2 (the platform used for this python3/gtk3 installer)
25 so existing code using COM doesn't work.
26 - Gio.Notification is not implemented on windows yet.
27 see https://bugzilla.gnome.org/show_bug.cgi?id=776583
28 - Gtk.StatusIcon with a context works but is deprecated. Showing a balloon using set_tooltip_markup
29 doesn't work.
30 See https://github.com/afiskon/py-gtk-example
31 - hexchat have implemented a solid c++ solution.
32 See https://github.com/hexchat/hexchat/tree/master/src/fe-gtk/notifications
33 I've chosen to implement notifications by calling a PowerShell script invoking
34 Windows Toast Notification API or Balloon Notification as fallback.
35 It's tested on Win7 32bit and Win10 64bit VMs from modern.ie
36 So we have a working solution until Gio.Notification is implemented on Windows.
37 """
38 import logging
39 import os
40 import os.path
41 import subprocess
42 import sys
43 import tempfile
45 import gpodder
47 logger = logging.getLogger(__name__)
48 _ = gpodder.gettext
50 __title__ = _('Notification Bubbles for Windows')
51 __description__ = _('Display notification bubbles for different events.')
52 __authors__ = 'Sean Munkel <SeanMunkel@gmail.com>'
53 __category__ = 'desktop-integration'
54 __mandatory_in__ = 'win32'
55 __only_for__ = 'win32'
58 class gPodderExtension(object):
59 def __init__(self, *args):
60 gpodder_script = sys.argv[0]
61 gpodder_script = os.path.realpath(gpodder_script)
62 self._icon = os.path.join(os.path.dirname(gpodder_script), "gpodder.ico")
64 def on_notification_show(self, title, message):
65 script = """
66 try {{
67 if ([Environment]::OSVersion.Version -ge (new-object 'Version' 10,0,10240)) {{
68 # use Windows 10 Toast notification
69 [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
70 [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
71 [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
72 # Need a real AppID (see https://stackoverflow.com/q/46814858)
73 # use gPodder app id if it's the installed, otherwise use PowerShell's AppID
74 try {{
75 $gpo_appid = Get-StartApps -Name "gpodder"
76 }} catch {{
77 write-host "Get-StartApps not available"
78 $gpo_appid = $null
80 if ($gpo_appid -ne $null) {{
81 $APP_ID = $gpo_appid[0].AppID
82 }} else {{
83 $APP_ID = '{{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}}\\WindowsPowerShell\\v1.0\\powershell.exe'
85 $template = @"
86 <toast activationType="protocol" launch="" duration="long">
87 <visual>
88 <binding template="ToastGeneric">
89 <image placement="appLogoOverride" src="{icon}" />
90 <text><![CDATA[{title}]]></text>
91 <text><![CDATA[{message}]]></text>
92 </binding>
93 </visual>
94 <audio silent="true" />
95 </toast>
97 $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
98 $xml.LoadXml($template)
99 $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
100 [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
101 Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
102 }} else {{
103 # use older Balloon notification when not on Windows 10
104 [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
105 $o = New-Object System.Windows.Forms.NotifyIcon
107 $o.Icon = "{icon}"
108 $o.BalloonTipIcon = "None"
109 $o.BalloonTipText = @"
110 {message}
112 $o.BalloonTipTitle = @"
113 {title}
116 $o.Visible = $True
117 $Delay = 10 # Delay value in seconds.
118 $o.ShowBalloonTip($Delay*1000)
119 Start-Sleep -s $Delay
120 $o.Dispose()
121 Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
123 }} catch {{
124 write-host "Caught an exception:"
125 write-host "Exception Type: $($_.Exception.GetType().FullName)"
126 write-host "Exception Message: $($_.Exception.Message)"
127 exit 1
129 """.format(icon=self._icon, message=message, title=title)
130 fh, path = tempfile.mkstemp(suffix=".ps1")
131 with open(fh, "w", encoding="utf_8_sig") as f:
132 f.write(script)
133 try:
134 # hide powershell command window using startupinfo
135 startupinfo = subprocess.STARTUPINFO()
136 startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
137 startupinfo.wShowWindow = subprocess.SW_HIDE
138 # to run 64bit powershell on Win10 64bit when running from 32bit gPodder
139 # (we need 64bit powershell on Win10 otherwise Get-StartApps is not available)
140 powershell = r"{}\sysnative\WindowsPowerShell\v1.0\powershell.exe".format(os.environ["SystemRoot"])
141 if not os.path.exists(powershell):
142 powershell = "powershell.exe"
143 subprocess.Popen([powershell,
144 "-ExecutionPolicy", "Bypass", "-File", path],
145 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
146 startupinfo=startupinfo)
147 except subprocess.CalledProcessError as e:
148 logger.error("Error in on_notification_show(title=%r, message=%r):\n"
149 "\t%r exit code %i\n\tstdout=%s\n\tstderr=%s",
150 title, message, e.cmd, e.returncode, e.stdout, e.stderr)
151 except FileNotFoundError:
152 logger.error("Error in on_notification_show(title=%r, message=%r): %s not found",
153 title, message, powershell)
155 def on_unload(self):
156 pass