ppt_control.pyon commit restructure project, add setuptools files (7cb045a)
   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    http_daemon = threading.Thread(name="http_daemon", target=run_http)
 189    http_daemon.setDaemon(True)
 190    http_daemon.start()
 191    print("Started HTTP server")
 192    
 193    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
 194    ws_daemon.setDaemon(True)
 195    ws_daemon.start()
 196    print("Started websocket server")
 197
 198class Slideshow:
 199    def __init__(self, instance):
 200        self.instance = instance
 201        if self.instance is None:
 202            raise ValueError("PPT instance cannot be None")
 203
 204        if self.instance.SlideShowWindows.Count == 0:
 205            raise ValueError("PPT instance has no slideshow windows")
 206        self.view = self.instance.SlideShowWindows[0].View
 207
 208        if self.instance.ActivePresentation is None:
 209            raise ValueError("PPT instance has no  active presentation")
 210        self.presentation = self.instance.ActivePresentation
 211
 212    def unload(self):
 213        connect_ppt()
 214
 215    def refresh(self):
 216        try:
 217            if self.instance is None:
 218                raise ValueError("PPT instance cannot be None")
 219
 220            if self.instance.SlideShowWindows.Count == 0:
 221                raise ValueError("PPT instance has no slideshow windows")
 222            self.view = self.instance.SlideShowWindows[0].View
 223
 224            if self.instance.ActivePresentation is None:
 225                raise ValueError("PPT instance has no  active presentation")
 226        except:
 227            self.unload()
 228
 229    def total_slides(self):
 230        try:
 231            self.refresh()
 232            return len(self.presentation.Slides)
 233        except ValueError or pywintypes.com_error:
 234            self.unload()
 235
 236    def current_slide(self):
 237        try:
 238            self.refresh()
 239            return self.view.CurrentShowPosition
 240        except ValueError or pywintypes.com_error:
 241            self.unload()
 242
 243    def visible(self):
 244        try:
 245            self.refresh()
 246            return self.view.State
 247        except ValueError or pywintypes.com_error:
 248            self.unload()
 249
 250    def prev(self):
 251        try:
 252            self.refresh()
 253            self.view.Previous()
 254            self.export_current_next()
 255        except ValueError or pywintypes.com_error:
 256            self.unload()
 257
 258    def next(self):
 259        try:
 260            self.refresh()
 261            self.view.Next()
 262            self.export_current_next()
 263        except ValueError or pywintypes.com_error:
 264            self.unload()
 265
 266    def first(self):
 267        try:
 268            self.refresh()
 269            self.view.First()
 270            self.export_current_next()
 271        except ValueError or pywintypes.com_error:
 272            self.unload()
 273                
 274    def last(self):
 275        try:
 276            self.refresh()
 277            self.view.Last()
 278            self.export_current_next()
 279        except ValueError or pywintypes.com_error:
 280            self.unload()
 281
 282    def goto(self, slide):
 283        try:
 284            self.refresh()
 285            if slide <= self.total_slides():
 286                self.view.GotoSlide(slide)
 287            else:
 288                self.last()
 289                self.next()
 290            self.export_current_next()
 291        except ValueError or pywintypes.com_error:
 292            self.unload()
 293
 294    def black(self):
 295        try:
 296            self.refresh()
 297            self.view.State = 3
 298            self.export_current_next()
 299        except ValueError or pywintypes.com_error:
 300            self.unload()
 301
 302    def white(self):
 303        try:
 304            self.refresh()
 305            self.view.State = 4
 306            self.export_current_next()
 307        except ValueError or pywintypes.com_error:
 308            self.unload()
 309
 310    def normal(self):
 311        try:
 312            self.refresh()
 313            self.view.State = 1
 314            self.export_current_next()
 315        except ValueError or pywintypes.com_error:
 316            self.unload()
 317
 318    def name(self):
 319        try:
 320            self.refresh()
 321            return self.presentation.Name
 322        except ValueError or pywintypes.com_error:
 323            self.unload()
 324
 325
 326    def export_current_next(self):
 327        self.export(self.current_slide())
 328        self.export(self.current_slide() + 1)
 329
 330    def export(self, slide):
 331        destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"
 332        os.makedirs(os.path.dirname(destination), exist_ok=True)
 333        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:
 334            if slide <= self.total_slides():
 335                attempts = 0
 336                while attempts < 3:
 337                    try:
 338                        self.presentation.Slides(slide).Export(destination, "JPG")
 339                        break
 340                    except:
 341                        pass
 342                    attempts += 1
 343            elif slide == self.total_slides() + 1:
 344                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))
 345            else:
 346                pass
 347
 348    def export_all(self):
 349        for i in range(1, self.total_slides()):
 350            self.export(i)
 351
 352def get_ppt_instance():
 353    instance = win32com.client.Dispatch('Powerpoint.Application')
 354    if instance is None or instance.SlideShowWindows.Count == 0:
 355        return None
 356    return instance
 357
 358def get_current_slideshow():
 359    return current_slideshow
 360
 361def connect_ppt():
 362    global STATE
 363    if STATE["connected"] == 1:
 364        print("Disconnected from PowerPoint instance")
 365    STATE = STATE_DEFAULT
 366    while True:
 367        try:
 368            instance = get_ppt_instance()
 369            global current_slideshow
 370            current_slideshow = Slideshow(instance)
 371            STATE["connected"] = 1
 372            STATE["current"] = current_slideshow.current_slide()
 373            STATE["total"] = current_slideshow.total_slides()
 374            print("Connected to PowerPoint instance")
 375            current_slideshow.export_all()
 376            break
 377        except ValueError as e:
 378            current_slideshow = None
 379            pass
 380        time.sleep(1)
 381
 382def start(_=None):
 383    #root = tk.Tk()
 384    #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png"))
 385    #root.geometry("250x150+300+300")
 386    #app = Interface(root)
 387    #interface_thread = threading.Thread(target=root.mainloop())
 388    #interface_thread.setDaemon(True)
 389    #interface_thread.start()
 390    start_server()
 391    connect_ppt()
 392    
 393
 394def null_action():
 395    pass
 396
 397class Interface(ttk.Frame):
 398
 399    def __init__(self, parent):
 400        ttk.Frame.__init__(self, parent)
 401
 402        self.parent = parent
 403
 404        self.initUI()
 405
 406    def initUI(self):
 407
 408        self.parent.title("ppt-control")
 409        self.style = ttk.Style()
 410        #self.style.theme_use("default")
 411
 412        self.pack(fill=tk.BOTH, expand=1)
 413
 414        quitButton = ttk.Button(self, text="Close",
 415            command=self.quit)
 416        quitButton.place(x=50, y=50)
 417        status_label = ttk.Label(self, text="PowerPoint status: not detected")
 418        status_label.place(x=10,y=10)
 419        
 420        
 421
 422def show_icon():
 423    menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False),
 424            pystray.MenuItem("Restart", lambda: start()),
 425            pystray.MenuItem("Settings", lambda: open_settings()))
 426    icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu)
 427    icon.visible = True
 428    icon.run(setup=start)
 429
 430if __name__ == "__main__":
 431    show_icon()