Add macOS support and Pyinstaller spec file
authorJakob Cornell <jakob+gpg@jcornell.net>
Sat, 28 Dec 2024 22:20:20 +0000 (16:20 -0600)
committerJakob Cornell <jakob+gpg@jcornell.net>
Sat, 28 Dec 2024 22:35:30 +0000 (16:35 -0600)
main.py
tutoring_tool.spec [new file with mode: 0644]

diff --git a/main.py b/main.py
index a8cf3cfaeb9dc09bbf650ea7102a00490aeb1969..5128bc60dee81dcb3b473439280dbb88fcf264a5 100644 (file)
--- a/main.py
+++ b/main.py
@@ -1,9 +1,11 @@
 from collections import defaultdict, namedtuple, OrderedDict
 from contextlib import contextmanager
 from enum import Enum
+from matplotlib import pyplot
 import itertools
 import json
-from matplotlib import pyplot
+import matplotlib
+import sys
 
 import tkinter as tk
 from tkinter import ttk
@@ -54,6 +56,13 @@ def win_write_clipboard_html(text):
                clip.SetClipboardData(fmt, compute_payload(text.encode('utf-8')))
 
 
+def mac_write_clipboard_html(text: str) -> None:
+       from pasteboard import Pasteboard
+       import pasteboard
+
+       Pasteboard().set_contents(text, type = pasteboard.HTML)
+
+
 TK_PAD = {'padx': 5, 'pady': 5}
 
 def center_on_parent(child, parent):
@@ -239,6 +248,12 @@ class MainView(View):
                                on_save_err()
                menu = tk.Menu(self.root)
                self.root.config(menu = menu)
+
+               # Intercept macOS system quit (app menu/Cmd+Q). https://stackoverflow.com/a/72152310
+               # Linux Tk doesn't seem to mind the createcommand call, so it's done on all platforms.
+               tk.Menu(menu, name = "apple")
+               self.root.createcommand("tk::mac::Quit", on_close)
+
                file_menu = tk.Menu(menu, tearoff = 0)
                file_menu.add_command(label = "Save", command = on_save, accelerator = "Ctrl+S")
                menu.add_cascade(label = "File", menu = file_menu)
@@ -354,7 +369,12 @@ class MainView(View):
                                for v in row:
                                        SubElem(tr, 'td').text = v
 
-                       win_write_clipboard_html(ElementTree.tostring(table, encoding = 'unicode'))
+                       html_text = ElementTree.tostring(table, encoding = "unicode")
+                       if sys.platform == "win32":
+                               win_write_clipboard_html(html_text)
+                       elif sys.platform == "darwin":
+                               mac_write_clipboard_html(html_text)
+
                self.gen_table_btn = ttk.Button(actions_frame, text = "Copy table to clipboard", command = on_gen_table)
 
                self.student_sel.pack(**TK_PAD, side = tk.LEFT)
@@ -760,7 +780,10 @@ class AutoJsonStorageMgr:
 
        def __init__(self, name):
                import os.path, pathlib
-               path = pathlib.Path(os.path.expanduser('~'), 'AppData', self.UUID)
+               if sys.platform == "win32":
+                       path = pathlib.Path(os.path.expanduser('~'), 'AppData', self.UUID)
+               else:
+                       path = pathlib.Path(os.path.expanduser("~"), self.UUID)
                path.mkdir(parents = True, exist_ok = True)
                self.path = path.joinpath(name)
 
@@ -777,6 +800,10 @@ class AutoJsonStorageMgr:
                        json.dump(data, f)
 
 
+# This seems to be necessary for Pyinstaller builds; not sure why.
+if sys.platform == "darwin":
+       matplotlib.use("macosx")
+
 model = MainModel(AutoJsonStorageMgr('data'), AutoJsonStorageMgr('state'))
 view = MainView()
 controller = MainController()
diff --git a/tutoring_tool.spec b/tutoring_tool.spec
new file mode 100644 (file)
index 0000000..fe664ca
--- /dev/null
@@ -0,0 +1,51 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+    ['main.py'],
+    pathex=[],
+    binaries=[],
+    datas=[],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    noarchive=False,
+    optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    [],
+    exclude_binaries=True,
+    name='tutoring_tool',
+    debug=True,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    console=False,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+    icon=['tt_icon.png'],  # one of the RedNotebook.svg icons from Papirus
+)
+coll = COLLECT(
+    exe,
+    a.binaries,
+    a.datas,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    name='Tutoring Tool',
+)
+app = BUNDLE(
+    coll,
+    name='Tutoring Tool.app',
+    icon='tt_icon.png',
+    bundle_identifier=None,
+)