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()