bugfixing, obs interface
[ppt-control.git] / ppt_control.py
index 75cf1b0a54264e6323c49c11c04ec3315ae810f4..2e0cc3d41debb9351674cbb9551e2cdaa63f3a87 100755 (executable)
@@ -15,9 +15,15 @@ import urllib
 import posixpath\r
 import time\r
 import pythoncom\r
+import pystray\r
+import tkinter as tk\r
+from tkinter import ttk\r
+from PIL import Image, ImageDraw\r
 \r
 logging.basicConfig()\r
 \r
+global STATE\r
+global STATE_DEFAULT\r
 global current_slideshow\r
 current_slideshow = None\r
 CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''\r
@@ -50,6 +56,9 @@ class Handler(server.SimpleHTTPRequestHandler):
         if len(words) > 0 and words[0] == "cache":\r
             if current_slideshow:\r
                 path = CACHEDIR + "\\" + current_slideshow.name()\r
+            else:\r
+                path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "black.jpg") + '/'\r
+                return path\r
             words.pop(0)\r
         else:\r
             path = self.directory\r
@@ -67,11 +76,13 @@ def run_http():
     http_server = server.HTTPServer(("", 80), Handler)\r
     http_server.serve_forever()\r
 \r
-STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""}\r
+STATE_DEFAULT = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""}\r
+STATE = STATE_DEFAULT\r
 USERS = set()\r
 \r
 \r
 def state_event():\r
+    print("Running state event")\r
     return json.dumps({"type": "state", **STATE})\r
 \r
 \r
@@ -80,12 +91,15 @@ def users_event():
 \r
 \r
 async def notify_state():\r
-    global current_slideshow\r
-    if current_slideshow:\r
+    print("Notifying state to " + str(len(USERS)) + " users")\r
+    global STATE\r
+    if current_slideshow and STATE["connected"] == 1:\r
         STATE["current"] = current_slideshow.current_slide()\r
         STATE["total"] = current_slideshow.total_slides()\r
         STATE["visible"] = current_slideshow.visible()\r
         STATE["name"] = current_slideshow.name()\r
+    else:\r
+        STATE = STATE_DEFAULT\r
     if USERS:  # asyncio.wait doesn't accept an empty list\r
         message = state_event()\r
         await asyncio.wait([user.send(message) for user in USERS])\r
@@ -108,6 +122,7 @@ async def unregister(websocket):
 \r
 \r
 async def ws_handle(websocket, path):\r
+    print("Received command")\r
     global current_slideshow\r
     # register(websocket) sends user_event() to websocket\r
     await register(websocket)\r
@@ -150,19 +165,22 @@ async def ws_handle(websocket, path):
                     current_slideshow.goto(int(data["value"]))\r
                 await notify_state()\r
             elif data["action"] == "refresh":\r
+                print("Received refresh command")\r
+                await notify_state()\r
                 if current_slideshow:\r
                     current_slideshow.export_current_next()\r
                     current_slideshow.refresh()\r
-                await notify_state()\r
             else:\r
                 logging.error("unsupported event: {}", data)\r
     finally:\r
         await unregister(websocket)\r
 \r
 def run_ws():\r
+    # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084\r
+    # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/\r
     pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)\r
     asyncio.set_event_loop(asyncio.new_event_loop())\r
-    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)\r
+    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None)\r
     asyncio.get_event_loop().run_until_complete(start_server)\r
     asyncio.get_event_loop().run_forever()\r
 \r
@@ -200,71 +218,120 @@ class Slideshow:
         if self.instance.ActivePresentation is None:\r
             raise ValueError("PPT instance has no  active presentation")\r
         self.presentation = self.instance.ActivePresentation\r
-        \r
-        self.export_all()\r
+\r
+    def unload(self):\r
+        connect_ppt()\r
 \r
     def refresh(self):\r
-        if self.instance is None:\r
-            raise ValueError("PPT instance cannot be None")\r
+        try:\r
+            if self.instance is None:\r
+                raise ValueError("PPT instance cannot be None")\r
 \r
-        #if self.instance.SlideShowWindows.Count == 0:\r
-        #    raise ValueError("PPT instance has no slideshow windows")\r
-        self.view = self.instance.SlideShowWindows[0].View\r
+            if self.instance.SlideShowWindows.Count == 0:\r
+                raise ValueError("PPT instance has no slideshow windows")\r
+            self.view = self.instance.SlideShowWindows[0].View\r
 \r
-        if self.instance.ActivePresentation is None:\r
-            raise ValueError("PPT instance has no  active presentation")\r
+            if self.instance.ActivePresentation is None:\r
+                raise ValueError("PPT instance has no  active presentation")\r
+        except:\r
+            self.unload()\r
 \r
     def total_slides(self):\r
-        return len(self.presentation.Slides)\r
+        try:\r
+            self.refresh()\r
+            return len(self.presentation.Slides)\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def current_slide(self):\r
-        return self.view.CurrentShowPosition\r
+        try:\r
+            self.refresh()\r
+            return self.view.CurrentShowPosition\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def visible(self):\r
-        return self.view.State\r
+        try:\r
+            self.refresh()\r
+            return self.view.State\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def prev(self):\r
-        self.refresh()\r
-        self.view.Previous()\r
+        try:\r
+            self.refresh()\r
+            self.view.Previous()\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def next(self):\r
-        self.refresh()\r
-        self.view.Next()\r
-        self.export_current_next()\r
+        try:\r
+            self.refresh()\r
+            self.view.Next()\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def first(self):\r
-        self.refresh()\r
-        self.view.First()\r
-        self.export_current_next()\r
+        try:\r
+            self.refresh()\r
+            self.view.First()\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
                 \r
     def last(self):\r
-        self.refresh()\r
-        self.view.Last()\r
-        self.export_current_next()\r
+        try:\r
+            self.refresh()\r
+            self.view.Last()\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def goto(self, slide):\r
-        self.refresh()\r
-        if slide <= self.total_slides():\r
-            self.view.GotoSlide(slide)\r
-        else:\r
-            self.last()\r
-            self.next()\r
-        self.export_current_next()\r
+        try:\r
+            self.refresh()\r
+            if slide <= self.total_slides():\r
+                self.view.GotoSlide(slide)\r
+            else:\r
+                self.last()\r
+                self.next()\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def black(self):\r
-        self.refresh()\r
-        self.view.State = 3\r
+        try:\r
+            self.refresh()\r
+            self.view.State = 3\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def white(self):\r
-        self.refresh()\r
-        self.view.State = 4\r
+        try:\r
+            self.refresh()\r
+            self.view.State = 4\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def normal(self):\r
-        self.refresh()\r
-        self.view.State = 1\r
+        try:\r
+            self.refresh()\r
+            self.view.State = 1\r
+            self.export_current_next()\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
 \r
     def name(self):\r
-        return self.presentation.Name\r
+        try:\r
+            self.refresh()\r
+            return self.presentation.Name\r
+        except ValueError or pywintypes.com_error:\r
+            self.unload()\r
+\r
 \r
     def export_current_next(self):\r
         self.export(self.current_slide())\r
@@ -279,7 +346,6 @@ class Slideshow:
                 while attempts < 3:\r
                     try:\r
                         self.presentation.Slides(slide).Export(destination, "JPG")\r
-                        time.sleep(0.5)\r
                         break\r
                     except:\r
                         pass\r
@@ -302,22 +368,74 @@ def get_ppt_instance():
 def get_current_slideshow():\r
     return current_slideshow\r
 \r
-\r
-if __name__ == "__main__":\r
-\r
-    start_server()\r
-    \r
+def connect_ppt():\r
+    global STATE\r
+    if STATE["connected"] == 1:\r
+        print("Disconnected from PowerPoint instance")\r
+    STATE = STATE_DEFAULT\r
     while True:\r
-        # Check if PowerPoint is running\r
-        instance = get_ppt_instance()\r
         try:\r
+            instance = get_ppt_instance()\r
+            global current_slideshow\r
             current_slideshow = Slideshow(instance)\r
             STATE["connected"] = 1\r
             STATE["current"] = current_slideshow.current_slide()\r
             STATE["total"] = current_slideshow.total_slides()\r
             print("Connected to PowerPoint instance")\r
+            current_slideshow.export_all()\r
             break\r
         except ValueError as e:\r
             current_slideshow = None\r
             pass\r
         time.sleep(1)\r
+\r
+def start(_=None):\r
+    #root = tk.Tk()\r
+    #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png"))\r
+    #root.geometry("250x150+300+300")\r
+    #app = Interface(root)\r
+    #interface_thread = threading.Thread(target=root.mainloop())\r
+    #interface_thread.setDaemon(True)\r
+    #interface_thread.start()\r
+    start_server()\r
+    connect_ppt()\r
+    \r
+\r
+def null_action():\r
+    pass\r
+\r
+class Interface(ttk.Frame):\r
+\r
+    def __init__(self, parent):\r
+        ttk.Frame.__init__(self, parent)\r
+\r
+        self.parent = parent\r
+\r
+        self.initUI()\r
+\r
+    def initUI(self):\r
+\r
+        self.parent.title("ppt-control")\r
+        self.style = ttk.Style()\r
+        #self.style.theme_use("default")\r
+\r
+        self.pack(fill=tk.BOTH, expand=1)\r
+\r
+        quitButton = ttk.Button(self, text="Close",\r
+            command=self.quit)\r
+        quitButton.place(x=50, y=50)\r
+        status_label = ttk.Label(self, text="PowerPoint status: not detected")\r
+        status_label.place(x=10,y=10)\r
+        \r
+        \r
+\r
+def show_icon():\r
+    menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False),\r
+            pystray.MenuItem("Restart", lambda: start()),\r
+            pystray.MenuItem("Settings", lambda: open_settings()))\r
+    icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu)\r
+    icon.visible = True\r
+    icon.run(setup=start)\r
+\r
+if __name__ == "__main__":\r
+    show_icon()\r