2e0cc3d41debb9351674cbb9551e2cdaa63f3a87
1import sys
2sys.coinit_flags= 0
3import win32com.client
4import pywintypes
5import os
6import shutil
7import http_server_39 as server
8#import http.server as server
9import socketserver
10import threading
11import asyncio
12import websockets
13import logging, json
14import urllib
15import posixpath
16import time
17import pythoncom
18import pystray
19import tkinter as tk
20from tkinter import ttk
21from PIL import Image, ImageDraw
22
23logging.basicConfig()
24
25global STATE
26global STATE_DEFAULT
27global current_slideshow
28current_slideshow = None
29CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''
30CACHE_TIMEOUT = 2*60*60
31
32class Handler(server.SimpleHTTPRequestHandler):
33 def __init__(self, *args, **kwargs):
34 super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))
35
36 def translate_path(self, path):
37 """Translate a /-separated PATH to the local filename syntax.
38
39 Components that mean special things to the local file system
40 (e.g. drive or directory names) are ignored. (XXX They should
41 probably be diagnosed.)
42
43 """
44 # abandon query parameters
45 path = path.split('?',1)[0]
46 path = path.split('#',1)[0]
47 # Don't forget explicit trailing slash when normalizing. Issue17324
48 trailing_slash = path.rstrip().endswith('/')
49 try:
50 path = urllib.parse.unquote(path, errors='surrogatepass')
51 except UnicodeDecodeError:
52 path = urllib.parse.unquote(path)
53 path = posixpath.normpath(path)
54 words = path.split('/')
55 words = list(filter(None, words))
56 if len(words) > 0 and words[0] == "cache":
57 if current_slideshow:
58 path = CACHEDIR + "\\" + current_slideshow.name()
59 else:
60 path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "black.jpg") + '/'
61 return path
62 words.pop(0)
63 else:
64 path = self.directory
65 for word in words:
66 if os.path.dirname(word) or word in (os.curdir, os.pardir):
67 # Ignore components that are not a simple file/directory name
68 continue
69 path = os.path.join(path, word)
70 if trailing_slash:
71 path += '/'
72 return path
73
74
75def run_http():
76 http_server = server.HTTPServer(("", 80), Handler)
77 http_server.serve_forever()
78
79STATE_DEFAULT = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""}
80STATE = STATE_DEFAULT
81USERS = set()
82
83
84def state_event():
85 print("Running state event")
86 return json.dumps({"type": "state", **STATE})
87
88
89def users_event():
90 return json.dumps({"type": "users", "count": len(USERS)})
91
92
93async def notify_state():
94 print("Notifying state to " + str(len(USERS)) + " users")
95 global STATE
96 if current_slideshow and STATE["connected"] == 1:
97 STATE["current"] = current_slideshow.current_slide()
98 STATE["total"] = current_slideshow.total_slides()
99 STATE["visible"] = current_slideshow.visible()
100 STATE["name"] = current_slideshow.name()
101 else:
102 STATE = STATE_DEFAULT
103 if USERS: # asyncio.wait doesn't accept an empty list
104 message = state_event()
105 await asyncio.wait([user.send(message) for user in USERS])
106
107
108async def notify_users():
109 if USERS: # asyncio.wait doesn't accept an empty list
110 message = users_event()
111 await asyncio.wait([user.send(message) for user in USERS])
112
113
114async def register(websocket):
115 USERS.add(websocket)
116 await notify_users()
117
118
119async def unregister(websocket):
120 USERS.remove(websocket)
121 await notify_users()
122
123
124async def ws_handle(websocket, path):
125 print("Received command")
126 global current_slideshow
127 # register(websocket) sends user_event() to websocket
128 await register(websocket)
129 try:
130 await websocket.send(state_event())
131 async for message in websocket:
132 data = json.loads(message)
133 if data["action"] == "prev":
134 if current_slideshow:
135 current_slideshow.prev()
136 await notify_state()
137 elif data["action"] == "next":
138 if current_slideshow:
139 current_slideshow.next()
140 await notify_state()
141 elif data["action"] == "first":
142 if current_slideshow:
143 current_slideshow.first()
144 await notify_state()
145 elif data["action"] == "last":
146 if current_slideshow:
147 current_slideshow.last()
148 await notify_state()
149 elif data["action"] == "black":
150 if current_slideshow:
151 if current_slideshow.visible() == 3:
152 current_slideshow.normal()
153 else:
154 current_slideshow.black()
155 await notify_state()
156 elif data["action"] == "white":
157 if current_slideshow:
158 if current_slideshow.visible() == 4:
159 current_slideshow.normal()
160 else:
161 current_slideshow.white()
162 await notify_state()
163 elif data["action"] == "goto":
164 if current_slideshow:
165 current_slideshow.goto(int(data["value"]))
166 await notify_state()
167 elif data["action"] == "refresh":
168 print("Received refresh command")
169 await notify_state()
170 if current_slideshow:
171 current_slideshow.export_current_next()
172 current_slideshow.refresh()
173 else:
174 logging.error("unsupported event: {}", data)
175 finally:
176 await unregister(websocket)
177
178def run_ws():
179 # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084
180 # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/
181 pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
182 asyncio.set_event_loop(asyncio.new_event_loop())
183 start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None)
184 asyncio.get_event_loop().run_until_complete(start_server)
185 asyncio.get_event_loop().run_forever()
186
187def start_server():
188 #STATE["current"] = current_slide()
189 http_daemon = threading.Thread(name="http_daemon", target=run_http)
190 http_daemon.setDaemon(True)
191 http_daemon.start()
192 print("Started HTTP server")
193
194 #run_ws()
195
196 ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
197 ws_daemon.setDaemon(True)
198 ws_daemon.start()
199 print("Started websocket server")
200
201 #try:
202 # ws_daemon.start()
203 # http_daemon.start()
204 #except (KeyboardInterrupt, SystemExit):
205 # cleanup_stop_thread()
206 # sys.exit()
207
208class Slideshow:
209 def __init__(self, instance):
210 self.instance = instance
211 if self.instance is None:
212 raise ValueError("PPT instance cannot be None")
213
214 if self.instance.SlideShowWindows.Count == 0:
215 raise ValueError("PPT instance has no slideshow windows")
216 self.view = self.instance.SlideShowWindows[0].View
217
218 if self.instance.ActivePresentation is None:
219 raise ValueError("PPT instance has no active presentation")
220 self.presentation = self.instance.ActivePresentation
221
222 def unload(self):
223 connect_ppt()
224
225 def refresh(self):
226 try:
227 if self.instance is None:
228 raise ValueError("PPT instance cannot be None")
229
230 if self.instance.SlideShowWindows.Count == 0:
231 raise ValueError("PPT instance has no slideshow windows")
232 self.view = self.instance.SlideShowWindows[0].View
233
234 if self.instance.ActivePresentation is None:
235 raise ValueError("PPT instance has no active presentation")
236 except:
237 self.unload()
238
239 def total_slides(self):
240 try:
241 self.refresh()
242 return len(self.presentation.Slides)
243 except ValueError or pywintypes.com_error:
244 self.unload()
245
246 def current_slide(self):
247 try:
248 self.refresh()
249 return self.view.CurrentShowPosition
250 except ValueError or pywintypes.com_error:
251 self.unload()
252
253 def visible(self):
254 try:
255 self.refresh()
256 return self.view.State
257 except ValueError or pywintypes.com_error:
258 self.unload()
259
260 def prev(self):
261 try:
262 self.refresh()
263 self.view.Previous()
264 self.export_current_next()
265 except ValueError or pywintypes.com_error:
266 self.unload()
267
268 def next(self):
269 try:
270 self.refresh()
271 self.view.Next()
272 self.export_current_next()
273 except ValueError or pywintypes.com_error:
274 self.unload()
275
276 def first(self):
277 try:
278 self.refresh()
279 self.view.First()
280 self.export_current_next()
281 except ValueError or pywintypes.com_error:
282 self.unload()
283
284 def last(self):
285 try:
286 self.refresh()
287 self.view.Last()
288 self.export_current_next()
289 except ValueError or pywintypes.com_error:
290 self.unload()
291
292 def goto(self, slide):
293 try:
294 self.refresh()
295 if slide <= self.total_slides():
296 self.view.GotoSlide(slide)
297 else:
298 self.last()
299 self.next()
300 self.export_current_next()
301 except ValueError or pywintypes.com_error:
302 self.unload()
303
304 def black(self):
305 try:
306 self.refresh()
307 self.view.State = 3
308 self.export_current_next()
309 except ValueError or pywintypes.com_error:
310 self.unload()
311
312 def white(self):
313 try:
314 self.refresh()
315 self.view.State = 4
316 self.export_current_next()
317 except ValueError or pywintypes.com_error:
318 self.unload()
319
320 def normal(self):
321 try:
322 self.refresh()
323 self.view.State = 1
324 self.export_current_next()
325 except ValueError or pywintypes.com_error:
326 self.unload()
327
328 def name(self):
329 try:
330 self.refresh()
331 return self.presentation.Name
332 except ValueError or pywintypes.com_error:
333 self.unload()
334
335
336 def export_current_next(self):
337 self.export(self.current_slide())
338 self.export(self.current_slide() + 1)
339
340 def export(self, slide):
341 destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"
342 os.makedirs(os.path.dirname(destination), exist_ok=True)
343 if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:
344 if slide <= self.total_slides():
345 attempts = 0
346 while attempts < 3:
347 try:
348 self.presentation.Slides(slide).Export(destination, "JPG")
349 break
350 except:
351 pass
352 attempts += 1
353 elif slide == self.total_slides() + 1:
354 shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))
355 else:
356 pass
357
358 def export_all(self):
359 for i in range(1, self.total_slides()):
360 self.export(i)
361
362def get_ppt_instance():
363 instance = win32com.client.Dispatch('Powerpoint.Application')
364 if instance is None or instance.SlideShowWindows.Count == 0:
365 return None
366 return instance
367
368def get_current_slideshow():
369 return current_slideshow
370
371def connect_ppt():
372 global STATE
373 if STATE["connected"] == 1:
374 print("Disconnected from PowerPoint instance")
375 STATE = STATE_DEFAULT
376 while True:
377 try:
378 instance = get_ppt_instance()
379 global current_slideshow
380 current_slideshow = Slideshow(instance)
381 STATE["connected"] = 1
382 STATE["current"] = current_slideshow.current_slide()
383 STATE["total"] = current_slideshow.total_slides()
384 print("Connected to PowerPoint instance")
385 current_slideshow.export_all()
386 break
387 except ValueError as e:
388 current_slideshow = None
389 pass
390 time.sleep(1)
391
392def start(_=None):
393 #root = tk.Tk()
394 #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png"))
395 #root.geometry("250x150+300+300")
396 #app = Interface(root)
397 #interface_thread = threading.Thread(target=root.mainloop())
398 #interface_thread.setDaemon(True)
399 #interface_thread.start()
400 start_server()
401 connect_ppt()
402
403
404def null_action():
405 pass
406
407class Interface(ttk.Frame):
408
409 def __init__(self, parent):
410 ttk.Frame.__init__(self, parent)
411
412 self.parent = parent
413
414 self.initUI()
415
416 def initUI(self):
417
418 self.parent.title("ppt-control")
419 self.style = ttk.Style()
420 #self.style.theme_use("default")
421
422 self.pack(fill=tk.BOTH, expand=1)
423
424 quitButton = ttk.Button(self, text="Close",
425 command=self.quit)
426 quitButton.place(x=50, y=50)
427 status_label = ttk.Label(self, text="PowerPoint status: not detected")
428 status_label.place(x=10,y=10)
429
430
431
432def show_icon():
433 menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False),
434 pystray.MenuItem("Restart", lambda: start()),
435 pystray.MenuItem("Settings", lambda: open_settings()))
436 icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu)
437 icon.visible = True
438 icon.run(setup=start)
439
440if __name__ == "__main__":
441 show_icon()