replace notify_state timer with PPT async events
authorAndrew Lorimer <andrew@lorimer.id.au>
Sun, 8 Aug 2021 06:51:29 +0000 (16:51 +1000)
committerAndrew Lorimer <andrew@lorimer.id.au>
Sun, 8 Aug 2021 06:51:29 +0000 (16:51 +1000)
README.md
ppt_control/config.py
ppt_control/http_server_39.py [changed mode: 0755->0644]
ppt_control/ppt_control.py [changed mode: 0755->0644]
ppt_control/ppt_control_obs.py [changed mode: 0755->0644]
ppt_control/static/black.jpg [changed mode: 0755->0644]
ppt_control/static/icons/ppt.png [changed mode: 0755->0644]
ppt_control/static/index.html [changed mode: 0755->0644]
index d8b8f82214e430e2e8dfce34acc86bfe8059d0f0..2d83314a79672f7b3b3ef7002f04666b7907ee31 100644 (file)
--- a/README.md
+++ b/README.md
@@ -20,7 +20,9 @@ will install all three components. You can then start the daemon by running
 
 `py -m ppt_control`
 
-from a command prompt (note the underscore). There are a few steps to set the package up fully:
+from a command prompt (note the underscore). On first run, Windows Defender will show a warning and attempt to block Python from starting the server. You can safely allow the program through the firewall. You can now start a PowerPoint slideshow and navigate to the server by IP address/hostname (`http://localhost` if on the same machine) and control the slideshow.
+
+There are a few steps to set the package up fully:
 
 ### Starting the daemon at bootup
 
@@ -28,7 +30,7 @@ There are several ways to start a Python program at login. Here is one method:
 
 1. Navigate to the directory containing the `pythonw` executable in Explorer (usually in `C:\Program Files\Python36` - run `python -c "import sys, print(sys.executable)"` to check)
 2. Right click on `pythonw.exe` and click "Create shortcut"
-3. A shortcut will be placed on the desktop. Go to the properties of this shortcut, and in the target field, append ` -m ppt_control` (after the quotes, including an initial space). You can also rename the shortcut if you like.
+3. Windows will ask you whether to place a shortcut on the desktop - click yes. Go to the properties of this shortcut, and in the target field, append ` -m ppt_control` (after the quotes, including an initial space). You can also change the name and icon of the shortcut if you like.
 4. Copy this shortcut into the Startup folder (`%AppData%\Microsoft\Windows\Start Menu\Programs\Startup`). To quickly navigate to this folder, open an Explorer window and type `startup` in the address bar.
 
 ### Using the HTTP interface in OBS
index 7964cc99f6169b48a2f70cb8d35e97bfe0241500..e0416a498084b13889d14a38d30be86d81032bf2 100644 (file)
@@ -6,10 +6,11 @@ prefs = None
 
 defaults = {
         'Main': {
-            'logging': 'info',
+            'logging': 'debug',
             'cache': r'''C:\Windows\Temp\ppt-cache''',
             'cache_format': 'JPG',
             'cache_timeout': 5*60,
+            'cache_init': True,
             'blackwhite': 'both'
         },
         'HTTP': {
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 4218f5c..8502bcb
@@ -47,6 +47,7 @@ interface_root = None
 interface_thread = None\r
 CONFIG_FILE = r'''..\ppt-control.ini'''\r
 LOGFILE = r'''..\ppt-control.log'''\r
+REFRESH_INTERVAL = 2\r
 logger = None\r
 refresh_daemon = None\r
 status_label = None\r
@@ -56,6 +57,7 @@ ws_daemon = None
 http_server = None\r
 reset_ppt_button = None\r
 icon = None\r
+ws_stop_event = False\r
 \r
 \r
 class Handler(server.SimpleHTTPRequestHandler):\r
@@ -125,6 +127,7 @@ def state_event():
 \r
 \r
 def notify_state():\r
+    logger.debug("Notifying state")\r
     global STATE\r
     if current_slideshow and STATE["connected"] == 1:\r
         try:\r
@@ -166,37 +169,37 @@ async def ws_receive(websocket, path):
             if data["action"] == "prev":\r
                 if current_slideshow:\r
                     current_slideshow.prev()\r
-                notify_state()\r
+                #notify_state()\r
             elif data["action"] == "next":\r
                 if current_slideshow:\r
                     current_slideshow.next()\r
-                notify_state()\r
+                #notify_state()\r
             elif data["action"] == "first":\r
                 if current_slideshow:\r
                     current_slideshow.first()\r
-                notify_state()\r
+                #notify_state()\r
             elif data["action"] == "last":\r
                 if current_slideshow:\r
                     current_slideshow.last()\r
-                notify_state()\r
+                #notify_state()\r
             elif data["action"] == "black":\r
                 if current_slideshow:\r
                     if current_slideshow.visible() == 3:\r
                         current_slideshow.normal()\r
                     else:\r
                         current_slideshow.black()\r
-                notify_state()\r
+                #notify_state()\r
             elif data["action"] == "white":\r
                 if current_slideshow:\r
                     if current_slideshow.visible() == 4:\r
                         current_slideshow.normal()\r
                     else:\r
                         current_slideshow.white()\r
-                notify_state()\r
+                #notify_state()\r
             elif data["action"] == "goto":\r
                 if current_slideshow:\r
                     current_slideshow.goto(int(data["value"]))\r
-                notify_state()\r
+                #notify_state()\r
             else:\r
                 logger.error("Received unnsupported event: {}", data)\r
     finally:\r
@@ -244,6 +247,30 @@ def start_ws():
     ws_daemon.start()\r
     logger.info("Started websocket server")\r
 \r
+def restart_ws():\r
+    global ws_daemon\r
+    global ws_stop_event\r
+    if ws_daemon and not ws_stop_event:\r
+        ws_stop_event = True\r
+        logger.debug("Stopped WebSocket server")\r
+        refresh_status()\r
+        #ws_daemon = None\r
+        time.sleep(2)\r
+        #start_ws()\r
+        refresh_status()\r
+    \r
+\r
+class ApplicationEvents:\r
+    def OnSlideShowNextSlide(self, *args):\r
+        notify_state()\r
+        logger.debug("Slide changed")\r
+        current_slideshow.export_current_next()\r
+\r
+    def OnSlideShowPrevSlide(self, *args):\r
+        notify_state()\r
+        logger.debug("Slide changed")\r
+        current_slideshow.export_current_next()\r
+\r
 class Slideshow:\r
     def __init__(self, instance, blackwhite):\r
         self.instance = instance\r
@@ -252,15 +279,21 @@ class Slideshow:
 \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
+        self.view = self.instance.SlideShowWindows(1).View\r
 \r
         if self.instance.ActivePresentation is None:\r
-            raise ValueError("PPT instance has no  active presentation")\r
+            raise ValueError("PPT instance has no active presentation")\r
         self.presentation = self.instance.ActivePresentation\r
 \r
         self.blackwhite = blackwhite\r
 \r
-        self.export_current_next()\r
+        if config.prefs["Main"]["cache_init"]:\r
+            self.export_all()\r
+        else:\r
+            self.export_current_next()\r
+\r
+        events = win32com.client.WithEvents(win32com.client.GetActiveObject("Powerpoint.Application"), ApplicationEvents)\r
+        logger.debug("Dispatched events")\r
 \r
     def unload(self):\r
         connect_ppt()\r
@@ -272,10 +305,10 @@ class Slideshow:
 \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
+            self.view = self.instance.SlideShowWindows(1).View\r
 \r
             if self.instance.ActivePresentation is None:\r
-                raise ValueError("PPT instance has no  active presentation")\r
+                raise ValueError("PPT instance has no active presentation")\r
         except:\r
             self.unload()\r
 \r
@@ -389,6 +422,7 @@ class Slideshow:
 \r
     def export(self, slide):\r
         destination = config.prefs["Main"]["cache"] + "\\" + self.name() + "\\" + str(slide) + ".jpg"\r
+        logger.debug("Exporting slide " + str(slide))\r
         os.makedirs(os.path.dirname(destination), exist_ok=True)\r
         if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > config.prefs.getint("Main", "cache_timeout"):\r
             if slide <= self.total_slides():\r
@@ -409,7 +443,7 @@ class Slideshow:
                 pass\r
 \r
     def export_all(self):\r
-        for i in range(1, self.total_slides()):\r
+        for i in range(1, self.total_slides() + 2):\r
             self.export(i)\r
 \r
 def get_ppt_instance():\r
@@ -423,13 +457,16 @@ def get_current_slideshow():
 \r
 def refresh_interval():\r
     while getattr(refresh_daemon, "do_run", True):\r
+        logger.debug("Refreshing general")\r
+        pythoncom.PumpWaitingMessages()\r
         current_slideshow.refresh()\r
-        notify_state()\r
-        refresh_status()\r
-        time.sleep(0.5)\r
+        if current_slideshow.visible != STATE["visible"]:\r
+            notify_state()\r
+        #refresh_status()\r
+        time.sleep(REFRESH_INTERVAL)\r
 \r
 def refresh_status():\r
-    if interface_root is not None and interface_root.state == "normal":\r
+    if interface_root is not None:\r
         logger.debug("Refreshing UI")\r
         if status_label is not None:\r
             status_label.config(text="PowerPoint status: " + ("not " if not STATE["connected"] else "") +  "connected")\r
@@ -497,14 +534,15 @@ def on_closing():
 def open_settings(_=None):\r
     global interface_root\r
     global interface_thread\r
-    interface_root = tk.Tk()\r
-    interface_root.protocol("WM_DELETE_WINDOW", on_closing)\r
-    interface_root.iconphoto(False, tk.PhotoImage(file=os.path.dirname(os.path.realpath(__file__)) + r'''\static\icons\ppt.png'''))\r
-    interface_root.geometry("600x300+300+300")\r
-    app = Interface(interface_root)\r
-    interface_thread = threading.Thread(target=interface_root.mainloop())\r
-    interface_thread.setDaemon(True)\r
-    interface_thread.start()\r
+    if interface_root is None:\r
+        interface_root = tk.Tk()\r
+        interface_root.protocol("WM_DELETE_WINDOW", on_closing)\r
+        interface_root.iconphoto(False, tk.PhotoImage(file=os.path.dirname(os.path.realpath(__file__)) + r'''\static\icons\ppt.png'''))\r
+        interface_root.geometry("600x300+300+300")\r
+        app = Interface(interface_root)\r
+        interface_thread = threading.Thread(target=interface_root.mainloop())\r
+        interface_thread.setDaemon(True)\r
+        interface_thread.start()\r
 \r
 def null_action():\r
     pass\r
@@ -546,7 +584,7 @@ class Interface(ttk.Frame):
         reset_http_button = ttk.Button(self, text="Restart", command=restart_http)\r
         reset_http_button.place(x=300, y=30)\r
 \r
-        #reset_ws_button = ttk.Button(self, text="Restart", command=null_action)\r
+        #reset_ws_button = ttk.Button(self, text="Restart", command=restart_ws)\r
         #reset_ws_button.place(x=300, y=50)\r
 \r
         status_label = ttk.Label(self)\r
@@ -563,13 +601,18 @@ class Interface(ttk.Frame):
         \r
 def exit_action(icon):\r
     logger.debug("User requested shutdown")\r
+    if interface_root is not None:\r
+        try:\r
+            interface_root.destroy()\r
+        except:\r
+            pass\r
     icon.visible = False\r
     icon.stop()\r
 \r
 def refresh_menu():\r
     icon.menu = (pystray.MenuItem("Status: " + "dis"*(not STATE["connected"]) + "connected", lambda: null_action(), enabled=False),\r
             pystray.MenuItem("Stop", lambda: exit_action(icon)),\r
-            pystray.MenuItem("Settings", lambda: open_settings(), enabled=False)\r
+            pystray.MenuItem("Settings", lambda: open_settings(), enabled=True)\r
             )\r
 \r
 def show_icon():\r
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)