|
48 | 48 | from tkinter import font as tkfont |
49 | 49 |
|
50 | 50 | APP_NAME = "ScratchPy Studio" |
51 | | -APP_VERSION = "1.0" |
| 51 | +APP_VERSION = "1.0.2" |
52 | 52 | PROJECT_EXT = ".spy" |
53 | 53 | IS_WINDOWS = sys.platform.startswith("win") |
54 | 54 |
|
| 55 | +REPO_URL = "https://github.com/ZDStudios/scratchpy-studio" |
| 56 | +RELEASES_URL = REPO_URL + "/releases/latest" |
| 57 | +RELEASES_API = ("https://api.github.com/repos/ZDStudios/scratchpy-studio" |
| 58 | + "/releases/latest") |
| 59 | + |
55 | 60 | # --------------------------------------------------------------------------- # |
56 | 61 | # Palette of colours - lifted from the Scratch 3 design language |
57 | 62 | # --------------------------------------------------------------------------- # |
@@ -3781,6 +3786,65 @@ def on_release(self, ev): |
3781 | 3786 | ASSET_DIR = os.path.join(APP_DIR, "scratchpy_assets") |
3782 | 3787 |
|
3783 | 3788 |
|
| 3789 | +def running_from() -> str: |
| 3790 | + """The exact file this copy of ScratchPy is running out of.""" |
| 3791 | + if FROZEN: |
| 3792 | + return sys.executable |
| 3793 | + try: |
| 3794 | + return os.path.abspath(__file__) |
| 3795 | + except NameError: # pragma: no cover |
| 3796 | + return "unknown" |
| 3797 | + |
| 3798 | + |
| 3799 | +def build_kind() -> str: |
| 3800 | + return "bundled app" if FROZEN else "source file" |
| 3801 | + |
| 3802 | + |
| 3803 | +def build_stamp() -> str: |
| 3804 | + """When the running copy was last changed - catches a stale build.""" |
| 3805 | + try: |
| 3806 | + when = time.localtime(os.path.getmtime(running_from())) |
| 3807 | + return time.strftime("%d %b %Y, %H:%M", when) |
| 3808 | + except Exception: |
| 3809 | + return "unknown" |
| 3810 | + |
| 3811 | + |
| 3812 | +def build_summary() -> str: |
| 3813 | + return "\n".join([ |
| 3814 | + "%s %s" % (APP_NAME, APP_VERSION), |
| 3815 | + "Running from the %s:" % build_kind(), |
| 3816 | + " %s" % running_from(), |
| 3817 | + " last changed %s" % build_stamp(), |
| 3818 | + "Python %s, Tk %s, %s %s" % (platform.python_version(), |
| 3819 | + tk.TkVersion, platform.system(), |
| 3820 | + platform.release()), |
| 3821 | + "%d blocks loaded" % len(SPECS), |
| 3822 | + ]) |
| 3823 | + |
| 3824 | + |
| 3825 | +def version_tuple(text: str) -> tuple: |
| 3826 | + parts = [] |
| 3827 | + for chunk in str(text).lstrip("vV").split("."): |
| 3828 | + digits = "".join(c for c in chunk if c.isdigit()) |
| 3829 | + parts.append(int(digits) if digits else 0) |
| 3830 | + return tuple(parts + [0, 0, 0])[:3] |
| 3831 | + |
| 3832 | + |
| 3833 | +def latest_release(timeout: float = 12.0) -> Tuple[str, str]: |
| 3834 | + """Ask GitHub for the newest published version. Returns (tag, error).""" |
| 3835 | + import urllib.request |
| 3836 | + request = urllib.request.Request( |
| 3837 | + RELEASES_API, |
| 3838 | + headers={"User-Agent": "ScratchPyStudio/" + APP_VERSION, |
| 3839 | + "Accept": "application/vnd.github+json"}) |
| 3840 | + try: |
| 3841 | + with urllib.request.urlopen(request, timeout=timeout) as reply: |
| 3842 | + data = json.loads(reply.read().decode("utf-8", "replace")) |
| 3843 | + return str(data.get("tag_name") or ""), "" |
| 3844 | + except Exception as exc: |
| 3845 | + return "", "%s: %s" % (type(exc).__name__, exc) |
| 3846 | + |
| 3847 | + |
3784 | 3848 | def find_python() -> str: |
3785 | 3849 | """The interpreter used to run your programs and to drive pip. |
3786 | 3850 |
|
@@ -5158,6 +5222,11 @@ def build_topbar(self): |
5158 | 5222 | outline="") |
5159 | 5223 | tk.Label(bar, text=APP_NAME, bg=UI["topbar"], fg="#FFFFFF", |
5160 | 5224 | font=(FONT_FAMILY, 13, "bold")).pack(side="left") |
| 5225 | + version = tk.Label(bar, text=" v" + APP_VERSION, bg=UI["topbar"], |
| 5226 | + fg="#C9B6FF", font=(FONT_FAMILY, 9), |
| 5227 | + cursor="hand2") |
| 5228 | + version.pack(side="left", padx=(4, 0)) |
| 5229 | + version.bind("<Button-1>", lambda e: self.open_settings()) |
5161 | 5230 | self.title_var = tk.StringVar(value="Untitled") |
5162 | 5231 | tk.Label(bar, textvariable=self.title_var, bg=UI["topbar"], |
5163 | 5232 | fg="#E4D9FF", font=(FONT_FAMILY, 10)).pack(side="left", |
@@ -6157,10 +6226,11 @@ def help_guide(self): |
6157 | 6226 | def about(self): |
6158 | 6227 | messagebox.showinfo( |
6159 | 6228 | "About " + APP_NAME, |
6160 | | - "%s %s\n\nA Scratch style editor that writes real Python.\n" |
| 6229 | + "A Scratch style editor that writes real Python.\n" |
6161 | 6230 | "Everything lives in one file, using nothing but the\n" |
6162 | | - "Python standard library.\n\nRunning on Python %s" % |
6163 | | - (APP_NAME, APP_VERSION, platform.python_version())) |
| 6231 | + "Python standard library.\n\n" |
| 6232 | + + build_summary() + "\n\n" + REPO_URL + |
| 6233 | + "\n\nSettings has a 'Check for updates' button.") |
6164 | 6234 |
|
6165 | 6235 | def on_close(self): |
6166 | 6236 | self.workspace.close_editor(True) |
@@ -6344,6 +6414,56 @@ def __init__(self, parent, app: "App"): |
6344 | 6414 | activebackground=UI["panel"], bd=0, highlightthickness=0, |
6345 | 6415 | font=(FONT_FAMILY, 10)).pack(anchor="w", padx=10) |
6346 | 6416 |
|
| 6417 | + box3 = tk.LabelFrame(top, text=" About this copy ", bg=UI["panel"], |
| 6418 | + fg=UI["text"], bd=1, relief="solid", |
| 6419 | + font=(FONT_FAMILY, 9, "bold"), labelanchor="nw") |
| 6420 | + box3.pack(fill="x", padx=24, pady=6, ipady=6) |
| 6421 | + |
| 6422 | + head = tk.Frame(box3, bg=UI["panel"]) |
| 6423 | + head.pack(fill="x", padx=10, pady=(6, 0)) |
| 6424 | + tk.Label(head, text="%s %s" % (APP_NAME, APP_VERSION), bg=UI["panel"], |
| 6425 | + fg=UI["accent"], |
| 6426 | + font=(FONT_FAMILY, 11, "bold")).pack(side="left") |
| 6427 | + tk.Label(head, text=" (%s)" % build_kind(), bg=UI["panel"], |
| 6428 | + fg="#8A93A5", font=(FONT_FAMILY, 9)).pack(side="left") |
| 6429 | + |
| 6430 | + tk.Label(box3, text=running_from(), bg=UI["panel"], fg=UI["text"], |
| 6431 | + font=(MONO_FAMILY, 8), anchor="w", justify="left", |
| 6432 | + wraplength=430).pack(anchor="w", padx=10, pady=(2, 0)) |
| 6433 | + tk.Label(box3, |
| 6434 | + text="last changed %s Python %s, Tk %s, %s %s" |
| 6435 | + % (build_stamp(), platform.python_version(), |
| 6436 | + tk.TkVersion, platform.system(), platform.release()), |
| 6437 | + bg=UI["panel"], fg="#8A93A5", font=(FONT_FAMILY, 8), |
| 6438 | + anchor="w").pack(anchor="w", padx=10) |
| 6439 | + tk.Label(box3, text="%d blocks loaded" % len(SPECS), bg=UI["panel"], |
| 6440 | + fg="#8A93A5", font=(FONT_FAMILY, 8), |
| 6441 | + anchor="w").pack(anchor="w", padx=10) |
| 6442 | + |
| 6443 | + row3 = tk.Frame(box3, bg=UI["panel"]) |
| 6444 | + row3.pack(fill="x", padx=10, pady=(8, 2)) |
| 6445 | + self.update_btn = tk.Button(row3, text="Check for updates", |
| 6446 | + command=self.check_updates, relief="flat", |
| 6447 | + bd=0, bg=UI["accent"], fg="#FFFFFF", |
| 6448 | + activebackground="#3373CC", |
| 6449 | + activeforeground="#FFFFFF", |
| 6450 | + font=(FONT_FAMILY, 9, "bold"), |
| 6451 | + cursor="hand2", padx=12, pady=3) |
| 6452 | + self.update_btn.pack(side="left") |
| 6453 | + tk.Button(row3, text="Copy these details", command=self.copy_details, |
| 6454 | + relief="flat", bd=0, bg="#EEF1F6", fg=UI["text"], |
| 6455 | + font=(FONT_FAMILY, 8), cursor="hand2", |
| 6456 | + padx=8).pack(side="left", padx=6) |
| 6457 | + tk.Button(row3, text="Open the project page", command=self.open_repo, |
| 6458 | + relief="flat", bd=0, bg="#EEF1F6", fg=UI["text"], |
| 6459 | + font=(FONT_FAMILY, 8), cursor="hand2", |
| 6460 | + padx=8).pack(side="left") |
| 6461 | + self.update_state = tk.Label(box3, text="", bg=UI["panel"], |
| 6462 | + fg="#8A93A5", font=(FONT_FAMILY, 8), |
| 6463 | + anchor="w", justify="left", |
| 6464 | + wraplength=430) |
| 6465 | + self.update_state.pack(anchor="w", padx=10, pady=(2, 0)) |
| 6466 | + |
6347 | 6467 | buttons = tk.Frame(top, bg=UI["panel"]) |
6348 | 6468 | buttons.pack(pady=(10, 18)) |
6349 | 6469 | tk.Button(buttons, text="Cancel", command=top.destroy, relief="flat", |
@@ -6386,6 +6506,71 @@ def refresh(self): |
6386 | 6506 | self.make_btn.configure(text="Repair the venv" if exists |
6387 | 6507 | else "Create the venv now") |
6388 | 6508 |
|
| 6509 | + # -- about this copy ---------------------------------------------------- # |
| 6510 | + |
| 6511 | + def copy_details(self): |
| 6512 | + self.top.clipboard_clear() |
| 6513 | + self.top.clipboard_append(build_summary()) |
| 6514 | + self.update_state.configure(text="Copied. Paste it into a bug report.") |
| 6515 | + |
| 6516 | + def open_repo(self): |
| 6517 | + import webbrowser |
| 6518 | + try: |
| 6519 | + webbrowser.open(REPO_URL) |
| 6520 | + except Exception: |
| 6521 | + self.update_state.configure(text=REPO_URL) |
| 6522 | + |
| 6523 | + def check_updates(self): |
| 6524 | + """Ask GitHub what the newest release is. Only ever when asked.""" |
| 6525 | + self.update_btn.configure(state="disabled", text="Checking...") |
| 6526 | + self.update_state.configure(text="Asking github.com...") |
| 6527 | + |
| 6528 | + def worker(): |
| 6529 | + tag, error = latest_release() |
| 6530 | + self.app.ui(lambda: self.show_update(tag, error)) |
| 6531 | + threading.Thread(target=worker, daemon=True).start() |
| 6532 | + |
| 6533 | + def show_update(self, tag: str, error: str): |
| 6534 | + try: |
| 6535 | + self.update_btn.configure(state="normal", text="Check for updates") |
| 6536 | + except Exception: |
| 6537 | + return # the dialog was closed while we waited |
| 6538 | + if error: |
| 6539 | + self.update_state.configure( |
| 6540 | + fg="#B36B00", |
| 6541 | + text="Could not reach github.com (%s). You can look at\n%s" |
| 6542 | + % (error.split(":")[0], RELEASES_URL)) |
| 6543 | + return |
| 6544 | + newest = version_tuple(tag) |
| 6545 | + mine = version_tuple(APP_VERSION) |
| 6546 | + if newest > mine: |
| 6547 | + self.update_state.configure( |
| 6548 | + fg="#B36B00", |
| 6549 | + text="Version %s is out. You have %s.\nDownload it from %s" |
| 6550 | + % (tag.lstrip("vV"), APP_VERSION, RELEASES_URL)) |
| 6551 | + if messagebox.askyesno( |
| 6552 | + "Update available", |
| 6553 | + "ScratchPy Studio %s is available and you have %s.\n\n" |
| 6554 | + "Open the download page?" % (tag.lstrip("vV"), APP_VERSION), |
| 6555 | + parent=self.top): |
| 6556 | + self.open_repo_releases() |
| 6557 | + elif newest < mine: |
| 6558 | + self.update_state.configure( |
| 6559 | + fg="#8A93A5", |
| 6560 | + text="You are running %s, which is newer than the published " |
| 6561 | + "%s." % (APP_VERSION, tag.lstrip("vV"))) |
| 6562 | + else: |
| 6563 | + self.update_state.configure( |
| 6564 | + fg="#2E9E5B", |
| 6565 | + text="Up to date. %s is the newest version." % APP_VERSION) |
| 6566 | + |
| 6567 | + def open_repo_releases(self): |
| 6568 | + import webbrowser |
| 6569 | + try: |
| 6570 | + webbrowser.open(RELEASES_URL) |
| 6571 | + except Exception: |
| 6572 | + pass |
| 6573 | + |
6389 | 6574 | def browse(self): |
6390 | 6575 | chosen = filedialog.askdirectory(title="Where should the venv live?", |
6391 | 6576 | parent=self.top) |
@@ -7682,6 +7867,9 @@ def main(argv: Optional[List[str]] = None) -> int: |
7682 | 7867 | if target and target.startswith("-"): |
7683 | 7868 | target = None |
7684 | 7869 | return MCPServer(target).serve() |
| 7870 | + if "--version" in args or "-V" in args: |
| 7871 | + print(build_summary()) |
| 7872 | + return 0 |
7685 | 7873 | if "--selftest" in args: |
7686 | 7874 | return selftest() |
7687 | 7875 | if "--make-icons" in args: |
|
0 commit comments