diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 0000000..43f4562 --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,83 @@ +# `.agents/` — agent working docs for GGate + +Index of the working docs and reference material agents use on GGate +(logic-circuit simulator, **GTK 4 + libadwaita**, heavy **Cairo** canvas). + +## Directory map + +- `TODO.md` — working backlog (P-levels; Done at the bottom). +- `plans/` — forward-looking implementation plans (icons). +- `skills/` — actionable how-to: `developing-gtk-apps.md` (architecture/plumbing), + `designing-gnome-ui.md` (HIG), `glc-format/SKILL.md` (authoring `.glc` circuits; + self-contained, portable to external models). +- `reference/` — `delegation-brief.md` (read first when delegating: repo facts, + conventions, verification) and `build-system-analysis.md` (build audit). +- `gtk4/` — raw GTK4/libadwaita/Pango/Gio/Cairo API docs by namespace; index at + `gtk4/README.md`. + +## GTK4 documentation database — how to use + +Scoped to the widgets/APIs this project actually uses, not the whole toolkit, so an +agent can continue UI work without re-deriving it from scratch. + +1. **Architecture / plumbing question** (lifecycle, threading, GSettings, actions, + packaging, "app crashes/freezes") → `skills/developing-gtk-apps.md`. +2. **UI / widget choice / HIG / "which widget, how to lay it out"** → + `skills/designing-gnome-ui.md`. +3. **Exact API of a widget already chosen** (constructor, methods, signals, gotchas) + → the relevant file in `gtk4/`. Start at `gtk4/README.md` for the index. + +Always prefer these over guessing. For anything version-sensitive or not covered, +confirm against the canonical docs: + +- Libadwaita 1.x: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/ +- GTK 4: https://docs.gtk.org/gtk4/ +- PyGObject: https://api.pygobject.gnome.org/ • PyCairo: https://pycairo.readthedocs.io/ + +## Project context (why these widgets) + +- **The canvas is the app.** `Gtk.DrawingArea` + Cairo draw every gate, wire, and the + new timing-waveform graph. `gtk4/drawing-cairo.md` is the most load-bearing file here. +- **Input = event controllers**, not legacy signals: `GestureClick`, + `EventControllerMotion/Key/Focus` drive placing/dragging/selecting components. +- **Dialogs are migrating to libadwaita**: the rewritten `PropertyWindow` + (`ggate/Components/Windows/Properties.py`) uses `Adw.Dialog` + boxed-list rows + + `Adw.Banner` for inline validation. See `gtk4/adwaita/`. +- **Text on canvas = Pango**, measured/rendered through Cairo. See `gtk4/pango/`. + +## Layout + +`gtk4/` holds the **raw canonical HTML reference pages** (one per class), fetched from +the official docs — read them directly. They are grouped by namespace: + +| Path | Source | Pages | +|------|--------|-------| +| `gtk4/README.md` | (navigation index) widget → where used in GGate → which file | — | +| `gtk4/adwaita/` | libadwaita 1.x — gnome.pages.gitlab.gnome.org/libadwaita | 17: Dialog, Application(Window), Preferences{Page,Group}, EntryRow, SpinRow, ComboRow, SwitchRow, Banner, StatusPage, HeaderBar, AboutDialog, AlertDialog, ResponseAppearance, Toast, ToastOverlay | +| `gtk4/gtk/` | GTK 4 — docs.gtk.org/gtk4 | 35: DrawingArea, Box, Label, Image, ScrolledWindow, ListBox(Row), Frame, Paned, Button, MenuButton, ToggleButton, SpinButton, CheckButton, ComboBox(Text), Color/FontDialog(Button), PopoverMenu, Gesture/EventController*, FileDialog, FileFilter, StringList, ListStore, CellRendererText, Builder, Window, HeaderBar, ActionBar | +| `gtk4/pango/` | Pango — docs.gtk.org/Pango | 2: Layout, FontDescription | +| `gtk4/gio/` | GIO — docs.gtk.org/gio | 2: SimpleAction, ListStore | +| `gtk4/cairo/` | PyCairo — pycairo.readthedocs.io | 4: context, surfaces, patterns, matrix | + +`skills/` holds three installable skills as authored guidance (not raw docs): + +| File | Covers | +|------|--------| +| `skills/developing-gtk-apps.md` | App architecture, lifecycle, threading, GSettings, packaging | +| `skills/designing-gnome-ui.md` | GNOME HIG, widget selection, UI polish | +| `skills/glc-format/SKILL.md` | GGate GLC circuit file format specification and catalog | + +> Pages are full HTML (nav chrome included) — read the class's *Constructors / Methods / +> Properties / Signals* sections. To refresh or add a class, curl the canonical URL, e.g. +> `curl -sL https://docs.gtk.org/gtk4/class..html -o gtk4/gtk/.html` +> (libadwaita: `.../libadwaita/doc/1-latest/class..html`). + +## Cross-cutting rules (do not violate) + +- **GTK is single-threaded.** Touch widgets only on the main thread; marshal with + `GLib.idle_add(...)`. (Cairo draw funcs already run on the main thread.) +- **GTK 4 has no `show_all()`** — widgets are visible by default; use `set_visible()`. +- **No `container.add()`** — use `box.append()`, `set_child()`, `Adw.PreferencesGroup.add()`. +- **Dialogs**: prefer `Adw.Dialog`/`Adw.AlertDialog` (adaptive, `.present(parent)`) over + `Gtk.Dialog`/`Gtk.MessageDialog` (deprecated in GTK 4.10). +- **File chooser**: prefer async `Gtk.FileDialog` (4.10+) over `Gtk.FileChooserDialog`. diff --git a/.agents/TODO.md b/.agents/TODO.md new file mode 100644 index 0000000..bc524b9 --- /dev/null +++ b/.agents/TODO.md @@ -0,0 +1,115 @@ +# GGate — working backlog + +Status legend: `[ ]` open · `[~]` in progress · `[x]` done. **(verify)** = confirm +against current code first. Owner model: Claude specs → agy implements → Claude reviews. +Companion docs: `reference/build-system-analysis.md`, `reference/delegation-brief.md`, +`plans/`, `skills/glc-format/SKILL.md`. + +## P1 — polish + +- [ ] **Guard against over-large circuits.** Warn (and optionally block) when loading a circuit + too large to handle, and warn when a save/compress or simulation will take too long, instead of + hanging/OOM-ing. (Run-time hang is mitigated by the non-blocking sim + the >15s log, but there's + no up-front guard.) +- [ ] Style: residual 2-vs-4-space indent, mixed quotes in older files (new work follows: double quotes, comments per the `comment-style` memory). + +## Build & packaging + +- [ ] **Flatpak** — decouple pip deps from network (`flatpak-pip-generator`) for offline build. +- [ ] **Snapcraft** — remove remote `curl|python3` scripts; drop hardcoded prefix. +- [ ] Relax `shapely` pin / pyproject cleanup (partly done — confirm). + +## P3 — feature backlog + +- [ ] Auto-scroll the timing diagram to follow the latest timestamp while running (deferred from + the timing plan §6 Q1). +- [ ] **Playback follow-ups (deferred).** Frame subsampling for very long timelines (so playback + holds the target duration instead of running long at the 16ms floor); optional per-session + speed control beyond the global `playback_duration` preference; a "Computing…" affordance for + pathological circuits whose up-front compute is slow. + +## Inline TODOs + +- [ ] `Managers/Alerts.py:8`, `MainFrame.py` — i18n/translations. +- [ ] `Managers/FileManager.py:104` — implement schematics export. +- [ ] `MainFrame.py` — toast on clipboard parse failure. +- [ ] `DrawArea.py:516` — optimize redraw. + +## ✅ Done + +Earlier work (app launches, dialogs migrated, sim/crash fixes) — see git history +`a0c0edc`…`a078fb1`. Recent (this development cycle): + +- [x] **Animated simulation playback (record-then-play)** — replaced the throttle-the-engine + attempt with a clean two-phase model: `CircuitManager` computes the full timeline at engine + speed in non-blocking idle slices (`begin_compute`/`compute_slice`), then a new + `ggate/CircuitPlaybackAnimation.py` GObject paces playback over the recorded frames + (`apply_frame_at`), pacing to a target duration (`playback_duration` pref) clamped to + 16–200ms/frame. State machine `computing→playing⇄paused→ended/idle` via `playback-state-changed`. + Non-animating circuits (no time evolution, e.g. no oscillator) skip playback and just show the + settled state. See `.agents/reviews/circuitmanager-sim-animation-review.md` for the rejected + first attempt. +- [x] **Floating playback control bar** (`ggate/PlaybackControls.py`) — OSD pill overlaid bottom- + centre of the canvas (DrawArea reworked to a `Gtk.Overlay` wrapping its scroller). Stop · + Restart · Play/Pause │ seek scale · fixed-width sim-time readout. Shown only for animating + circuits, persists through the whole session, Stop ends the run via the header toggle. +- [x] **`playback_duration` preference** — new Simulation-page setting (`Preference.py`, + `Preferences.py`) controlling how long an animation takes to play back. +- [x] **Header simulation controls simplified to Play/Stop** — removed the redundant header Pause + button and the orphaned `pause_running_mode` path now that the floating bar owns transport. +- [x] **Live-edit recompute (switch/property desync fix)** — editing a property or toggling a + switch during a run now cancels playback and recomputes from t=0 via a shared + `MainFrame.recompute_simulation()`; switch toggles persist into their initial-state value so a + from-scratch recompute holds them. +- [x] **Icon system** (plan: `plans/icon-creation.md`) — hand-drawn component SVGs in two + variants (ANSI/IEC), gresource→freedesktop layout with aliases, `IconTheme.add_resource_path` + at startup, `ComponentView`/`MenuPopover` switched to icon-name lookups, `_get_icon_path` + deleted. Includes new themed 7-segment and LED icons. +- [x] **Fix gresource icon layout** — tidied the dev XML's double-"actions" nesting/misfiled + components into a clean freedesktop layout, dev + installed in sync. +- [x] **PIPO clock-bubble draw bug** — `PIPOShiftRegister.py` drew the clock trigger bubble at + the `CK` coordinate (`outpin_b + 40`) instead of `SL` (`outpin_b + 20`). +- [x] **Generate `config.py` via meson** (`configure_file`) instead of hardcoded VERSION/paths, + keeping the dev `run.py` path working. +- [x] **Desktop file + AppStream install via meson** — meson installs the renamed + desktop/metadata, not just legacy `setup.py`. +- [x] **Unify dev vs installed resource source** — dev XML generated by `run.py`, installed by + `resources.xml.in` + meson glob. +- [x] **Timing diagram width cap warning** — added a warning banner (`Adw.Banner`) in the timing graph dialog to notify the user when the diagram width is capped at Cairo's limit. +- [x] **First-paint centering decoupled** — fixed `ggate/DrawArea.py` and `ggate/MainFrame.py` so that initial canvas viewport centering is always performed on file/buffer load, even when the resize auto-centering preference is disabled. +- [x] **translator-credits & header cleanups** — cleaned up the commented-out translator-credits block in `MainFrame.py` and removed the stale `indent-tabs-mode` editor directives from 8 Python files. + +- [x] **Timing diagram visual polish** — increased waveform amplitude within each row (from 20px to 28px), auto-fit/default the time range to the actual extent of simulation history, calculated a saner default zoom for high-frequency signals to avoid picket-fences, and fixed dead vertical space by drawing backgrounds down the full allocated height. + +- [x] **Property window widget reuse & leak fix** — optimized `PropertyWindow` (`ggate/Components/Windows/Properties.py`) to build its toolbar view, header bar, and banner once on initialization and reuse them, avoiding widget leaks. Also removed the redundant section header from Seven Segment LED properties. + +- [x] **Remove GTK3 leftovers & dead exporter stub** — deleted dead GTK3 leftover files `ggate/TimingDiagramWindow.py` and `ggate/DiagramArea.py`, and the dead exporter stub `ggate/Components/Managers/Exporter.py`. + +- [x] **Header bar migrated to Adw.HeaderBar** — replaced Gtk.HeaderBar with Adw.HeaderBar for standard Adwaita layout, positioning menu buttons and simulation controls on the right. +- [x] **AlertDialogs duplicate response fixed** — refactored class to dynamically instantiate Adw.AlertDialog instead of subclassing it to avoid registering same response IDs multiple times. +- [x] **Theme Light/Dark Variants** — renamed existing dark themes (Frappé, Mocha, Space) to (Dark) and created new (Light) variant JSON files: FrappeLight.json (using Catppuccin Latte), MochaLight.json (using Catppuccin Latte/Light variant), and SpaceLight.json (using Space Light variant). +- [x] **Zoom limit restrictions** — restricted zoom level range on the canvas to between 0.5 and 20.0 (in ggate/DrawArea.py). +- [x] **GLC file serialization and parsing zoom/positions** — added a `meta` block to output and restore the current canvas zoom level on load, and cleaned up coordinate serialization/parsing using comprehensions and float string handling. +- [x] **Preferences font picker** — completed typography section with monospace font filtering. +- [x] **Timing diagram revamp** — `Display.py` (`Adw.Dialog` + `ToolbarView`, scale/range/ + save toolbar) and `Diagram.py` (synced scrolls, pure `set_draw_func` rendering, gesture + scrubbing, empty-history placeholder) rewritten; renders waveforms theme-aware. +- [x] **Theming system** — built-in themes (Classic/Space/Frappé/Mocha) as JSON in + `data/themes/`, globbed into the gresource and discovered dynamically at runtime; + `Themes.py` registry + `apply_theme`/`apply_chrome`; theme picker in Preferences. +- [x] **Preferences revamp** — migrated to `Adw.PreferencesDialog` with multiple pages + (Appearance/Theme/Simulation/Canvas) and Adw rows. +- [x] **Theme picker fixed** — model rebuilt from registry (chevron shows), selection no + longer reverts to Custom. +- [x] **Oscillator timing + gate sim safety** — `analyze_logic` runs at run start so + oscillators populate history; gates compute via `all/any/sum` (no IndexError on + unconnected pins); `.glc` property types load correctly (`parse_section`). +- [x] **Dev gresource never stale** — `run.py` regenerates + compiles `dev-resources.gresource` + from the asset dirs each launch. +- [x] **Build portability** — non-portable `find|sed`/inline-python globbing → shell helpers; + `prefix` shadowing fixed; `RUNNING_FROM_SOURCE` replaces vague `DEV_MODE`. +- [x] **CI** — Fedora-container GitHub Actions workflow produces a `meson dist` `tar.gz`. +- [x] **About dialog wired** (`on_action_about_pressed` → `AboutGGate.create().present`). +- [x] **Version unified** — `config.py`/`__init__.py`/`pyproject`/`meson` all `5.0.0`. +- [x] **bin/ggate.in** imports + version string correct. +- [x] **`.glc` format spec** documented (`skills/glc-format/SKILL.md`). diff --git a/.agents/plans/icon-creation.md b/.agents/plans/icon-creation.md new file mode 100644 index 0000000..8a6e8fa --- /dev/null +++ b/.agents/plans/icon-creation.md @@ -0,0 +1,670 @@ +# GGate Icon Creation and Asset Management Plan + +This document outlines the strategy for clean, scalable, and standardized icon management in GGate (GTK4 + libadwaita). It addresses existing asset path issues, missing icons, and provides a programmatic workflow for generating component icons directly from the canvas rendering code. + +--- + +## 1. Executive Summary & Core Recommendation + +GGate currently suffers from a split asset-delivery model: +1. **Direct Filesystem Lookups:** The side palette ([ComponentView.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/ComponentView.py)) and context menus ([MenuPopover.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/MenuPopover.py)) resolve absolute paths relative to local directories, which breaks both inside Python virtual environments and installed system prefixes (e.g., `/usr/lib/python3/...`). +2. **Non-Standard GResource Structure:** The resource compiler config in [dev-resources.xml](file:///Users/ekureedem/Documents/Projects/GGate/data/images/dev-resources.xml) bundles icons under a non-standard hierarchy (`/data/icons/scalable/actions/`), misfiling components under actions and bypassing GTK's native `Gtk.IconTheme` matching engine. +3. **PNG/SVG Mixture & Missing Assets:** Component icons are a mix of PNGs and SVGs. Important toolbar assets (like the specific net connector drawing icon) are missing, forcing fallback placeholders. + +### Recommendation: Programmatic Cairo SVG Generation + GResource Theme Mapping +* **For Logic Gates:** Rather than manually drawing 25+ logic gate SVGs twice (for ANSI and IEC styles), we will write a dev-time script (`bin/generate-gate-icons.py`) that instantiates each gate component, evaluates its bounding box, mocks a Cairo `cairo.SVGSurface`, and invokes `drawComponent`. This ensures **100% accuracy**, zero maintenance overhead, and instant updates when canvas drawing code changes. +* **For Infrastructure:** Restructure the resource compile targets using GResource `alias` attributes to conform to the Freedesktop directory spec (`hicolor/scalable/actions` and `hicolor/scalable/devices`). Register the resource base with `Gtk.IconTheme` at startup, and switch all references to standard icon-name lookups (`Gtk.Image.new_from_icon_name`). + +--- + +## 2. Complete Icon Inventory + +### A. Logic Gate & Component Palette Icons +These represent the placeable circuit elements loaded in the side palette. Currently, they reside in `data/images/components/`. + +| Component Key | Component Name | Current State | Target Format | Generation Style / Notes | +|---|---|---|---|---| +| `definitions.component_NOT` | `not` | PNG only (`not.png`) | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_AND` | `and` | PNG & SVG | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_OR` | `or` | PNG & SVG | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_XOR` | `xor` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_NAND` | `nand` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_NOR` | `nor` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_tribuff` | `tribuff` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_SW` | `sw` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_7seg` | `7seg` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_LED` | `led` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_VDD` | `vdd` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_GND` | `gnd` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_OSC` | `osc` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_probe` | `probe` | PNG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_text` | `text` | PNG & SVG | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_RSFF` | `rsff` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_JKFF` | `jkff` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_DFF` | `dff` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_TFF` | `tff` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_counter` | `counter` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_adder` | `adder` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_SISO` | `siso` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_SIPO` | `sipo` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_PISO` | `piso` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | +| `definitions.component_PIPO` | `pipo` | SVG only | SVG (Symbolic) | Programmatic (ANSI & IEC) | + +### B. Toolbar & Action Icons +These are used for editor manipulation and toolbar commands in [MainFrame.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/MainFrame.py). + +| Icon Role | Reference Name | Source Type | Current State | Target State / Fix | +|---|---|---|---|---| +| **Rotate Left** | `rotate-left-symbolic` | Custom SVG | SVG (`actions/rotate-left.svg`) | Modernized 16x16 viewbox symbolic | +| **Rotate Right** | `rotate-right-symbolic` | Custom SVG | SVG (`actions/rotate-right.svg`) | Modernized 16x16 viewbox symbolic | +| **Flip Horizontally** | `flip-horizontal-symbolic` | Custom SVG | SVG (`actions/flip-horizontal.svg`) | Modernized 16x16 viewbox symbolic | +| **Flip Vertically** | `flip-vertical-symbolic` | Custom SVG | SVG (`actions/flip-vertical.svg`) | Modernized 16x16 viewbox symbolic | +| **Add Net** | `add-net-symbolic` | Custom SVG | **Missing** (uses fallback `list-add`) | Create custom SVG showing wire symbol | +| **Simulation Start** | `media-playback-start-symbolic` | System | Standard theme icon | Keep standard Freedesktop icon | +| **Simulation Pause** | `media-playback-pause-symbolic` | System | Standard theme icon | Keep standard Freedesktop icon | +| **Simulation Stop** | `media-playback-stop-symbolic` | System | Standard theme icon | Keep standard Freedesktop icon | +| **Open Menu** | `open-menu-symbolic` | System | Standard theme icon | Keep standard Freedesktop icon | + +### C. Application & Document Icons +Standard system integration assets. + +| Icon Role | Target Name | Size Directory | Current State | Target State | +|---|---|---|---|---| +| **App Launcher Icon** | `org.astralco.ggate.svg` | `scalable/apps` | SVG (`scalable/ggate.svg`) | Scalable SVG (high-res compliant) | +| **App Launcher Icon** | `org.astralco.ggate.png` | `apps/[size]x[size]` | PNGs (`apps/[size]/ggate.png`) | Regenerated from scalable target | +| **GGate Document Icon** | `application-x-ggate-circuit-symbolic.svg` | `scalable/mimetypes` | **Missing** (only PNGs exist) | Scalable SVG representing circuit file | +| **GGate Document Icon** | `application-x-ggate-circuit.png` | `mimetypes/[size]x[size]` | PNGs (`mime/[size]/text-glc.png`) | Rename & regenerate to match MIME spec | + +### D. Per-Component Visual Specifications + +This subsection provides the ground-truth visual specs derived directly from each component's canvas drawing implementation (`drawComponent`, `drawComponentEditOverlap`, etc.). + +#### 1. `not` (NOT Gate) +* **Source File:** [NOT.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/NOT.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 90, 0]` +* **Body Silhouette:** + * **ANSI (distinctive):** Right-pointing triangle with vertices `(30, -35)`, `(30, -5)`, and `(60, -20)`. + * **IEC (rectangular):** Rectangle starting at `(30, -40)` with width `30` and height `40`. +* **Distinguishing Marks:** + * **Both styles:** Inversion bubble (circle) centered at `(66, -20)` with radius `6` (extending `x` from `60` to `72`). + * **IEC Only:** Centered internal text `"1"` at `(45, -40)` (align: `0.5`, `0.0`). +* **Terminals & Pins:** + * **Input:** 1 pin at `(10, -20)`. Stub: `(10, -20)` to `(30, -20)`. + * **Output:** 1 pin at `(90, -20)`. Stub: `(72, -20)` to `(90, -20)` (connecting to the edge of the inversion bubble). +* **Smart Crop (Body only):** `[30, -40, 72, 0]` + +#### 2. `and` (AND Gate) +* **Source File:** [AND.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/AND.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 100, 0]` +* **Body Silhouette:** + * **ANSI (distinctive):** D-shape. Flat back segment from `(30, -40)` to `(30, 0)`. Parallel top/bottom lines extending to `(60, -40)` and `(60, 0)`. Front semi-circle centered at `(60, -20)` with radius `20`. + * **IEC (rectangular):** Rectangle starting at `(30, -40)` with width `50` and height `40`. +* **Distinguishing Marks:** + * **IEC Only:** Centered internal text `"&"` at `(55, -40)` (align: `0.5`, `0.0`). +* **Terminals & Pins:** + * **Inputs (2 or 3):** + * 2-pin: `(10, -30)` and `(10, -10)`. Stubs: `(10, -30)` to `(30, -30)`, `(10, -10)` to `(30, -10)`. + * 3-pin: Adds `(10, -20)`. Stub: `(10, -20)` to `(30, -20)`. + * **Output:** 1 pin at `(100, -20)`. Stub: `(80, -20)` to `(100, -20)`. +* **Smart Crop (Body only):** `[30, -40, 80, 0]` + +#### 3. `or` (OR Gate) +* **Source File:** [OR.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/OR.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 100, 0]` +* **Body Silhouette:** + * **ANSI (distinctive):** Shield-like shape pointing right. + - Curved back edge: Bezier curve from `(30, -40)` to `(30, 0)` with control points `(40, -30)` and `(40, -10)`. + - Curved top edge: Bezier from `(30, -40)` to tip `(80, -20)` with control points `(50, -40)` and `(65, -40)`. + - Curved bottom edge: Bezier from `(30, 0)` to tip `(80, -20)` with control points `(50, 0)` and `(65, 0)`. + * **IEC (rectangular):** Rectangle starting at `(30, -40)` with width `50` and height `40`. +* **Distinguishing Marks:** + * **IEC Only:** Centered internal text `"≥1"` at `(55, -40)` (align: `0.5`, `0.0`). +* **Terminals & Pins:** + * **Inputs (2 or 3):** + * 2-pin: `(10, -30)` and `(10, -10)`. Stubs: ANSI to `(36, -30)` / `(36, -10)`, IEC to `(30, -30)` / `(30, -10)`. + * 3-pin: Adds `(10, -20)`. Stubs: ANSI to `(36, -30)` / `(37, -20)` / `(36, -10)`, IEC to `(30, y)`. + * **Output:** 1 pin at `(100, -20)`. Stub: `(80, -20)` to `(100, -20)`. +* **Smart Crop (Body only):** `[30, -40, 80, 0]` + +#### 4. `xor` (XOR Gate) +* **Source File:** [XOR.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/XOR.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 100, 0]` +* **Body Silhouette:** + * **ANSI (distinctive):** Same shield shape as OR, plus an extra curved line offset at the input back: Bezier from `(25, -40)` to `(25, 0)` with control points `(35, -30)` and `(35, -10)`. + * **IEC (rectangular):** Rectangle starting at `(30, -40)` with width `50` and height `40`. +* **Distinguishing Marks:** + * **ANSI Only:** Extra curved input back line. + * **IEC Only:** Centered internal text `"=1"` at `(55, -40)` (align: `0.5`, `0.0`). +* **Terminals & Pins:** + * **Inputs (2 or 3):** Same connections/stubs as OR. + * **Output:** 1 pin at `(100, -20)`. Stub: `(80, -20)` to `(100, -20)`. +* **Smart Crop (Body only):** `[25, -40, 80, 0]` + +#### 5. `nand` (NAND Gate) +* **Source File:** [NAND.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/NAND.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 110, 0]` +* **Body Silhouette:** + * **ANSI (distinctive):** Same D-shape as AND. + * **IEC (rectangular):** Rectangle starting at `(30, -40)` with width `50` and height `40`. +* **Distinguishing Marks:** + * **ANSI Only:** Inversion bubble (circle) at `(86, -20)` with radius `6` (extends `x` from `80` to `92`). + * **IEC Only:** Centered internal text `"&"` at `(55, -40)` (align: `0.5`, `0.0`) and output diagonal inversion stroke from `(80, -28)` to `(92, -20)`. +* **Terminals & Pins:** + * **Inputs (2 or 3):** Same as AND. + * **Output:** 1 pin at `(110, -20)`. Stub: `(92, -20)` to `(110, -20)`. +* **Smart Crop (Body only):** `[30, -40, 92, 0]` + +#### 6. `nor` (NOR Gate) +* **Source File:** [NOR.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/NOR.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 110, 0]` +* **Body Silhouette:** + * **ANSI (distinctive):** Same shield shape as OR. + * **IEC (rectangular):** Rectangle starting at `(30, -40)` with width `50` and height `40`. +* **Distinguishing Marks:** + * **ANSI Only:** Inversion bubble (circle) at `(86, -20)` with radius `6` (extends `x` from `80` to `92`). + * **IEC Only:** Centered internal text `"≥1"` at `(55, -40)` (align: `0.5`, `0.0`) and output diagonal inversion stroke from `(80, -28)` to `(92, -20)`. +* **Terminals & Pins:** + * **Inputs (2 or 3):** Same as OR. + * **Output:** 1 pin at `(110, -20)`. Stub: `(92, -20)` to `(110, -20)`. +* **Smart Crop (Body only):** `[30, -40, 92, 0]` + +#### 7. `tribuff` (Tri-State Buffer) +* **Source File:** [TSB.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/TSB.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 90, 0]` (*Note: Control pin stub vertically extends to `y = -60`*). +* **Body Silhouette:** + * **ANSI / All styles:** Right-pointing triangle with vertices `(30, -35)`, `(30, -5)`, and `(60, -20)`. + * *Note: The code does not define a separate IEC variant (the triangle is drawn only when `Preference.symbol_type == 0`)*. +* **Distinguishing Marks:** + * **Active Low Control:** Circle (control bubble) at `(40, -35)` with radius `4` (if triggered on low level). + * **Inversion Output:** Circle (bubble) at `(64, -20)` with radius `4` (if inverted output). +* **Terminals & Pins:** + * **Data Input:** 1 pin at `(10, -20)`. Stub: `(10, -20)` to `(30, -20)`. + * **Control (Enable):** 1 pin at `(40, -60)`. Stub: `(40, -60)` to `(40, -38)` (active low) or `(40, -30)` (active high). + * **Output:** 1 pin at `(80, -20)`. Stub: `(68, -20)` to `(80, -20)` (inverted) or `(58, -20)` to `(80, -20)` (non-inverted). +* **Smart Crop (Body only):** `[30, -40, 68, 0]` + +#### 8. `sw` (Switch) +* **Source File:** [SW.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/SW.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 70, 0]` +* **Body Silhouette:** + * Rectangle starting at `(10, -40)` with width `40` and height `40`. +* **Distinguishing Marks:** + * Horizontal inner track rectangle: `(15, -30)` with width `30` and height `10`. + * Centered internal text labels: `"L"` at `(20, -10)` and `"H"` at `(40, -10)` (align: `0.5`, `0.5`). + * Slide handle: Outlined rectangle at `(17, -28)` of size `12x6` (Edit mode / Low state) or filled rectangle at `(30.5, -28.5)` of size `13x7` (High state in Run mode). +* **Terminals & Pins:** + * **Output:** 1 pin at `(70, -20)`. Stub: `(50, -20)` to `(70, -20)`. + * **Inputs:** None. +* **Smart Crop (Body only):** `[10, -40, 50, 0]` + +#### 9. `7seg` (Seven-Segment Display) +* **Source File:** [SevenSegment.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/StateViewers/SevenSegment.py) +* **Bounding Box (`comp_rect`):** `[10, -80, 100, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -80)` with width `70` and height `80`. +* **Distinguishing Marks:** + * Centered 7-segment display layout, with segments drawn as polygons: + - **a (top):** `(60, -70), (62, -68), (88, -68), (90, -70), (88, -72), (62, -72)` + - **b (top-right):** `(90, -70), (88, -68), (88, -42), (90, -40), (92, -42), (92, -68)` + - **c (bottom-right):** `(90, -40), (88, -38), (88, -12), (90, -10), (92, -12), (92, -38)` + - **d (bottom):** `(60, -10), (62, -8), (88, -8), (90, -10), (88, -12), (62, -12)` + - **e (bottom-left):** `(60, -40), (58, -38), (58, -12), (60, -10), (62, -12), (62, -38)` + - **f (top-left):** `(60, -70), (58, -68), (58, -42), (60, -40), (62, -42), (62, -68)` + - **g (middle):** `(60, -40), (62, -38), (88, -38), (90, -40), (88, -42), (62, -42)` + - Centered internal labels on left: `"IA"`, `"IB"`, `"IC"`, and `"ID"` at `(35, -70)`, `(35, -50)`, `(35, -30)`, and `(35, -10)` (align: `0.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** 4 pins at `(10, -70)`, `(10, -50)`, `(10, -30)`, and `(10, -10)`. Stubs go from `(10, y)` to `(30, y)`. + - **Outputs:** None. +* **Smart Crop (Body only):** `[30, -80, 100, 0]` + +#### 10. `led` (LED Display) +* **Source File:** [LED.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/StateViewers/LED.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 70, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -40)` with width `40` and height `40`. +* **Distinguishing Marks:** + * Centered inner circular LED element at `(50, -20)` with radius `8`. +* **Terminals & Pins:** + - **Input:** 1 pin at `(10, -20)`. Stub goes from `(10, -20)` to `(30, -20)`. + - **Outputs:** None. +* **Smart Crop (Body only):** `[30, -40, 70, 0]` + +#### 11. `vdd` (VDD Rail) +* **Source File:** [VDD.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/VDD.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 30, 0]` +* **Body Silhouette:** + * Horizontal bar: `(10, -20)` to `(30, -20)`. + * Tiny square dot at `(19, -21)` of size 2x2. +* **Distinguishing Marks:** + * Centered text label `"Vdd"` drawn above the bar at `(20, -30)` (align: `0.5`, `0.5`). +* **Terminals & Pins:** + - **Output:** 1 pin at `(20, 0)`. Stub drawn vertically from `(20, -20)` to `(20, 0)`. + - **Inputs:** None. +* **Smart Crop (Body only):** `[10, -40, 30, -20]` + +#### 12. `gnd` (GND Rail) +* **Source File:** [GND.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/GND.py) +* **Bounding Box (`comp_rect`):** `[10, -50, 30, 0]` +* **Body Silhouette:** + * Horizontal bar: `(10, -30)` to `(30, -30)`. +* **Distinguishing Marks:** + * Three parallel diagonal ground stripes extending down-left: + - Stripe 1: `(16, -30)` to `(12, -20)` + - Stripe 2: `(22, -30)` to `(18, -20)` + - Stripe 3: `(28, -30)` to `(24, -20)` + * Centered text label `"GND"` drawn below stripes at `(20, -10)` (align: `0.5`, `0.5`). +* **Terminals & Pins:** + - **Output:** 1 pin at `(20, -50)`. Stub drawn vertically from `(20, -50)` to `(20, -30)`. + - **Inputs:** None. +* **Smart Crop (Body only):** `[10, -30, 30, 0]` + +#### 13. `osc` (Oscillator) +* **Source File:** [OSC.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Standard/OSC.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 70, 0]` +* **Body Silhouette:** + * Rectangle starting at `(10, -40)` with width `40` and height `40`. +* **Distinguishing Marks:** + * Inner square-wave glyph drawn at `y = -10` and `y = -30` levels: + - Path: `(15, -10) -> (20, -10) -> (20, -30) -> (25, -30) -> (25, -10) -> (30, -10) -> (30, -30) -> (35, -30) -> (35, -10) -> (40, -10) -> (40, -30) -> (45, -30)`. +* **Terminals & Pins:** + - **Output:** 1 pin at `(70, -20)`. Stub goes from `(50, -20)` to `(70, -20)`. + - **Inputs:** None. +* **Smart Crop (Body only):** `[10, -40, 50, 0]` + +#### 14. `probe` (Probe) +* **Source File:** [Probe.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Miscellaneous/Probe.py) +* **Bounding Box (`comp_rect`):** Dynamic: `[10, -20, 55 + label_width, 0]`. `label_width` defaults to minimum 12. +* **Body Silhouette:** + * Circle centered at `(40, -10)` with radius `10`. +* **Distinguishing Marks:** + * Centered internal text label `"V"` at `(40, -10)` (align: `0.5`, `0.5`). + * External text label: drawn starting at `(55, -10)` containing name string (align: `0.0`, `0.5`). +* **Terminals & Pins:** + - **Input:** 1 pin at `(10, -10)`. Stub goes from `(10, -10)` to `(30, -10)`. + - **Outputs:** None. +* **Smart Crop (Body only):** `[30, -20, 50, 0]` + +#### 15. `text` (Text Box) +* **Source File:** [Text.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Miscellaneous/Text.py) +* **Bounding Box (`comp_rect`):** Dynamic: `[10, -10 - height/2, 10 + width, -10 + height/2]`, minimum size 12x12. +* **Body Silhouette:** None (no body outline is drawn). +* **Distinguishing Marks:** + * User-specified text drawn starting at `(10, -10)` (align: `0.0`, `0.5`). +* **Terminals & Pins:** None. +* **Smart Crop (Body only):** Matches dynamic bounding box. + +#### 16. `rsff` (RS Flip-Flop) +* **Source File:** [RSFF.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/FlipFlops/RSFF.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 100, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -40)` with width `50` and height `40`. +* **Distinguishing Marks:** + * Centered internal text labels: + - `"S"` at `(35, -30)` (align: `0.0`, `0.5`). + - `"R"` at `(35, -10)` (align: `0.0`, `0.5`). + - `"Q"` at `(75, -30)` (align: `1.0`, `0.5`). + - `"~Q"` at `(75, -10)` (align: `1.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** 2 pins: `(10, -30)` (`S`) and `(10, -10)` (`R`). Stubs go from `(10, y)` to `(30, y)`. + - **Outputs:** 2 pins: `(100, -30)` (`Q`) and `(100, -10)` (`~Q`). Stubs go from `(80, y)` to `(100, y)`. +* **Smart Crop (Body only):** `[30, -40, 80, 0]` + +#### 17. `jkff` (JK Flip-Flop) +* **Source File:** [JKFF.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/FlipFlops/JKFF.py) +* **Bounding Box (`comp_rect`):** `[10, -60, 110, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -60)` with width `60` and height `60`. +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, -30)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, -35), (40, -30), (30, -25)`. + * Centered internal text labels: + - `"J"` at `(45, -50)` (align: `0.0`, `0.5`). + - `"CK"` at `(45, -30)` (align: `0.0`, `0.5`). + - `"K"` at `(45, -10)` (align: `0.0`, `0.5`). + - `"Q"` at `(85, -50)` (align: `1.0`, `0.5`). + - `"~Q"` at `(85, -10)` (align: `1.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** 3 pins: `(10, -50)` (`J`), `(10, -30)` (`CK`), and `(10, -10)` (`K`). Stubs for `J` and `K` go to `(30, y)`. `CK` stub goes to `(30, -30)` (positive edge) or `(20, -30)` (negative edge). + - **Outputs:** 2 pins: `(110, -50)` (`Q`), `(110, -10)` (`~Q`). Stubs go from `(90, y)` to `(110, y)`. +* **Smart Crop (Body only):** `[30, -60, 90, 0]` + +#### 18. `dff` (D Flip-Flop) +* **Source File:** [DFF.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/FlipFlops/DFF.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 110, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -40)` with width `60` and height `40`. +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, -10)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, -15), (40, -10), (30, -5)`. + * Centered internal text labels: + - `"D"` at `(45, -30)` (align: `0.0`, `0.5`). + - `"CK"` at `(45, -10)` (align: `0.0`, `0.5`). + - `"Q"` at `(85, -30)` (align: `1.0`, `0.5`). + - `"~Q"` at `(85, -10)` (align: `1.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** 2 pins: `(10, -30)` (`D`) and `(10, -10)` (`CK`). Stub for `D` goes to `(30, -30)`. `CK` stub goes to `(30, -10)` (positive edge) or `(20, -10)` (negative edge). + - **Outputs:** 2 pins: `(110, -30)` (`Q`), `(110, -10)` (`~Q`). Stubs go from `(90, y)` to `(110, y)`. +* **Smart Crop (Body only):** `[30, -40, 90, 0]` + +#### 19. `tff` (T Flip-Flop) +* **Source File:** [TFF.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/FlipFlops/TFF.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 110, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -40)` with width `60` and height `40`. +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, -20)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, -25), (40, -20), (30, -15)`. + * Centered internal text labels: + - `"T"` at `(45, -20)` (align: `0.0`, `0.5`). + - `"Q"` at `(85, -30)` (align: `1.0`, `0.5`). + - `"~Q"` at `(85, -10)` (align: `1.0`, `0.5`). +* **Terminals & Pins:** + - **Input:** 1 pin at `(10, -20)` (`T`). Stub goes to `(30, -20)` (positive edge) or `(20, -20)` (negative edge). + - **Outputs:** 2 pins: `(110, -30)` (`Q`), `(110, -10)` (`~Q`). Stubs go from `(90, y)` to `(110, y)`. +* **Smart Crop (Body only):** `[30, -40, 90, 0]` + +#### 20. `counter` (Mod-N Counter) +* **Source File:** [ModNCounter.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Counters/ModNCounter.py) +* **Bounding Box (`comp_rect`):** Dynamic based on bits: `[10, min((outpin_t - 10, -60)), 120, max((outpin_b + 10, -20))]` (default 4 bits: `[10, -80, 120, 0]`). +* **Body Silhouette:** + * Rectangle starting at `(30, comp_rect[1])` with width `70` and height `comp_rect[3] - comp_rect[1]` (width 70, height 80 for default 4 bits). +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, -50)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, -55), (40, -50), (30, -45)`. + * Centered internal text labels: + - `"CK"` at `(45, -50)` (align: `0.0`, `0.5`). + - `"RST"` at `(45, -30)` (align: `0.0`, `0.5`). + - `"Q0"`, `"Q1"`, ... at `(95, y)` (align: `1.0`, `0.5`) for each output pin. +* **Terminals & Pins:** + - **Inputs:** 2 pins: `(10, -50)` (`CK`), `(10, -30)` (`RST`). `RST` stub goes to `(30, -30)`. `CK` stub goes to `(30, -50)` (positive edge) or `(20, -50)` (negative edge). + - **Outputs:** `bits` output pins: `(120, y)` for `y` in range `outpin_t` to `outpin_b` with step 20 (e.g. `[-70, -50, -30, -10]` for default 4 bits). Stubs go from `(100, y)` to `(120, y)`. +* **Smart Crop (Body only):** `[30, comp_rect[1], 100, comp_rect[3]]` + +#### 21. `adder` (Adder) +* **Source File:** [Calculator.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Calculator.py) +* **Bounding Box (`comp_rect`):** `[10, -40, 100, 0]` (Half Adder) or `[10, -60, 100, 0]` (Full Adder). +* **Body Silhouette:** + * Rectangle starting at `(30, -40)` with size 50x40 (Half) or starting at `(30, -60)` with size 50x60 (Full). +* **Distinguishing Marks:** + * Centered internal text labels: + - **Half Adder:** `"X"` at `(35, -30)`, `"Y"` at `(35, -10)`, `"C"` at `(75, -30)`, `"S"` at `(75, -10)`. + - **Full Adder:** `"X"` at `(35, -50)`, `"Y"` at `(35, -30)`, `"Z"` at `(35, -10)`, `"C"` at `(75, -50)`, `"S"` at `(75, -10)`. +* **Terminals & Pins:** + - **Inputs:** + - Half Adder (2 pins): `(10, -30)` (`X`), `(10, -10)` (`Y`). Stubs to `(30, y)`. + - Full Adder (3 pins): `(10, -50)` (`X`), `(10, -30)` (`Y`), `(10, -10)` (`Z`). Stubs to `(30, y)`. + - **Outputs:** 2 pins: + - Half Adder: `(100, -30)` (`C`), `(100, -10)` (`S`). Stubs from `(80, y)` to `(100, y)`. + - Full Adder: `(100, -50)` (`C`), `(100, -10)` (`S`). Stubs from `(80, y)` to `(100, y)`. +* **Smart Crop (Body only):** `[30, comp_rect[1], 80, 0]` + +#### 22. `siso` (SISO Shift Register) +* **Source File:** [SISOShiftRegister.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Counters/SISOShiftRegister.py) +* **Bounding Box (`comp_rect`):** `[10, -60, 120, 0]` +* **Body Silhouette:** + * Rectangle starting at `(30, -60)` with width `70` and height `60`. +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, -10)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, -15), (40, -10), (30, -5)`. + * Centered internal text labels: + - `"SISO"` at `(65, -50)` (align: `0.5`, `0.5`). + - `"D"` at `(45, -30)` (align: `0.0`, `0.5`). + - `"CK"` at `(45, -10)` (align: `0.0`, `0.5`). + - `"Q"` at `(95, -30)` (align: `1.0`, `0.5`). + - `"~Q"` at `(95, -10)` (align: `1.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** 2 pins: `(10, -30)` (`D`), `(10, -10)` (`CK`). `D` stub goes to `(30, -30)`. `CK` stub goes to `(30, -10)` (positive edge) or `(20, -10)` (negative edge). + - **Outputs:** 2 pins: `(120, -30)` (`Q`), `(120, -10)` (`~Q`). Stubs go from `(100, y)` to `(120, y)`. +* **Smart Crop (Body only):** `[30, -60, 100, 0]` + +#### 23. `sipo` (SIPO Shift Register) +* **Source File:** [SIPOShiftRegister.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Counters/SIPOShiftRegister.py) +* **Bounding Box (`comp_rect`):** Dynamic based on bits: `[10, min((outpin_t - 30, -90)), 120, max((outpin_b + 10, -10))]` (default 4 bits: `[10, -100, 120, 0]`). +* **Body Silhouette:** + * Rectangle starting at `(30, comp_rect[1])` with width `70` and height `comp_rect[3] - comp_rect[1]` (width 70, height 100 for default 4 bits). +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, -40)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, -45), (40, -40), (30, -35)`. + * Centered internal text labels: + - `"SIPO"` at `(65, comp_rect[1] + 10)` (align: `0.5`, `0.5`). + - `"D"` at `(45, -60)` (align: `0.0`, `0.5`). + - `"CK"` at `(45, -40)` (align: `0.0`, `0.5`). + - `"RST"` at `(45, -20)` (align: `0.0`, `0.5`). + - `"Q0"`, `"Q1"`, ... at `(95, y)` (align: `1.0`, `0.5`) for each output pin. +* **Terminals & Pins:** + - **Inputs:** 3 pins: `(10, -60)` (`D`), `(10, -40)` (`CK`), and `(10, -20)` (`RST`). `D` and `RST` stubs go to `(30, y)`. `CK` stub goes to `(30, -40)` (positive edge) or `(20, -40)` (negative edge). + - **Outputs:** `bits` output pins: `(120, y)` for `y` in range `outpin_t` to `outpin_b` with step 20 (e.g. `[-70, -50, -30, -10]` for default 4 bits). Stubs go from `(100, y)` to `(120, y)`. +* **Smart Crop (Body only):** `[30, comp_rect[1], 100, comp_rect[3]]` + +#### 24. `piso` (PISO Shift Register) +* **Source File:** [PISOShiftRegister.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Counters/PISOShiftRegister.py) +* **Bounding Box (`comp_rect`):** Dynamic based on bits: `[10, min((inpin_t - 50, -90)), 120, max((inpin_b + 30, -30))]` (default 4 bits: `[10, -140, 120, 0]`). +* **Body Silhouette:** + * Rectangle starting at `(30, comp_rect[1])` with width `70` and height `comp_rect[3] - comp_rect[1]` (width 70, height 140 for default 4 bits). +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, inpin_b + 20)` with radius `5` (if negative-edge triggered). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, inpin_b + 15), (40, inpin_b + 20), (30, inpin_b + 25)`. + * Centered internal text labels: + - `"PISO"` at `(65, comp_rect[1] + 10)` (align: `0.5`, `0.5`). + - `"W/S"` at `(45, inpin_t - 20)` (align: `0.0`, `0.5`). + - `"D0"`, `"D1"`, ... at `(45, y)` (align: `0.0`, `0.5`) for each data input pin. + - `"CK"` at `(45, inpin_b + 20)` (align: `0.0`, `0.5`). + - `"Q"` at `(95, -70)` (align: `1.0`, `0.5`). + - `"~Q"` at `(95, -50)` (align: `1.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** `bits + 2` pins: `(10, inpin_t - 20)` (`W/S`), `(10, y)` for `y` in range `inpin_t` to `inpin_b` with step 20 (data inputs), and `(10, inpin_b + 20)` (`CK`). All stubs except `CK` go to `(30, y)`. `CK` stub goes to `(30, inpin_b + 20)` (positive edge) or `(20, inpin_b + 20)` (negative edge). + - **Outputs:** 2 pins: `(120, -70)` (`Q`), `(120, -50)` (`~Q`). Stubs go from `(100, y)` to `(120, y)`. +* **Smart Crop (Body only):** `[30, comp_rect[1], 100, comp_rect[3]]` + +#### 25. `pipo` (PIPO Shift Register) +* **Source File:** [PIPOShiftRegister.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/Components/LogicGates/Counters/PIPOShiftRegister.py) +* **Bounding Box (`comp_rect`):** Dynamic based on bits: `[10, min((outpin_t - 90, -90)), 120, max((outpin_b + 50, -30))]` (default 4 bits: `[10, -200, 120, 0]`). +* **Body Silhouette:** + * Rectangle starting at `(30, comp_rect[1])` with width `70` and height `comp_rect[3] - comp_rect[1]` (width 70, height 200 for default 4 bits). +* **Distinguishing Marks:** + * Negative-edge trigger clock bubble: Circle centered at `(25, outpin_b + 20)` with radius `5` (*Note: Drawn at `SL` position due to clock bubble position drawing bug in code*). + * Clock-edge triangle: Inward-pointing triangle at clock input `(30, outpin_b + 35), (40, outpin_b + 40), (30, outpin_b + 45)`. + * Centered internal text labels: + - `"PIPO"` at `(65, comp_rect[1] + 10)` (align: `0.5`, `0.5`). + - `"S0"` at `(45, outpin_t - 60)` (align: `0.0`, `0.5`). + - `"S1"` at `(45, outpin_t - 40)` (align: `0.0`, `0.5`). + - `"SR"` at `(45, outpin_t - 20)` (align: `0.0`, `0.5`). + - `"D0"`, `"D1"`, ... at `(45, y)` (align: `0.0`, `0.5`) for each data input pin. + - `"Q0"`, `"Q1"`, ... at `(95, y)` (align: `1.0`, `0.5`) for each output pin. + - `"SL"` at `(45, outpin_b + 20)` (align: `0.0`, `0.5`). + - `"CK"` at `(45, outpin_b + 40)` (align: `0.0`, `0.5`). +* **Terminals & Pins:** + - **Inputs:** `bits + 5` pins: `(10, outpin_t - 60)` (`S0`), `(10, outpin_t - 40)` (`S1`), `(10, outpin_t - 20)` (`SR`), `(10, y)` for `y` in range `outpin_t` to `outpin_b` with step 20 (data inputs), `(10, outpin_b + 20)` (`SL`), and `(10, outpin_b + 40)` (`CK`). All stubs except `CK` go to `(30, y)`. `CK` stub goes to `(30, outpin_b + 40)` (positive edge) or `(20, outpin_b + 40)` (negative edge). + - **Outputs:** `bits` output pins: `(120, y)` for `y` in range `outpin_t` to `outpin_b` with step 20. Stubs go from `(100, y)` to `(120, y)`. +* **Smart Crop (Body only):** `[30, comp_rect[1], 100, comp_rect[3]]` + +--- + + +## 3. The Accuracy Approach: Programmatic Gate Icon Generation + +### Analysis: Programmatic vs. Hand-Authored SVGs + +```mermaid +graph TD + A[Icon Creation Options] --> B(Programmatic Generation) + A --> C(Hand-Authoring SVGs) + + B --> B1[PRO: 100% accurate to canvas rendering] + B --> B2[PRO: Instantly updates when component code changes] + B --> B3[PRO: Handles ANSI & IEC symbol types at zero cost] + B --> B4[CON: Needs setup to crop out port pins/labels] + + C --> C1[PRO: Complete design-grid freedom] + C --> C2[CON: High maintenance - 50 SVGs to update on redraws] + C --> C3[CON: Human error can lead to mismatched shapes] +``` + +### Recommendation +**Programmatic generation is highly recommended.** +By executing a generator script in a Python/PyGObject environment, we can dynamically compile the logic gate SVGs. Since the logic gate classes inherit from `BaseComponent` and implement `drawComponent(cr, layout)`, we can reuse their exact drawing functions. + +### Implementing Programmatic Cairo Generation +A script named `bin/generate-gate-icons.py` will follow these steps: +1. Initialize GTK and Pango (to load font mappings). +2. For each logic gate registered in `logic_gates` (from `ggate.Components.LogicGates`): + - Instantiate the component object. + - Read its bounding box: `rect = component.comp_rect` (e.g. `[10, -40, 100, 0]`). + - Calculate width and height: `w = rect[2] - rect[0]`, `h = rect[3] - rect[1]`. + - Apply **Smart Bounding-Box Cropping**: + * To draw only the core gate symbol without terminal connector lines or labels, crop the drawing box to the body itself using the coordinates specified in Section 2.D (e.g., for `and`, cropping the left `x`-start from `10` to `30` and the right `x`-end from `100` to `80`, eliminating the pin-stubs). + - Generate two files per component: + * `gate-[name]-symbolic.svg` (ANSI format: `Preference.symbol_type = 0`). + * `gate-[name]-iec-symbolic.svg` (IEC format: `Preference.symbol_type = 1`). + - Create a `cairo.SVGSurface(filepath, w + padding, h + padding)`. + - Instantiate a PangoCairo layout for text labels. + - Translate coordinates by `(-rect[0] + padding_x, -rect[1] + padding_y)`. + - Set standard symbolic stroke styles (`cr.set_line_width(1.5)` and `cr.set_source_rgb(0, 0, 0)`). + - Execute `component.drawComponent(cr, layout)`. + - Call `surface.finish()` to flush the SVG vectors to disk. + +--- + +## 4. Canonical SVG Specifications + +To ensure crisp rendering and native system integration (especially light/dark mode recoloring), the generated and hand-authored SVGs must strictly adhere to the following specifications: + +### A. General Specs +* **Standard Grid (Actions):** 16x16 pixels viewbox with a 1px protective margin (active drawing area: 14x14 pixels). +* **Standard Grid (Components):** 24x24 or 32x32 pixels viewbox. +* **Stroke Alignment:** Stroke centers should align to half-pixels for 1px lines (to remain crisp on pixel grids) and whole pixels for 2px lines. + +### B. GTK Symbolic Colors +GTK4 automatically parses and recolors files ending with `-symbolic.svg`. Fills and strokes must use the standard system palette hex codes without adding local inline style blocks that override them: +* **Foreground (Standard):** `#2e3436` (GTK replaces this with the current stylesheet text color: black on light mode, white on dark mode). +* **Accent/Blue:** `#3584e4` (GTK replaces with the theme's active accent color). +* **Destructive/Red:** `#e01b24` +* **Success/Green:** `#2ec27e` +* **Warning/Yellow:** `#e5a50a` +* **Styling Rule:** All paths must use `fill="currentColor"` or standard `#2e3436` with a transparent background. Do not embed gradients, complex shadow filters, or hardcoded white background fills in symbolic files. + +--- + +## 5. Integrating with GResource and Freedesktop Layouts + +To allow loading icons strictly by name (e.g., `Gtk.Image.new_from_icon_name("gate-and-symbolic")`), we must configure GResource and Gtk's IconTheme correctly. + +### A. Resource Path Restructuring (Using Alias Mapping) +Instead of reorganizing the physical file structure in git, we will map files in [dev-resources.xml](file:///Users/ekureedem/Documents/Projects/GGate/data/images/dev-resources.xml) and [resources.xml.in](file:///Users/ekureedem/Documents/Projects/GGate/data/images/resources.xml.in) using the GResource `alias` attribute: + +```xml + + + + actions/flip-horizontal.svg + actions/flip-vertical.svg + actions/rotate-left.svg + actions/rotate-right.svg + actions/add-net.svg + + + components/and.svg + components/and-iec.svg + components/or.svg + components/or-iec.svg + + +``` + +### B. Dynamically Building Aliases in Meson +In [data/images/meson.build](file:///Users/ekureedem/Documents/Projects/GGate/data/images/meson.build), we will modify the XML generation loop so Meson generates the aliases automatically during release builds: + +```meson +# Proposed data/images/meson.build snippet +icon_files_xml = [] +foreach file: icon_files + parts = file.split('/') + category = parts[0] # 'components' or 'actions' + filename = parts[1] # e.g., 'and.svg' + base_name = filename.split('.')[0] + + # Map category to freedesktop context + context = (category == 'components') ? 'devices' : category + + # Format target symbolic name + if category == 'components' + # The generator produces both ANSI and IEC files + # E.g., components/and.svg -> gate-and-symbolic.svg + # E.g., components/and-iec.svg -> gate-and-iec-symbolic.svg + alias_name = 'gate-' + base_name + '-symbolic.svg' + else + alias_name = base_name + '-symbolic.svg' + endif + + alias_path = 'hicolor/scalable/' + context + '/' + alias_name + icon_files_xml += '' + file + '' +endforeach +``` + +### C. Application Theme Registration +In [MainFrame.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/MainFrame.py), replace the old search path hack: +```python +# DEPRECATED +themed_icons.add_search_path(config.DATADIR + "/images") +``` + +With the native GResource registration inside `do_startup()` of `GLogicApplication`: +```python +# RECOMMENDED +display = Gdk.Display.get_default() +if display: + theme = Gtk.IconTheme.get_for_display(display) + # Register the GResource path prefix where GTK will search for icons + resource_prefix = "/org/astralco/ggate/Dev/icons" if config.RUNNING_FROM_SOURCE else "/org/astralco/ggate/icons" + theme.add_resource_path(resource_prefix) +``` + +### D. Updating Code References +1. **Palette Icons ([ComponentView.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/ComponentView.py)):** + Remove direct file paths and check the user preference dynamically: + ```python + suffix = "-iec" if Preference.symbol_type == 1 else "" + icon.set_from_icon_name(f"gate-{gate}{suffix}-symbolic") + ``` +2. **Context Menus ([MenuPopover.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/MenuPopover.py)):** + Change custom XML attributes from `verb-icon` (pointing to a path) to the standard `icon` attribute: + ```xml + + + Flip Horizontally + menu.flip_hori + flip-horizontal-symbolic + + ``` + +--- + +## 6. Phased Work Plan + +### Phase 1: Resource Infrastructure Setup +1. **Meson & GResource Update:** Update [data/images/meson.build](file:///Users/ekureedem/Documents/Projects/GGate/data/images/meson.build) and [dev-resources.xml](file:///Users/ekureedem/Documents/Projects/GGate/data/images/dev-resources.xml) to map resources using the new alias layout. +2. **Theme Registration:** Update [MainFrame.py](file:///Users/ekureedem/Documents/Projects/GGate/ggate/MainFrame.py) to register the `/icons` resource path at startup and delete the legacy `add_search_path` filesystem hack. +3. **Reference Refactoring:** Update `ComponentView.py` and `MenuPopover.py` to load icons by name rather than file paths. + +### Phase 2: Programmatic Icon Generation Script +1. **Development of `bin/generate-gate-icons.py`:** Write and refine the programmatic Cairo SVG generation script. +2. **Smart Cropping Adjustment:** Run tests to define the exact cropping limits for each of the 25 components to ensure ports/wires are excluded while logic gate symbols look crisp. +3. **Generation:** Run the script to generate all 50 SVGs (25 ANSI, 25 IEC) and write them to `data/images/components/`. + +### Phase 3: Hand-Authored Assets & System Files +1. **Action Icons:** Update `flip-horizontal.svg`, `flip-vertical.svg`, `rotate-left.svg`, and `rotate-right.svg` to be 16x16 symbolic SVGs. +2. **Net Icon:** Design the missing `add-net-symbolic.svg` showing a clean vector wire segment. +3. **App & MIME Icons:** Modernize `org.astralco.ggate.svg` and `application-x-ggate-circuit-symbolic.svg`, then update Meson file installers and setup configs to deploy them to standard directories. + +### Phase 4: Verification & Testing +1. **Theme Switch Verification:** Verify that toggling between ANSI and IEC symbols updates the ComponentView icons on the fly. +2. **Light/Dark Mode Check:** Test the application in both light mode and dark mode to ensure GTK recolors all symbolic icons correctly. +3. **Distribution Verification:** Run Flatpak and Snap packaging builds to confirm that compiled `.gresource` files and desktop icon registries load without warning messages. + +--- + +## 7. Open Questions for the PM + +1. **Terminal Pins in Component Icons:** Do we want the gate icons in the sidebar to display the small terminal connection dots on the margins, or should we crop them strictly to the gate symbol body? (Cropping them out is recommended for cleaner, less cluttered icons in the sidebar list). +2. **Custom Actions vs. Standard GTK Icons:** For common operations like flip or rotate, should we use our custom logic-gate-themed flip/rotate icons (which reflect a buffer shape), or should we use standard system desktop icons like `object-flip-horizontal-symbolic`? (We recommend keeping the custom buffer-based icons as they reinforce the circuit-editing domain). +3. **Dynamic Component View Refresh:** Should the component view sidebar listen to preference changes and refresh its icons instantly when the user toggles ANSI/IEC symbol types, or is it acceptable to require an application restart for the sidebar icons to switch? (Dynamic updating is highly recommended and simple to implement by listening to setting notifications). diff --git a/.agents/reference/build-system-analysis.md b/.agents/reference/build-system-analysis.md new file mode 100644 index 0000000..3a5865b --- /dev/null +++ b/.agents/reference/build-system-analysis.md @@ -0,0 +1,35 @@ +# GGate Remaining Build & Packaging Issues + +This document tracks the outstanding build, packaging, and resource issues in GGate. All previously identified issues (such as local variable shadowing, missing imports in the launcher, CLI version mismatch, static version unification, app ID casing, and desktop file naming) have been resolved. + +--- + +## 1. Resource and Icon Path Handling (P2) +* **Status**: **Still Pending** +* **Linked Plan**: [.agents/plans/icon-creation.md](file:///.agents/plans/icon-creation.md) +* **Description**: Component icons are still loaded using direct filesystem path lookups (e.g. `_get_icon_path` in `ComponentView.py` and absolute file path constructs in `MenuPopover.py`). +* **Issues**: + 1. This breaks when the application is installed globally (since the source `data/images` path is not present at the python site-packages folder level). + 2. The GResource path layout is non-standard (redundant `data/` subdirectory, missing icon theme folder hierarchy like `hicolor`, and double actions paths). +* **Recommended Resolution**: + - Restructure the icons folder hierarchy inside both the dev and release GResource bundles to follow the Freedesktop icon theme standards: `/org/astralco/ggate/icons/hicolor/[size]/[context]/[name].[ext]`. + - Use `GResource` aliases to map resource paths cleanly. + - Load all icons strictly by name using `Gtk.Image.new_from_icon_name` or `resource://` URLs. + +--- + +## 2. Flatpak Build Improvements +* **Status**: **Still Pending** +* **Description**: The Flatpak manifest (`build-aux/flatpak/org.astralco.ggate.json`) compiles python modules using pip with active network access (`--share=network`). +* **Issues**: For official distribution on Flathub, builds must be fully offline and reproducible. +* **Recommended Resolution**: Use `flatpak-pip-generator` to generate a static list of dependencies with SHA-256 hashes, allowing local offline pip installation. + +--- + +## 3. Snapcraft Build Improvements +* **Status**: **Still Pending** +* **Description**: + 1. The snapcraft recipe (`build-aux/snapcraft/snapcraft.yaml`) uses a remote installer script via `curl` and pipes it to python (`curl -sSL https://install.python-poetry.org | python3 -`) to fetch Poetry. + 2. The recipe passes a hardcoded prefix: `--prefix=/snap/ggate/current/usr`. +* **Issues**: Downloading third-party installer scripts at build time is insecure and prone to network failure. Hardcoded snap prefix paths make snap execution non-relocatable across alternative snap channels. +* **Recommended Resolution**: Use native Snapcraft python plugins or local offline packaging of pip dependencies, and let Snapcraft handle the meson installation prefix automatically. diff --git a/.agents/reference/delegation-brief.md b/.agents/reference/delegation-brief.md new file mode 100644 index 0000000..a216f1a --- /dev/null +++ b/.agents/reference/delegation-brief.md @@ -0,0 +1,47 @@ +# Delegation Brief — GGate + +Stable preliminary context for any implementer (subagent / agy) working on GGate. +Delegations reference this file instead of restating it. Read this first, then the +task-specific prompt. + +## Repo +- Path / working dir: `/Users/ekureedem/Documents/Projects/GGate` (git repo, branch off `main`). +- Stack: Python `>=3.10`, GTK 4 (`>=4.16`), libadwaita (`>=1.6.8`), Cairo-rendered canvas. +- App IDs: `org.astralco.ggate` (installed), `org.astralco.ggate.Dev` (dev). +- Authoritative architecture notes: `CLAUDE.md` (read it). Working backlog: `.agents/TODO.md`. + +## Conventions (enforced in review) +- Double quotes for strings, always. +- Minimal inline comments — single-line, only on genuinely non-obvious / "magic" code. + NO docstrings, NO line-by-line narration, NO `# ...existing code...` markers. +- No unused/dangling imports or variables. No extra methods/types unless necessary. +- Avoid deep nesting: extract a helper at ~3 levels; prefer early returns. +- GTK4: single-threaded UI (`GLib.idle_add` to marshal); no `show_all()`/`container.add()` + (use `set_visible`/`append`/`set_child`); prefer `Adw.Dialog`/`Adw.AlertDialog` and + `Gtk.FileDialog`. Never call `set_size_request` inside a draw callback (layout loop). +- Commits: Conventional Commits, one-line subject. **Do NOT commit unless told.** + +## Build & resources +- Meson is the build system. Resources compile to gresource bundles: + - Dev: `run.py` loads `dev-resources.gresource` (from `data/images/dev-resources.xml`). + - Installed: `ggate-resources.gresource` (from `data/images/resources.xml.in`, via + `data/images/meson.build` which does dynamic XML generation — reuse that technique). +- Resource prefixes follow `/org/astralco/ggate/...` (installed) and + `/org/astralco/ggate/Dev/...` (dev). Dev-vs-installed detection lives in `config.py`. +- New bundled assets MUST go through Meson + both gresource XMLs and load by resource + path — never an absolute/relative filesystem path (breaks in venv/installed prefixes). +- Icon/asset gresource conventions are detailed in `.agents/plans/icon-creation.md`. + +## Sandbox / verification +- GTK CANNOT headless-launch here (setuid sandbox blocks GTK init). Verify at LOGIC level: + `python3 -c "import ast; ast.parse(open(p).read())"` per changed .py; `json.load` per + JSON; small comparison scripts; `grep` assertions; `meson setup build`/`--reconfigure` + if `meson` is on PATH (else paste the generated XML/meson snippets for review). + +## Plans (specs live here) +- `.agents/plans/icon-creation.md` — icon generation + dynamic gresource/meson globbing. + +## Reporting +- Re-read current on-disk state before editing (prior partial work may exist). +- Report: per-task summary, files created/changed (precise paths), verification results, + and anything deferred or uncertain. Do not commit. diff --git a/.agents/skills/designing-gnome-ui.md b/.agents/skills/designing-gnome-ui.md new file mode 100644 index 0000000..1e1b631 --- /dev/null +++ b/.agents/skills/designing-gnome-ui.md @@ -0,0 +1,368 @@ +--- +name: designing-gnome-ui +description: Use when designing, implementing, or modifying UI for GNOME apps; before writing UI code; when reviewing existing UI for HIG compliance; when working with GTK 4/libadwaita or styling Qt/PySide6 for GNOME +--- + +# Designing GNOME UI + +Design GNOME UIs that are HIG-compliant, polished, and user-centered. + +**Core principle:** No UI code without design decisions. Pattern selection and quality verification happen before implementation. + +**Quality layers:** Compliance (follows HIG) → Polish (feels premium) → Rigor (handles edge cases) + +**Companion skill:** For app architecture (lifecycle, threading, GSettings, actions, packaging), use `developing-gtk-apps`. + +## What's New (libadwaita 1.6-1.8) + +| Need | Widget/API | Notes | +|------|------------|-------| +| Exclusive toggles (view mode) | `AdwToggleGroup` | Replaces multiple `GtkToggleButton` | +| Loading indicator | `AdwSpinner` | Works with animations disabled | +| Persistent bottom controls | `AdwBottomSheet` | Music player, persistent actions | +| Wrapping content (tags) | `AdwWrapBox` | Auto-wraps like text | +| Inline view switching | `AdwInlineViewSwitcher` | For cards, sidebars | +| Keyboard shortcuts | `AdwShortcutsDialog` | Replaces deprecated `GtkShortcutsWindow` | +| System accent color | Automatic | Apps follow desktop preference via portal | +| System fonts | `AdwStyleManager` | Access monospace/document fonts | + +**Deprecations:** `.dim-label` → use `.dimmed` class + +```python +# AdwToggleGroup - view mode switching +toggle_group = Adw.ToggleGroup() +toggle_group.add(Adw.Toggle(icon_name="view-grid-symbolic", name="grid")) +toggle_group.add(Adw.Toggle(icon_name="view-list-symbolic", name="list")) +toggle_group.connect("notify::active-name", lambda g, p: set_view(g.get_active_name())) +header.pack_start(toggle_group) + +# AdwBottomSheet - music player controls +bottom_sheet = Adw.BottomSheet() +bottom_sheet.set_content(main_content) +bottom_sheet.set_sheet(player_controls) +bottom_sheet.set_open(True) # Show sheet +window.set_content(bottom_sheet) + +# AdwWrapBox - tag display +wrap_box = Adw.WrapBox(spacing=6) +for tag in ["Python", "GTK", "GNOME", "libadwaita"]: + chip = Gtk.Label(label=tag) + chip.add_css_class("chip") # Custom styling + wrap_box.append(chip) + +# System fonts (1.7+) - for code editors, document views +style_manager = Adw.StyleManager.get_default() +mono_font = style_manager.get_monospace_font_name() # User's preferred mono font +doc_font = style_manager.get_document_font_name() # User's preferred document font +# Also available as CSS: --monospace-font-family, --document-font-family +``` + +## The Process + +```dot +digraph gnome_ui_process { + rankdir=LR; + node [shape=box]; + + "UI Task" -> "1. Context" -> "2. Patterns" -> "3. Details" -> "4. Checklist" -> "Implement"; + "4. Checklist" -> "2. Patterns" [label="issues" style=dashed]; +} +``` + +1. **Context:** User goal, app type, constraints (screen size, input) +2. **Patterns:** Select containers, navigation, controls, feedback +3. **Details:** Typography, spacing, icons, writing style +4. **Checklist:** Verify compliance, polish, rigor before code + +## Container Selection + +```dot +digraph containers { + rankdir=TB; + node [shape=box]; + + "Building what?" [shape=diamond]; + "AdwApplicationWindow + HeaderBar" [style=filled fillcolor=lightgreen]; + "AdwPreferencesWindow" [style=filled fillcolor=lightgreen]; + "AdwDialog" [style=filled fillcolor=lightgreen]; + + "Building what?" -> "AdwApplicationWindow + HeaderBar" [label="main window"]; + "Building what?" -> "AdwPreferencesWindow" [label="settings"]; + "Building what?" -> "AdwDialog" [label="modal action"]; +} +``` + +| Scenario | Default | Notes | +|----------|---------|-------| +| App window | `AdwApplicationWindow` + `AdwHeaderBar` | Remember user size, start ~800x600 | +| Settings | `AdwPreferencesWindow` | Handles groups, search, subpages | +| List of items | `AdwPreferencesGroup` with rows | Boxed list style | +| Primary action | Single button, header bar end | `suggested-action` class if emphasized | +| Destructive action | `destructive-action` class | Requires undo or confirmation | + +## Navigation Selection + +| Structure | Default Pattern | +|-----------|-----------------| +| Single view | None needed | +| 2-4 views | `AdwViewSwitcher` in header bar | +| Many/dynamic views | `AdwNavigationSplitView` (sidebar) | +| Hierarchical | `AdwNavigationView` (drill-down) | + +## Control Defaults + +| Need | Default | Avoid | +|------|---------|-------| +| On/Off | `AdwSwitchRow` | Checkbox for settings | +| Choose one (few) | `AdwComboRow` | Radio buttons outside dialogs | +| Choose one (many) | `AdwComboRow` + search | Long unsearchable dropdowns | +| Text input | `AdwEntryRow` | Bare `GtkEntry` | +| Multiline text | `GtkTextView` + `card` class | Bare unstyled text view | +| Number | `AdwSpinRow` | Text entry for numbers | +| Date | `GtkCalendar` in popover | Text entry for dates | +| Action in list | `AdwActionRow` + suffix button | Multiple buttons per row | +| Search | `GtkSearchBar` + toggle button | Always-visible search box | + +### Search Bar Pattern + +```python +# Search bar slides down from header, toggle with button or Ctrl+F +search_bar = Gtk.SearchBar() +search_entry = Gtk.SearchEntry() +search_bar.set_child(search_entry) +search_bar.connect_entry(search_entry) +search_bar.set_key_capture_widget(window) # Type-to-search + +# Toggle button in header bar +search_btn = Gtk.ToggleButton(icon_name="system-search-symbolic") +search_btn.set_tooltip_text("Search") +search_bar.bind_property("search-mode-enabled", search_btn, "active", + GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE) +header.pack_end(search_btn) +toolbar_view.add_top_bar(search_bar) +``` + +### Form Validation Pattern + +```python +# Use error CSS class on invalid fields +def validate_entry(row): + text = row.get_text() + if not text or len(text) < 3: + row.add_css_class("error") + row.set_tooltip_text("Name must be at least 3 characters") + return False + row.remove_css_class("error") + row.set_tooltip_text("") + return True + +name_row.connect("changed", lambda r: validate_entry(r)) +``` + +**Validation timing:** On change for format checks, on focus-out for expensive checks, on submit for final validation. + +## List Widget Selection + +| Content | Widget | Why | +|---------|--------|-----| +| Settings/preferences | `AdwPreferencesGroup` | Boxed list style, handles rows | +| Navigation list (sidebar) | `GtkListBox` | Selection support, activatable rows | +| Large/dynamic data | `GtkListView` | Virtual scrolling, performance | +| Grid of items | `GtkGridView` | Thumbnail grids, icon views | + +**Selection modes:** Use `Gtk.SingleSelection` for navigation, `Gtk.MultiSelection` for bulk actions. Toggle selection mode with header bar button + action bar for bulk operations. See reference for code patterns. + +## Iconography + +**Rules:** +- Symbolic icons only (outline, monochrome) - never full-color in UI +- Source from GNOME Icon Library (`icon-library` app) +- Header bar: icon-only buttons, always add tooltips +- Naming: `action-object-symbolic` (e.g., `list-add-symbolic`) +- **Dynamic icons:** Update icon name based on state (e.g., `user-trash-symbolic` → `user-trash-full-symbolic`) + +| Action | Icon | +|--------|------| +| Add/New | `list-add-symbolic` | +| Delete | `user-trash-symbolic` | +| Settings | `emblem-system-symbolic` | +| Menu | `open-menu-symbolic` | +| Search | `system-search-symbolic` | +| Edit | `document-edit-symbolic` | +| Back | `go-previous-symbolic` | +| Drill-down | `go-next-symbolic` | +| Sync | `emblem-synchronizing-symbolic` | +| Offline | `network-offline-symbolic` | +| Warning | `dialog-warning-symbolic` | +| Error | `dialog-error-symbolic` | +| Select mode | `selection-mode-symbolic` | +| Check/Done | `emblem-ok-symbolic` | +| Close | `window-close-symbolic` | +| Refresh | `view-refresh-symbolic` | + +## Feedback Selection + +```dot +digraph feedback { + rankdir=TB; + node [shape=box]; + + "What happened?" [shape=diamond]; + "Transient or persistent?" [shape=diamond]; + "AdwToast" [style=filled fillcolor=lightgreen label="AdwToast (default)"]; + "AdwBanner" [style=filled fillcolor=lightyellow]; + "AdwDialog" [style=filled fillcolor=lightpink]; + "Progress/Spinner" [style=filled fillcolor=lightblue]; + + "What happened?" -> "Transient or persistent?" [label="state/error"]; + "What happened?" -> "AdwDialog" [label="needs decision"]; + "What happened?" -> "Progress/Spinner" [label="ongoing operation"]; + "Transient or persistent?" -> "AdwToast" [label="transient event"]; + "Transient or persistent?" -> "AdwBanner" [label="persistent state"]; +} +``` + +| Scenario | Default | Details | +|----------|---------|---------| +| Action done | `AdwToast` | Short message, optional undo | +| Destructive action | `AdwToast` + undo | Prefer over confirmation dialog | +| Error (recoverable) | `AdwToast` | Brief, auto-retry silently | +| Error (blocking) | `AdwDialog` | Explain problem and required fix | +| Persistent state | `AdwBanner` | Offline, degraded mode, auth required | +| Needs decision | `AdwDialog` | Conflicts, irreversible actions | +| Short wait (<5s) | `AdwSpinner` | No progress bar | +| Long operation (>30s) | Progress bar + text | "13 of 42 processed" | + +**Error escalation:** Toast (transient) → Banner (persists) → Dialog (requires action) +- Network blip: Toast, auto-retry +- Prolonged offline: Banner with "Retry" button +- Auth expired: Dialog + Banner until resolved + +**Dialog rules:** +- Cancel button first (left), action button last (right) +- Specific verbs ("Delete", "Save"), never "OK" or "Yes" +- Destructive actions use `destructive-action` style + +**Context menus:** Use `GtkPopoverMenu` for right-click actions (remove, rename, properties). Keep menus short; move complex actions to dialogs. + +### Empty State Pattern + +```python +# Show placeholder when list is empty +empty_state = Adw.StatusPage( + icon_name="folder-symbolic", + title="No Projects", + description="Create a project to get started" +) +create_btn = Gtk.Button(label="Create Project") +create_btn.add_css_class("pill") +create_btn.add_css_class("suggested-action") +empty_state.set_child(create_btn) + +# Use stack to switch between list and empty state +stack.add_named(list_view, "content") +stack.add_named(empty_state, "empty") +stack.set_visible_child_name("empty" if model.get_n_items() == 0 else "content") +``` + +## Quality Checklist + +**Create TodoWrite items for each applicable check before implementing.** + +### Layer 1: Compliance + +- [ ] Correct container type and header bar structure +- [ ] Navigation pattern matches content structure +- [ ] Standard widgets used (not custom where native exists) +- [ ] Symbolic icons from GNOME Icon Library +- [ ] Typography uses style classes (`title-1`, `heading`, `body`, `caption`) +- [ ] Libadwaita spacing defaults (no custom margins) +- [ ] Header capitalization for labels, sentence for descriptions + +### Layer 2: Polish + +- [ ] Clear visual hierarchy - important elements prominent +- [ ] Controls and text properly aligned +- [ ] Consistent patterns throughout +- [ ] Empty states have placeholder page (icon + message + action) +- [ ] Loading states show spinner/skeleton, never frozen UI +- [ ] Smooth resize and view transitions +- [ ] Comfortable density - not cramped, not sparse + +### Layer 3: Rigor + +- [ ] All controls keyboard-accessible (Tab, Enter, Space) +- [ ] All elements have accessible names for screen readers +- [ ] Works with high contrast (`GTK_THEME=Adwaita:hc`) +- [ ] Works with 200% text scaling +- [ ] Error handling for every input/action +- [ ] Edge cases handled (empty lists, long text, missing data) +- [ ] Destructive actions have undo where possible +- [ ] Responsive: works at 800x600, adapts to larger + +## Accessibility Quick Check + +```bash +# Test high contrast +GTK_THEME=Adwaita:hc ./myapp + +# Test large text (set in GNOME Settings > Accessibility first) + +# Test with screen reader +orca & +./myapp + +# Keyboard-only: unplug mouse, navigate entire app with Tab/Enter/Space +``` + +**Code:** Set accessible labels for icon-only buttons and images: +```python +button.update_property([Gtk.AccessibleProperty.LABEL], ["Add new item"]) +image.update_property([Gtk.AccessibleProperty.LABEL], ["Project thumbnail"]) +``` + +## Red Flags - STOP + +- Custom styling where libadwaita has a pattern +- Multiple "suggested" or "destructive" buttons per view +- Confirmation dialogs for reversible actions (use undo) +- Text over images or textured backgrounds +- Non-GNOME icons without strong justification +- Missing tooltips on icon-only header bar buttons +- Generic labels ("OK", "Yes", "No", "Submit") +- Frozen UI during operations (missing loading states) + +## Non-GTK Apps (Qt/PySide6) + +When styling Qt apps for GNOME: +- Use Adwaita-qt or manual QSS matching Adwaita colors +- Follow same patterns conceptually (header bar → toolbar, etc.) +- Match spacing, typography scale, and icon style +- Test alongside native GNOME apps for consistency + +## Reference Files + +| Need | File | +|------|------| +| Basic UI patterns | `gnome-hig-reference.md` | +| Advanced patterns | `gnome-advanced-patterns.md` | + +**gnome-hig-reference.md** - Read for most apps: +- Container, navigation, control, feedback patterns with code +- Search bar, form validation, **filter models**, grid views, selection modes +- File chooser dialogs, dark/light mode, responsive breakpoints +- Primary menu structure, About dialog, Shortcuts window +- Typography, writing style, **CSS color variables**, common mistakes +- **Accessibility testing** commands (high contrast, screen reader) +- **Phone/tablet breakpoints**, adaptive layouts + +**gnome-advanced-patterns.md** - Read when building: +- Drag & drop (reordering, file drops, cross-widget DnD) +- Undo/Redo (command pattern, history management) +- Tabs (AdwTabView, multi-document apps) +- System notifications (GNotification vs Toast) +- Media display (image viewers, video controls, **pinch-to-zoom gestures**) +- Split/Paned views (resizable panels) +- Welcome/Onboarding (first-run, feature callouts) +- Popovers (tool palettes, color pickers) +- **Keyboard shortcuts** (mnemonics, shortcut controllers) diff --git a/.agents/skills/developing-gtk-apps.md b/.agents/skills/developing-gtk-apps.md new file mode 100644 index 0000000..e72531e --- /dev/null +++ b/.agents/skills/developing-gtk-apps.md @@ -0,0 +1,198 @@ +--- +name: developing-gtk-apps +description: Use when building GTK 4/libadwaita applications; before writing app boilerplate; when debugging threading, signals, or lifecycle issues; when setting up GSettings, resources, or packaging; delegates UI/widget decisions to designing-gnome-ui skill +--- + +# Developing GTK Apps + +Build robust GTK 4/libadwaita applications with correct architecture, lifecycle, and patterns. + +**Core principle:** Get the foundation right before the UI. Application lifecycle, threading model, and resource management are where most GTK apps break. + +**Relationship to UI skill:** This skill handles architecture and plumbing. For widget selection, layout, and HIG compliance, use `designing-gnome-ui`. + +## Decision Flow + +| Task | Use | +|------|-----| +| Which widget for settings? | designing-gnome-ui | +| How to structure preferences window? | designing-gnome-ui | +| App crashes on startup | THIS SKILL | +| UI freezes during operation | THIS SKILL | +| How to save user preferences | THIS SKILL (GSettings) | +| Signal not firing/memory leak | THIS SKILL | +| Setting up new app boilerplate | THIS SKILL | +| Packaging for Flatpak | THIS SKILL | + +## What's Current (libadwaita 1.7+, GTK 4.18+) + +**API deprecations to avoid:** +- `GtkShortcutsWindow` → Use `AdwShortcutsDialog` (libadwaita 1.8+) +- `.dim-label` CSS class → Use `.dimmed` class +- X11/Broadway backends are deprecated in GTK 4 (removal planned for GTK 5) + +**New patterns (libadwaita 1.6-1.8):** +- `AdwSpinner` - Preferred over `GtkSpinner` +- `AdwToggleGroup` - Replaces multiple exclusive `GtkToggleButton` instances +- `AdwBottomSheet` - Persistent bottom sheets +- `AdwWrapBox` - Box that wraps children to new lines +- `AdwInlineViewSwitcher` - For cards, sidebars, boxed lists + +## Application Boilerplate + +```python +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Gtk, Adw, Gio + +class MyApp(Adw.Application): + def __init__(self): + super().__init__( + application_id="com.example.MyApp", + flags=Gio.ApplicationFlags.DEFAULT_FLAGS + ) + + def do_activate(self): + win = self.props.active_window + if not win: + win = MyWindow(application=self) + win.present() + +class MyWindow(Adw.ApplicationWindow): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.set_default_size(800, 600) + +def main(): + app = MyApp() + return app.run(None) +``` + +### Application ID Rules + +| Rule | Example | +|------|---------| +| Reverse domain notation | `com.example.AppName` | +| Only alphanumeric + dots | `org.gnome.TextEditor` | +| Min 2 segments | `com.myapp` (not `myapp`) | +| Match desktop file | `com.example.MyApp.desktop` | + +### Lifecycle Signals + +| Signal | When | Use For | +|--------|------|---------| +| `startup` | Once, app launches | Actions, CSS, GSettings | +| `activate` | Each launch/raise | Create/present window | +| `shutdown` | App exits | Save state, cleanup | +| `open` | Files passed to app | Handle file arguments | + +```python +def do_startup(self): + Adw.Application.do_startup(self) # Chain up FIRST + self.setup_actions() +``` + +## Threading - The Critical Rule + +**GTK is single-threaded. All UI calls MUST happen on the main thread.** + +```python +# WRONG - will crash +def background_task(): + result = slow_computation() + self.label.set_text(result) # CRASH + +# RIGHT - use GLib.idle_add +def background_task(): + result = slow_computation() + GLib.idle_add(self.label.set_text, result) # Safe + +threading.Thread(target=background_task).start() +``` + +For async patterns with `Gio.Task` and cancellation, see `gtk-patterns-reference.md`. + +## Actions (Quick Reference) + +Actions connect UI to behavior. Define at app level (`app.action`) or window level (`win.action`). + +```python +# In do_startup - app-level action +quit_action = Gio.SimpleAction.new("quit", None) +quit_action.connect("activate", lambda a, p: self.quit()) +self.add_action(quit_action) +self.set_accels_for_action("app.quit", ["q"]) + +# In window __init__ - window-level action +save_action = Gio.SimpleAction.new("save", None) +save_action.connect("activate", self.on_save) +self.add_action(save_action) +self.get_application().set_accels_for_action("win.save", ["s"]) +``` + +For stateful actions (toggles), parameterized actions, and menu integration, see `gtk-patterns-reference.md`. + +## GSettings (Quick Reference) + +Persist user preferences with GSettings. Requires a schema file. + +```python +# In app __init__ +self.settings = Gio.Settings.new("com.example.MyApp") + +# Read/write values +dark = self.settings.get_boolean("dark-mode") +self.settings.set_boolean("dark-mode", True) + +# Bind to widget property (auto-syncs) +self.settings.bind("window-width", window, "default-width", + Gio.SettingsBindFlags.DEFAULT) + +# React to changes +self.settings.connect("changed::dark-mode", self.on_dark_changed) +``` + +For schema XML format and installation, see `gtk-patterns-reference.md`. + +## Debugging (Quick Reference) + +```bash +GTK_DEBUG=interactive myapp # Open GTK Inspector (Ctrl+Shift+D) +G_MESSAGES_DEBUG=all myapp # Show all debug messages +G_DEBUG=fatal-criticals myapp # Abort on critical warnings +GSETTINGS_BACKEND=memory myapp # Test without persisting settings +``` + +For full debugging patterns, profiling, and GDB integration, see `gtk-debugging-reference.md`. + +## Red Flags - STOP + +- Calling UI methods from threads (use `GLib.idle_add`) +- Missing `do_startup` chain-up +- Signal handlers without disconnect on destroy +- Blocking operations in signal handlers +- Hardcoded paths instead of XDG directories +- Missing application ID or wrong format +- Using `time.sleep()` in main thread +- Using `GtkShortcutsWindow` (deprecated - use `AdwShortcutsDialog`) +- Using `GtkSpinner` for libadwaita apps (use `AdwSpinner`) + +## Reference Files + +| Need | File | +|------|------| +| GObject classes, properties, **signals**, list models, **property bindings**, **factories** | `gtk-gobject-reference.md` | +| Actions, GSettings, Resources, Blueprint, async file ops | `gtk-patterns-reference.md` | +| Desktop file, **AppStream metadata**, Meson, Flatpak, **icons**, **Python deps** | `gtk-packaging-reference.md` | +| Testing with pytest, **async testing**, **headless/CI testing** | `gtk-testing-reference.md` | +| Internationalization, gettext, **ngettext plurals**, .po files, **Blueprint i18n**, **RTL testing** | `gtk-i18n-reference.md` | +| DBus activation, **interface export**, background services, **Flatpak portals** | `gtk-dbus-reference.md` | +| GTK Inspector, env vars, **profiling**, **memory debugging** | `gtk-debugging-reference.md` | +| UI patterns, widgets, HIG | Use `designing-gnome-ui` skill | + +## External References + +- [Blueprint docs](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) +- [Libadwaita API](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/) +- [GTK 4 API](https://docs.gtk.org/gtk4/) diff --git a/.agents/skills/glc-format/SKILL.md b/.agents/skills/glc-format/SKILL.md new file mode 100644 index 0000000..392252a --- /dev/null +++ b/.agents/skills/glc-format/SKILL.md @@ -0,0 +1,153 @@ +--- +name: glc-format +description: Use when reading, writing, or programmatically generating GGate logic-circuit (`.glc`) files, or troubleshooting their load/save. +--- + +# GGate GLC Circuit File Format + +`.glc` is GGate's plain-text logic-circuit format: it lists the components, their +placement and initial state, and the wires between them. This skill is enough to +author or parse a `.glc` file by hand. + +**Core principle:** Wires connect *geometrically*. A `net` segment joins to a +component pin, or to another net, wherever their endpoint coordinates coincide — +there are no ids. Property values containing structural characters must be escaped. + +## Decision flow + +| Task | What to do | +|------|------------| +| Declare file version/compatibility | Add a `version` section (first) | +| Place a component | Section whose header is the component type key | +| Connect pins electrically | Add `net` segment(s) whose endpoints match pin coordinates | +| Put `= , : \` inside a text/name value | Escape it (`\e \c \s \\`) | + +## File shape + +- Plain **UTF-8 text**. +- A file is a list of **sections** separated by a blank line (`\n\n`). +- Each section is: + - a **header line** — the section type, no leading space; then + - zero or more **attribute lines** `key=v1,v2,...` (a single leading space is + optional and ignored). +- An attribute line is split into exactly two parts on the first `=`, and the value + is split on `,`. **Keys and raw values must not contain unescaped `=` or `,`.** +- The first section should be `version`; component and `net` sections follow in any order. + +## `version` section + +``` +version + author=5.0.0 + required=2.6.0 +``` + +- `author` — the GGate version that wrote the file. +- `required` — the minimum GGate version needed to open it; if the opening app is + older, the load is rejected. Safe defaults: `author=5.0.0`, `required=2.6.0`. + +## Component sections + +The header is a **component type key** (see catalog). Attributes: + +| key | meaning | format | +|-----|---------|--------| +| `position` | top-left placement on the canvas | `x,y` (integers, grid units) | +| `input_level` | initial logic level of each **input** pin | `0`/`1` per pin; empty if none | +| `output_level` | initial logic level of each **output** pin | `0`/`1` per pin; empty if none | +| `matrix` | 2×2 orientation transform `a,b,c,d` | identity = `1,0,0,1` | +| `properties` | component settings | `name:value,name:value,...` | + +- `input_level`/`output_level` have one entry per input/output pin; leave empty for + components that have none (e.g. a source has no inputs, a probe has no outputs). The + simulator overwrites these once it runs. +- `matrix` is the rotation/flip. `1,0,0,1` is upright; 90°/180°/270° and flips are the + usual 2×2 integer matrices. +- `properties` are `name:value` pairs; `name` must be one of the component's property + names (catalog below). The value is unescaped, then interpreted as that property's + type (integer / float / select-index / boolean / string). Unknown names are ignored. + +## `net` sections (wires) + +``` +net + position=540,290,410,290 +``` + +- `position` = `x1,y1,x2,y2` — one straight wire segment between two points. +- Connectivity is geometric: route a `net` segment so its endpoint lands exactly on a + pin's coordinate to connect it; segments that share an endpoint are the same net. + +## Encoding (property values only) + +`=` `,` `:` and `\` are structural, so they are escaped inside property **values** on +write and restored on read: + +| character | stored as | +|-----------|-----------| +| `\` | `\\` | +| `,` | `\c` | +| `=` | `\e` | +| `:` | `\s` | + +Example: the label `T=0.25µs` is stored as `text:T\e0.25µs`. Values must stay on one +line (newlines are not escaped). + +## Component catalog + +Type keys and their property names. `tphl`/`tplh` are the high→low / low→high +propagation delays (µs). Select-index properties store the chosen option's index. + +| key | properties | +|-----|------------| +| `not` | `tphl, tplh` | +| `and`, `or`, `nand`, `nor`, `xor` | `inputs` (2 or 3), `tphl, tplh` | +| `tribuff` | `active`, `inverted` (select-indexes) | +| `sw` | `initstate` (0/1) | +| `osc` | `period`, `shift`, `duration` (µs), `initstate` (0/1) | +| `probe` | `name` (string; labels the signal in the timing graph) | +| `text` | `text` (string label, not simulated) | +| `led`, `7seg` | color select-index (`0`=Red, `1`=Blue, `2`=Green, `3`=Yellow) | +| `vdd`, `gnd` | (none) | +| `rsff` | `tphl, tplh` | +| `dff`, `jkff`, `tff` | `trig` (`0`=positive edge, `1`=negative), `tphl, tplh` | +| `counter` | `n`, `bits`, `trig`, `tphl, tplh` | +| `adder` | `halffull` (`0`=half, `1`=full), `tphl, tplh` | +| `siso`, `sipo`, `piso`, `pipo` | `bits`, `trig`, `tphl, tplh` | +| `net` | `position=x1,y1,x2,y2` only | + +## Worked example + +A source oscillator wired to a probe: + +``` +version + author=5.0.0 + required=2.6.0 + +osc + position=340,260 + input_level= + output_level=0 + matrix=1,0,0,1 + properties=period:2.5,shift:0.0,duration:80.0,initstate:0 + +probe + position=530,250 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:Signal1 + +net + position=540,290,410,290 +``` + +## Gotchas + +- Separate sections with exactly one blank line; the file ends without a trailing blank line. +- Each attribute line must split into exactly two parts on `=` — escape any structural + character in values. +- Wire endpoints must land exactly on pin coordinates, or the connection isn't made. +- Parsing is lenient: unknown sections/properties are skipped, so a malformed file loads + partially instead of erroring. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3c6a112 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,55 @@ +name: Build Pipeline + +on: + push: + branches: [ "main", "v5(development)" ] + tags: [ "v*" ] + pull_request: + branches: [ "main", "v5(development)" ] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + dist: + runs-on: ubuntu-latest + container: fedora:41 + steps: + - name: Install dependencies + run: > + dnf install -y + git meson ninja-build glib2-devel + gtk4-devel libadwaita-devel + python3-gobject-devel python3-devel desktop-file-utils + python3-gobject python3-cairo + python3-igraph python3-shapely python3-packaging + + - name: Checkout code + uses: actions/checkout@v5 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Configure + run: meson setup build + + - name: Compile + run: meson compile -C build + + - name: Build source tarball + run: meson dist -C build --no-tests --formats gztar + + - name: Upload tarball artifact + uses: actions/upload-artifact@v7 + with: + name: ggate-source-tarball + path: build/meson-dist/*.tar.gz + + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v3 + with: + files: build/meson-dist/*.tar.gz diff --git a/.github/workflows/deb.yml b/.github/workflows/deb.yml new file mode 100644 index 0000000..9970c74 --- /dev/null +++ b/.github/workflows/deb.yml @@ -0,0 +1,50 @@ +name: Deb Build + +on: + release: + types: [published] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + deb: + runs-on: ubuntu-latest + container: ubuntu:26.04 + steps: + - name: Install build dependencies + run: | + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y \ + git build-essential devscripts debhelper dh-python \ + meson ninja-build pkg-config \ + python3-dev python3-gi-dev libcairo2-dev \ + libgtk-4-dev libadwaita-1-dev \ + python3-gi python3-gi-cairo python3-igraph python3-shapely python3-packaging + + - name: Checkout code + uses: actions/checkout@v5 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Stage debian/, generate changelog, and build package + run: | + cp -r build-aux/debian debian + python3 build-aux/gen-deb-changelog.py > debian/changelog + dpkg-buildpackage -us -uc -b + + - name: Upload deb artifact + uses: actions/upload-artifact@v7 + with: + name: ggate-deb + path: ../ggate_*.deb + + - name: Attach to release + uses: softprops/action-gh-release@v3 + with: + files: ../ggate_*.deb diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 0000000..56d3016 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,27 @@ +name: Flatpak Build + +on: + release: + types: [published] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + flatpak-builder: + runs-on: ubuntu-latest + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 + options: --privileged + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Flatpak environment + run: | + flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak install -y flathub org.gnome.Platform//48 org.gnome.Sdk//48 + + - name: Build Flatpak + run: | + flatpak-builder --disable-rofiles-fuse --repo=repo --force-clean build-dir build-aux/flatpak/org.astralco.ggate.json diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 0000000..7258a96 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,27 @@ +name: Snap Build + +on: + release: + types: [published] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + snapcraft: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Build Snap + uses: snapcore/action-build@v1 + id: snapcraft + with: + path: build-aux/snapcraft + + - name: Upload Snap artifact + uses: actions/upload-artifact@v7 + with: + name: ggate-snap + path: ${{ steps.snapcraft.outputs.snap }} diff --git a/.gitignore b/.gitignore index e2f5ec3..c17a18e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,15 @@ ./*.py -**.glc +*.glc +!data/examples/*.glc +.flatpak-builder/ ./*.png **/__pycache__/ m4/ *.cache/ -.vscode/ -debian/ +# /debian is a transient CI copy of build-aux/debian; ignore it and deb build dirs +/debian/ +obj-*/ +build-dir/ *.egg-info/ dist/ build/ @@ -16,4 +20,7 @@ deb_dist/ .api-keys .DS_Store .ruff_cache -.mypy_cache \ No newline at end of file +.mypy_cache +dev-* +.agents/gtk4 +.claude/ \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.vscode/copilot-instructions.md similarity index 82% rename from .github/copilot-instructions.md rename to .vscode/copilot-instructions.md index 4b6fdfa..af7ef40 100644 --- a/.github/copilot-instructions.md +++ b/.vscode/copilot-instructions.md @@ -4,4 +4,5 @@ - Do NOT add extra methods or types unless explicitly requested or absolutely necessary - ALWAYS use double quotes for strings - Verify the correctness of all codes and answers strictly and meticulously -- Ensure the logic in the suggested code doesn't conflict with those in the levels above. \ No newline at end of file +- Ensure the logic in the suggested code doesn't conflict with those in the levels above. +- When creating commit messages, ensure they follow conventional commits and one liners \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..657bbdb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "jock.svg", + "mesonbuild.mesonbuild", + "mrmomo.meson-build", + "charliermarsh.ruff", + "github.vscode-github-actions" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6e281a5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.autoImportCompletions": true, + "svg.preview.backgroundCustom": "#eee", + "svg.preview.background": "custom" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0dd32..fa40bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # ChangeLog +## 2026-06-15 Ekure Edem + Animated simulation playback: compute the full timeline, then play it back paced + Floating playback control bar (Stop/Restart/Play, seek, sim-time) for animating circuits + Add playback duration preference; only animate circuits that evolve over time (e.g. oscillators) + Simplify header simulation controls to Play/Stop + Recompute simulation on live edits (switch toggle / property change) to fix desync + New themed 7-segment and LED component icons + +## 2026-06-14 Ekure Edem + Revamp timing diagram: Adw dialog with themed Cairo waveforms + Add theming system with Classic, Space, Frappé and Mocha themes + Migrate preferences to Adw.PreferencesDialog + Dynamic JSON to gresource pipeline; recompile dev resources on launch + Fix oscillator timing and gate simulation crashes + ## 2025-04-26 Ekure Edem Switch all tabs to spaces Reorganized project structure and split files to seperate parts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f9c4805 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,143 @@ +# CLAUDE.md — GGate + +Reference page for working on GGate. Facts here come from a project scan; when in doubt, +read the cited file. Keep this current as the codebase changes. + +## What GGate is + +A **logic-circuit simulator** — desktop app, **Python + GTK 4 + libadwaita**, with a +**Cairo**-rendered editor canvas. Users place logic gates/components, wire them into nets, +and simulate. Circuit files are `.glc` (examples in `data/examples/`). + +Requires: Python `>=3.10,<4`, `gtk4 >= 4.16`, `libadwaita-1 >= 1.6.8`. Runtime deps +(`pyproject.toml`): `pygobject`, `pycairo`, `python-igraph`, `shapely`, `packaging`, +`meson-python`. App ID `org.astralco.ggate` (dev: `org.astralco.ggate.Dev`). + +## Working model + +- **PM** (user): direction and priorities. +- **Claude** (me): senior dev / project owner — architecture, design, task breakdown, + review, holding context. I spec the work and verify it; I don't hand-write feature code. +- **agy** (`mcp__agy-bridge__*`, Gemini): the implementer — writes/edits code from my specs. + Also my recon tool (`delegate`/`analyze_files`) for read-only scans. + +Loop: I decide *what* and *how* → write a self-contained delegation (file paths, exact +change, acceptance criteria, `.agents/` references, conventions) → agy implements → I review +the diff → iterate. **Every UI delegation cites the relevant `.agents/gtk4/` page** so agy +works from ground truth, not memory. + +## Run flow + +- **Dev**: `run.py` loads `./dev-resources.gresource`, instantiates `GLogicApplication`. +- **Installed**: Meson generates `bin/ggate.in` → loads `ggate-resources.gresource` from + pkgdatadir, runs `GLogicApplication`. +- `GLogicApplication` (`Adw.Application`, in `MainFrame.py`) owns app actions (New/Open/ + Save/Preferences/Help/About…). `MainFrame` (`Adw.ApplicationWindow`, same file) builds + the UI and wires the canvas, file managers, circuit model, palette, and dialogs. + +## Architecture + +- **Canvas / rendering** — `ggate/DrawArea.py`: `DrawArea` (a `Gtk.ScrolledWindow`) hosts a + `Gtk.DrawingArea` with `set_draw_func(on_draw)`. All graphics (grid, components, nets, + selection) are drawn with **Cairo** onto a cached offscreen surface, then blitted. +- **Circuit model** — `ggate/CircuitManager.py`: stores components/wires/state, undo + history, snapping, net validation, simulation steps. + - **igraph** in `analyze_net_connections()` — vertices = pin positions, edges = net + segments; `graph.components()` resolves electrical connectivity. + - **shapely** in `split_nets()` — `Point`/`LineString` hit-tests for clicks on nets. +- **Components** — `ggate/Components/LogicGates/`. All subclass `BaseComponent` (ABC in + `SystemComponents.py`). + - Abstract: `drawComponent`, `drawComponentEditOverlap`, `drawComponentRunOverlap`. + - Common overrides: `propertyChanged` (validate; return `bool` or `PropertyError`), + `calculate` (sim state), `initialize` (defaults). + - `PropertyError(message, positions)` — error text + indices of invalid property rows + (drives red highlight + banner in the properties dialog). + - Concrete gates grouped under `Standard/`, `FlipFlops/`, `Counters/`, `StateViewers/`, + `Miscellaneous/`. `Calculator.py` does gate computation. +- **Property typing** — `const.py` enums map to libadwaita rows in the properties dialog: + `property_bool→SwitchRow`, `property_int→SpinRow`, `property_float→SpinRow(digits)`, + `property_select→ComboRow`, `property_string→EntryRow`. + +## Module map (`ggate/`) + +| File | Responsibility | +|------|----------------| +| `MainFrame.py` | `GLogicApplication` + `MainFrame` window; actions, wiring | +| `DrawArea.py` | editor canvas (ScrolledWindow + DrawingArea + Cairo) | +| `CircuitManager.py` | circuit model: storage, history, nets (igraph/shapely), sim | +| `ComponentView.py` | side palette of placeable gate categories | +| `Exporter.py` | render schematic to Cairo image/PDF/PS/SVG (the **real** one) | +| `MenuPopover.py` | context popover menus | +| `Preference.py` | load/save user settings to disk | +| `StatusDisplay.py` | `Gtk.Label` subclass for status text | +| `Utils.py` | vector/text/Cairo helpers | +| `const.py` / `config.py` | constants & metadata / VERSION, APP_PREFIX, paths | +| `Components/Managers/` | `Alerts`, `FileManager`; `Exporter.py` here is a **dead stub** | +| `Components/Windows/` | dialogs — see below | +| `Components/Windows/TimingGraph/` | `Display` (dialog) + `Diagram` (Cairo waveform) | +| `TimingDiagramWindow.py`, `DiagramArea.py` | **dead** GTK3 leftovers | + +## The dialogs (`Components/Windows/`) + +| File | Class / base | Status | +|------|--------------|--------| +| `About.py` | `AboutGGate` → `Adw.AboutDialog` | built, **not wired** into MainFrame | +| `Preferences.py` | `PreferencesWindow` (`Gtk.Dialog`, deprecated) | wired, works | +| `Properties.py` | `PropertyWindow` (`Adw.Dialog`) | wired but **buggy** (API drift) | +| `TimingGraph/Display.py` | `Adw.Dialog` wrapper | wired but **broken** (empty/parent) | +| `TimingGraph/Diagram.py` | `Gtk.ScrolledWindow` + 2 `DrawingArea` | draw fns **not registered** | + +These dialogs are the **current cleanup focus** — details and the grounded bug list live in +`.agents/TODO.md`. + +## Build & packaging + +- **Meson**: `meson.build` (deps, resource compile → `ggate-resources.gresource`, + `install_subdir('ggate')`), `meson_options.txt`, `bin/ggate.in`, `data/images/` gresource. +- **Flatpak**: `build-aux/flatpak/org.astralco.ggate.json` (GNOME 47 runtime, pip deps). +- **Snap**: `build-aux/snapcraft/snapcraft.yaml` (`core22`, poetry deps, meson plugin). +- **Dev resources**: `run.py` loads `./dev-resources.gresource`. This bundle is a build + artifact (git-ignored) and must be recompiled from `data/images/dev-resources.xml` + whenever bundled assets (icons, themes) change — a stale bundle silently drops new + resources (e.g. themes not enumerating → empty registry). `run.py` now recompiles it on + every launch via `glib-compile-resources`; never hand-edit the `.gresource`. +- ⚠️ `data/images/dev-resources.xml` is currently hand-maintained and git-ignored, so it + can drift from the Meson-generated installed resource (`resources.xml.in` + the + `data/images/meson.build` glob). A "proper" fix is to generate the dev XML the same way + Meson generates the installed one (single source of truth). See `.agents/TODO.md`. +- ⚠️ `config.py`/`__init__.py` versions are **static**, not Meson-populated, and currently + **disagree** with `pyproject.toml`/`meson.build` (4.0.0 vs 5.0.0). See `.agents/TODO.md` P0. + +## Conventions (enforce in review) + +From `.vscode/copilot-instructions.md`, as amended by PM direction: +- **Comments (PM-amended):** *minimal inline comments* only, on genuinely non-obvious / + "magic" code (a tricky index trick, a non-obvious workaround). NOT module/class/method + docstrings, NOT line-by-line narration. Keep it sparse. Still no `// ...existing code...` + placeholder markers. +- **No unused/dangling** imports or variables. **No extra** methods/types unless necessary. +- **Avoid deep nesting:** ~3 levels of nesting is the signal to extract a helper function. + Prefer small focused functions + early returns over pyramids of `if`/`for`. +- **Double quotes** for strings, always. Match surrounding style; verify meticulously. +- Commits: **Conventional Commits, one-line subject.** + +⚠️ The existing code violates some of this (mixed 2/4-space indent, mixed quotes, stale +`indent-tabs-mode: t` headers). New work should follow the rules above; don't propagate the +old inconsistency. + +GTK4 rules (full list in `.agents/README.md`): single-threaded UI (`GLib.idle_add` to +marshal); no `show_all()`/`container.add()` (use `set_visible`/`append`/`set_child`); prefer +`Adw.Dialog`/`Adw.AlertDialog` over `Gtk.Dialog`/`Gtk.MessageDialog`; prefer `Gtk.FileDialog` +(4.10+) over `Gtk.FileChooserDialog`. + +## Companion docs + +- `.agents/README.md` — index/map of the whole `.agents/` directory. +- `.agents/TODO.md` — working backlog (P-levels; Done at the bottom). +- `.agents/plans/` — forward-looking implementation plans. +- `.agents/skills/` — `developing-gtk-apps` (architecture) + `designing-gnome-ui` (HIG) + + `glc-format` (authoring `.glc` circuit files). +- `.agents/reference/` — `delegation-brief` (read first when delegating) + + `build-system-analysis` (build audit). +- `.agents/gtk4/` — raw HTML API docs by namespace (adwaita/gtk/pango/gio/cairo); + `.agents/gtk4/README.md` is the widget → where-used-in-GGate map. diff --git a/INSTALL.md b/INSTALL.md index 424b7ef..f7ab747 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,27 +1,31 @@ # Installation Instructions -## __Required Packages__ *optional +## From source (development) - On Debian systems, you can install dependencies with the following command: +Make sure the system dependencies are present: Python `>= 3.10`, GTK `>= 4.16`, +libadwaita `>= 1.6.8`, and the Python packages `pygobject`, `pycairo`, `python-igraph`, +`shapely`, `packaging`. - ```bash - sudo apt-get install python3-gi python3-gi-cairo libgtk-4-0 - ``` +Then run, from the project root: -## __Install__ +```bash +python3 run.py +``` -* Download the `.deb` package from release and install. -* Clone the repo and install from source +You can pass a `.glc` circuit file to open it on launch: - ```bash - git clone http://github.com/astraldev/GGate - cd GGate - sudo pip install . - ``` +```bash +python3 run.py data/examples/oscillators.glc +``` -Or install using __snap__ +## Flatpak -> Coming soon +*Coming soon* -Or install using __flatpak__ -> Coming soon +## Snap + +*Coming soon* + +## Deb + +*Coming soon* diff --git a/NEWS b/NEWS index ea3798d..1d1127f 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,15 @@ # NEWS +2026-06: version 5.0.0 + +* Animated simulation playback with a floating transport bar (stop, restart, play/pause, seek) +* Playback duration preference; only circuits that evolve over time animate +* Simplified header simulation controls to Play/Stop +* Recompute the simulation on live edits (switch toggle / property change) +* New themed component icon set (ANSI + IEC), with light/dark theming +* Revamped timing diagram (Adw dialog, themed Cairo waveforms) +* Migrated dialogs to libadwaita; build via meson, with CI + 2023-09-03: version 4.0.0-rc * Changed name to aid publishing diff --git a/README.md b/README.md index bf31796..b044922 100644 --- a/README.md +++ b/README.md @@ -2,56 +2,80 @@ ![GGate Logo](./data/images/apps/256x256/ggate.png) -A logic gate simulator for linux developed with Gtk and python. +A logic-circuit simulator for Linux, built with **Python**, **GTK 4** and **libadwaita**. +Place logic gates and components on a Cairo-rendered canvas, wire them into nets, run the +simulation, and watch the circuit evolve. -## Requirements +[![GGate simulating an oscillator circuit with the playback bar](./gallery/ggate-playback.png)](./gallery/ggate-demo.mp4) -This application requires any linux environment on which **GTK4** is installed. Or **Ubuntu ^22.10** on windows using **WSL** +▶ [Watch the 30s demo](./gallery/ggate-demo.mp4) — or browse the full [gallery](./gallery). ## Features -* Includes Tri State Buffer -* LED and 7 segment for state viewing -* Easy to understand UI and accesible shortcuts -* Contains all logic gates with appropriate diagrams and alternative (IEC, MIL/ANSI) -* Supports Simulation and Timing Diagrams -* Support exporting of drawings and timing diagrams. -* Variable component properties -* Contains all types Flip Flop -* Has display contents like LED, 7 Segments etc -* File handling support +* All standard logic gates, with both **IEC** and **MIL/ANSI** symbol sets +* Flip-flops, counters, shift registers (SISO/SIPO/PISO/PIPO) and a tri-state buffer +* State viewers — LEDs and a 7-segment display +* Interactive switches and a clock oscillator +* **Simulation** with an animated, **record-then-play** playback and a floating transport bar + (stop, restart, play/pause, seek, live sim-time) +* **Timing diagrams** for the simulated signals +* Export drawings and timing diagrams (PNG / PDF / PS / SVG) +* Per-component editable properties +* Light/dark theming (Classic, Space, Frappé, Mocha) +* Open/save circuits as `.glc` files -Check out the latest changelog on GGate [here.](./NEWS) and [future plans](./TODO) +See the [changelog](./CHANGELOG.md) for recent changes and the [roadmap](./TODO) for what's planned. -## Installation +## Screenshots -To run without installing -```bash -python3 run.py -``` +| Timing diagram | Preferences | +|---|---| +| ![Timing diagram](./gallery/ggate-timing-diagram.png) | ![Preferences](./gallery/ggate-preferences.png) | -GGate will soon be available on snapcraft, flatpak and normal apt. View [installation](./INSTALL.md) for more details +Themes ship in light and dark variants: -## Screenshots +| Classic | Space (Dark) | Mocha (Light) | +|---|---|---| +| ![Classic theme](./gallery/ggate-theme-classic.png) | ![Space dark theme](./gallery/ggate-theme-space-dark.png) | ![Mocha light theme](./gallery/ggate-theme-mocha-light.png) | -### Main window +More previews live in [`gallery/`](./gallery). -![Main window](./screenshots/ggate-window.png) +## From v1 to v5 -### Tool Bar +GGate began life in 2012 as **GLogic**, a wxWidgets desktop app. It has since been rebuilt into +a modern GNOME application: -![Tool Bar](./screenshots/ggate-toolbar.png) +* **v1 (2012)** — initial release; basic gates, oscillator, probe, timing chart. +* **v2 (2012)** — migrated wxWidgets → GTK+; added the property window, IEC symbols and preferences. +* **v3 (2022)** — ported to **GTK 4**, added the tri-state buffer and shift registers. +* **v4 (2023)** — renamed to GGate for publishing; revamped the timing dialog. +* **v5 (2026)** — libadwaita throughout, a themed component icon set (ANSI + IEC) with light/dark + themes, a meson build with CI, and **animated record-then-play simulation** with a floating + transport bar and live recompute on edits. + +## Requirements -## Contribution +* Python `>= 3.10` +* GTK `>= 4.16`, libadwaita `>= 1.6.8` +* Python packages: `pygobject`, `pycairo`, `python-igraph`, `shapely`, `packaging` -GGate is open to any contributions and translation aid. +## Running -### How To Contribute +To run from source without installing: -Fork this repo, make your edits and submit a pull request +```bash +python3 run.py +``` + +To open a circuit on launch, pass a `.glc` file (examples live in [`data/examples/`](./data/examples)): + +```bash +python3 run.py data/examples/oscillators.glc +``` -## Donation +Packaged builds (Snap, Flatpak, and meson install) are in progress — see [INSTALL.md](./INSTALL.md). -GGate project is open to funding and donation support. +## Contributing -[![Download GGate](https://a.fsdn.com/con/app/sf-download-button)](https://sourceforge.net/projects/ggate/files/latest/download) +Contributions and translation help are welcome. Fork the repository, make your changes, and +open a pull request. diff --git a/TODO b/TODO index 4f59dcc..25e1684 100644 --- a/TODO +++ b/TODO @@ -1,38 +1,15 @@ # Todo -* Animate logic circuit +Roadmap of upcoming work. The detailed working backlog lives in `.agents/TODO.md`. +## Planned -### Icons +* Packaging: Flatpak, Snap and Deb builds +* Guard against over-large circuits (warn before a load/save/simulation hangs) +* Auto-scroll the timing diagram to follow the latest timestamp while running +* Playback polish: frame subsampling for very long timelines, optional per-session speed -> Done +## Done -- tff -- rsff -- jkff -- counter -- dff -- 7seg -- PIPO -- PISO -- SISO -- SIPO -- counter -- dff -- adder -- and -- nand -- gnd -- not -- or -- xor -- nor - -> To convert - -- vdd -- tribuff -- sw -- probe -- osc -- led +* Animated logic-circuit playback with a floating transport bar +* Themed component icon set (all components, ANSI + IEC variants) diff --git a/bin/ggate b/bin/ggate deleted file mode 100755 index ee5ed1f..0000000 --- a/bin/ggate +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- -# -# Copyright (C) Koichi Akabe 2012 -# -# GGate is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# GGate is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see . -# - -import gi, sys - -gi.require_version("Gtk", '4.0') -gi.require_version('PangoCairo', '1.0') - -from ggate.MainFrame import GLogicApplication - -if __name__ == "__main__": - app = GLogicApplication() - if len(sys.argv) > 1 and sys.argv[1] == "--version": - print(f"My GTK Application Version {__VERSION__}") - sys.exit(0) - - app.run() diff --git a/bin/ggate.in b/bin/ggate.in new file mode 100755 index 0000000..cfdad0b --- /dev/null +++ b/bin/ggate.in @@ -0,0 +1,26 @@ +#!@PYTHON@ + +import os +import gi +import sys + +gi.require_version("Gtk", "4.0") +gi.require_version("PangoCairo", "1.0") +gi.require_version("Adw", "1") + +from gi.repository import Gio +from ggate import __version__ +from ggate.MainFrame import GLogicApplication + +pkgdatadir = "@pkgdatadir@" + +if __name__ == "__main__": + resource = Gio.Resource.load(os.path.join(pkgdatadir, "ggate-resources.gresource")) + resource._register() + + app = GLogicApplication() + if len(sys.argv) > 1 and sys.argv[1] == "--version": + print(f"GGate {__version__}") + sys.exit(0) + + app.run() diff --git a/bin/meson.build b/bin/meson.build new file mode 100644 index 0000000..689b90b --- /dev/null +++ b/bin/meson.build @@ -0,0 +1,8 @@ +configure_file( + input: './ggate.in', + output: 'ggate', + configuration: conf, + install: true, + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', +) \ No newline at end of file diff --git a/build-aux/capture_screenshots.py b/build-aux/capture_screenshots.py new file mode 100644 index 0000000..3695f12 --- /dev/null +++ b/build-aux/capture_screenshots.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Procedurally capture a gallery of GGate previews — themes, playback, timing +diagram, preferences and properties — to ./gallery/. + +Bootstraps exactly like run.py (compiles + registers the dev gresource, so themes +and icons load), then drives the real app through a sequence of "scenes", grabbing +an in-app snapshot of each (no external screenshot tool; X11 and Wayland both work). + +Run on a real desktop session with GTK4/libadwaita: + + python3 build-aux/capture_screenshots.py + +Must NOT run under a setuid/setgid shell (GTK refuses to init there). +""" + +import os +import re +import sys +import unicodedata + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("PangoCairo", "1.0") +gi.require_version("Adw", "1") + +from gi.repository import Gtk, Gdk, Gio, GLib, Adw + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT) + +import run as ggate_run # noqa: E402 (run.py's bootstrap helpers) +from ggate import Preference # noqa: E402 +from ggate.Themes import THEME_REGISTRY, apply_theme, apply_chrome # noqa: E402 +from ggate.MainFrame import GLogicApplication # noqa: E402 +from ggate.const import definitions as const # noqa: E402 +from ggate.Components.Windows.Preferences import PreferencesWindow # noqa: E402 + +EXAMPLE = os.path.join(ROOT, "data", "examples", "oscillators.glc") +OUTDIR = os.path.join(ROOT, "gallery") +WINDOW_SIZE = (1280, 800) +SETTLE_MS = 800 # repaint/animation settle before a grab +GAP_MS = 350 # pause between scenes +RUN_SETTLE_MS = 2200 # let compute finish + playback start + + +def slug(text): + text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode() + return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", text.lower())).strip("-") + + +def snapshot(window, name): + width, height = window.get_width(), window.get_height() + if width <= 0 or height <= 0: + print("skip (not realized):", name) + return + paintable = Gtk.WidgetPaintable.new(window) + snap = Gtk.Snapshot.new() + paintable.snapshot(snap, width, height) + node = snap.to_node() + if node is None: + print("skip (empty node):", name) + return + texture = window.get_native().get_renderer().render_texture(node, None) + path = os.path.join(OUTDIR, f"ggate-{name}.png") + texture.save_to_png(path) + print("saved", path) + + +def first_component(win): + # prefer a component that actually has editable properties for a richer dialog + comps = [c for c in win.circuit.components if c[0] != const.component_net] + for c in comps: + if getattr(c[1], "properties", None): + return c + return comps[0] if comps else None + + +def build_scenes(win): + scenes = [] + + # one canvas/palette shot per registered theme + for name in THEME_REGISTRY: + def setup(w, n=name): + apply_theme(n, Preference) + apply_chrome(n, Gdk.Display.get_default()) + # match the libadwaita chrome to the theme variant (screenshot only) + scheme = Adw.ColorScheme.FORCE_LIGHT if "Light" in n else Adw.ColorScheme.FORCE_DARK + Adw.StyleManager.get_default().set_color_scheme(scheme) + w.drawarea.redraw = True + w.drawarea.queue_draw() + scenes.append({"name": f"theme-{slug(name)}", "setup": setup}) + + # start a run -> playback bar visible + scenes.append({ + "name": "playback", + "setup": lambda w: w.toggle_run(), + "settle": RUN_SETTLE_MS, + }) + + # timing diagram (history exists from the run above) + scenes.append({ + "name": "timing-diagram", + "setup": lambda w: w.timing_diagram.display(), + "teardown": lambda w: w.timing_diagram.force_close(), + }) + + # stop the run, back to edit mode + scenes.append({ + "name": None, + "setup": lambda w: w.action_run.set_active(False), + }) + + # preferences dialog + def open_prefs(w): + w._gallery_prefs = PreferencesWindow(w) + w._gallery_prefs.present(w) + scenes.append({ + "name": "preferences", + "setup": open_prefs, + "teardown": lambda w: w._gallery_prefs.force_close(), + }) + + # properties dialog for the first real component + def open_props(w): + comp = first_component(w) + if comp is not None: + w.prop_window.show_properties(comp[1], w) + scenes.append({ + "name": "properties", + "setup": open_props, + "teardown": lambda w: w.prop_window.dismiss(), + }) + + return scenes + + +def main(): + os.makedirs(OUTDIR, exist_ok=True) + + # same bootstrap as run.py so the dev gresource (themes, icons) is live + ggate_run.build_dev_resources() + try: + Gio.Resource.load(os.path.join(ROOT, "dev-resources.gresource"))._register() + except GLib.Error as exc: + print("warning: could not register dev gresource:", exc) + + sys.argv = [sys.argv[0], EXAMPLE] # MainFrame opens sys.argv[1] on startup + app = GLogicApplication() + + def on_ready(_app): + win = app.get_active_window() + win.set_default_size(*WINDOW_SIZE) + win.present() + scenes = build_scenes(win) + + def run_scene(i): + if i >= len(scenes): + app.quit() + return False + scene = scenes[i] + try: + if scene.get("setup"): + scene["setup"](win) + except Exception as exc: # one bad scene shouldn't abort the gallery + print(f"scene {scene['name']} setup failed:", exc) + GLib.timeout_add(scene.get("settle", SETTLE_MS), lambda: capture(i)) + return False + + def capture(i): + scene = scenes[i] + try: + if scene.get("name"): + snapshot(win, scene["name"]) + if scene.get("teardown"): + scene["teardown"](win) + except Exception as exc: + print(f"scene {scene['name']} capture failed:", exc) + GLib.timeout_add(GAP_MS, lambda: run_scene(i + 1)) + return False + + GLib.timeout_add(900, lambda: run_scene(0)) + + app.connect_after("activate", on_ready) + app.run([sys.argv[0]]) + + +if __name__ == "__main__": + main() diff --git a/build-aux/debian/control b/build-aux/debian/control new file mode 100644 index 0000000..d92f7ad --- /dev/null +++ b/build-aux/debian/control @@ -0,0 +1,31 @@ +Source: ggate +Section: utils +Priority: optional +Maintainer: Astraldev +Build-Depends: debhelper-compat (= 13), + meson (>= 1.4.0), + ninja-build, + pkg-config, + python3-dev, + python3-gi-dev, + libcairo2-dev, + libgtk-4-dev (>= 4.16.0), + libadwaita-1-dev (>= 1.6.8), + dh-python +Standards-Version: 4.6.2 +Homepage: https://github.com/astraldev/GGate + +Package: ggate +Architecture: any +Depends: ${python3:Depends}, + ${misc:Depends}, + python3-gi, + python3-gi-cairo, + python3-igraph, + python3-shapely, + python3-packaging, + libgtk-4-1 (>= 4.16.0), + libadwaita-1-0 (>= 1.6.8) +Description: A digital logic circuit simulator and designer + GGate is a digital logic circuit simulator that allows you to design + and test digital circuits using standard logic gates and components. diff --git a/build-aux/debian/copyright b/build-aux/debian/copyright new file mode 100644 index 0000000..cba1d8c --- /dev/null +++ b/build-aux/debian/copyright @@ -0,0 +1,7 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ggate +Source: https://github.com/astraldev/GGate + +Files: * +Copyright: 2026 Astraldev +License: GPL-3.0+ diff --git a/build-aux/debian/rules b/build-aux/debian/rules new file mode 100755 index 0000000..2e113b3 --- /dev/null +++ b/build-aux/debian/rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 + +%: + dh $@ --with python3 --buildsystem=meson diff --git a/build-aux/flatpak/org.astralco.ggate.json b/build-aux/flatpak/org.astralco.ggate.json new file mode 100644 index 0000000..40c1599 --- /dev/null +++ b/build-aux/flatpak/org.astralco.ggate.json @@ -0,0 +1,46 @@ +{ + "app-id" : "org.astralco.ggate", + "runtime" : "org.gnome.Platform", + "runtime-version" : "48", + "sdk" : "org.gnome.Sdk", + "command" : "ggate", + "finish-args" : [ + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--device=dri", + "--filesystem=home" + ], + "modules" : [ + { + "name": "python-modules", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --prefix=${FLATPAK_DEST} pycairo pygobject igraph shapely packaging" + ], + "build-options": { + "build-args": [ + "--share=network" + ] + }, + "sources": [] + }, + { + "name" : "ggate", + "builddir" : true, + "buildsystem" : "meson", + "sources" : [ + { + "type" : "dir", + "path" : "../../" + } + ], + "config-opts" : [ + "--libdir=lib" + ] + } + ], + "build-options" : { + "env" : { } + } +} diff --git a/build-aux/gen-deb-changelog.py b/build-aux/gen-deb-changelog.py new file mode 100644 index 0000000..8c1c102 --- /dev/null +++ b/build-aux/gen-deb-changelog.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# Generate multi-entry debian/changelog from meson.build version and CHANGELOG.md. +import os +import re +import sys +import datetime +import email.utils + +VERSION_PATTERNS = [ + re.compile(r"[Bb]ump version to (\d+\.\d+(?:\.\d+)?)"), + re.compile(r"[Rr]elease version (\d+\.\d+(?:\.\d+)?)"), + re.compile(r"[Bb]ump to (\d+\.\d+(?:\.\d+)?)"), + re.compile(r"(?:^|\s)[vV](\d+\.\d+(?:\.\d+)?)") +] + +class Section: + def __init__(self, date_str, author): + # Section attributes + self.date_str = date_str + self.author = author + self.bullets = [] + self.explicit_version = None + self.version = None + +def get_meson_version(meson_path): + # Extract version from meson.build + with open(meson_path, "r", encoding="utf-8") as f: + content = f.read() + match = re.search(r"version\s*:\s*'([0-9][^']*)'", content) + if match: + return match.group(1) + return "0.0.0" + +def extract_version(line): + # Find explicit version in a line + for pattern in VERSION_PATTERNS: + match = pattern.search(line) + if match: + return match.group(1) + return None + +def parse_header(line): + # Parse header fields + content = line[2:].strip() + parts = content.split(None, 1) + if len(parts) < 2: + return None, None + return parts[0], parts[1].strip() + +def parse_changelog(changelog_path): + # Parse changelog sections + sections = [] + current_section = None + with open(changelog_path, "r", encoding="utf-8") as f: + for line in f: + if line.startswith("## "): + date_str, author = parse_header(line) + if date_str: + current_section = Section(date_str, author) + sections.append(current_section) + elif current_section is not None: + stripped = line.strip() + if stripped: + current_section.bullets.append(stripped) + ver = extract_version(stripped) + if ver and current_section.explicit_version is None: + current_section.explicit_version = ver + return sections + +def resolve_versions(sections, meson_version): + # Assign versions to stanzas + if not sections: + return + + sections[0].version = meson_version + for i in range(1, len(sections)): + if sections[i].explicit_version: + sections[i].version = sections[i].explicit_version + + is_anchor = [s.version is not None for s in sections] + + for i in range(len(sections)): + if not is_anchor[i]: + ref_ver = "0" + for j in range(i - 1, -1, -1): + if is_anchor[j]: + ref_ver = sections[j].version + break + + a = i + while a > 0 and not is_anchor[a - 1]: + a -= 1 + b = i + while b < len(sections) - 1 and not is_anchor[b + 1]: + b += 1 + + n_val = b - i + 1 + sections[i].version = f"{ref_ver}~dev{n_val}" + +def format_rfc2822_date(date_str): + # Format date as RFC-2822 + try: + dt = datetime.datetime.strptime(date_str, "%Y-%m-%d").astimezone() + return email.utils.format_datetime(dt) + except Exception: + return email.utils.formatdate(localtime=True) + +def generate_changelog(sections): + # Print formatted stanzas to stdout + for i, sec in enumerate(sections): + rfc_date = format_rfc2822_date(sec.date_str) + print(f"ggate ({sec.version}-1) unstable; urgency=medium\n") + for bullet in sec.bullets: + print(f" * {bullet}") + print(f"\n -- {sec.author} {rfc_date}") + if i < len(sections) - 1: + print() + +def main(): + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + meson_path = os.path.join(root, "meson.build") + changelog_path = os.path.join(root, "CHANGELOG.md") + + meson_version = get_meson_version(meson_path) + sections = parse_changelog(changelog_path) + resolve_versions(sections, meson_version) + generate_changelog(sections) + +if __name__ == "__main__": + main() diff --git a/build-aux/generate-resources-xml.py b/build-aux/generate-resources-xml.py new file mode 100755 index 0000000..995e72f --- /dev/null +++ b/build-aux/generate-resources-xml.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import os +import glob +import sys +import argparse + +def main(): + parser = argparse.ArgumentParser(description="Generate GResource XML file dynamically") + parser.add_argument("--prefix", required=True, help="GResource prefix") + parser.add_argument("--output", required=True, help="Output XML file path") + parser.add_argument("--source-dir", required=True, help="Source data/images directory") + args = parser.parse_args() + + # Base directory is data/images + images_dir = os.path.abspath(args.source_dir) + # Project data dir is parent of images_dir (which is data/) + data_dir = os.path.dirname(images_dir) + + comp_files = sorted(glob.glob(os.path.join(images_dir, "components", "*.svg"))) + act_files = sorted(glob.glob(os.path.join(images_dir, "actions", "*.svg"))) + theme_files = sorted(glob.glob(os.path.join(data_dir, "themes", "*.json"))) + + lines = [] + lines.append('') + lines.append('') + lines.append(f' ') + + # Add components and actions (relative to images_dir) mapped to standard icon paths + # Collect all component file base names (without extension) + comp_basenames = {os.path.splitext(os.path.basename(f))[0] for f in comp_files} + + for f in comp_files: + rel = os.path.relpath(f, images_dir).replace("\\", "/") + basename = os.path.basename(f) + name_no_ext = os.path.splitext(basename)[0] + if name_no_ext.endswith("-iec"): + base_name = name_no_ext[:-4] + lines.append(f' {rel}') + else: + lines.append(f' {rel}') + if f"{name_no_ext}-iec" not in comp_basenames: + lines.append(f' {rel}') + for f in act_files: + rel = os.path.relpath(f, images_dir).replace("\\", "/") + basename = os.path.basename(f) + name_no_ext = os.path.splitext(basename)[0] + lines.append(f' {rel}') + + # Add themes (alias is basename, path relative to images_dir) + for f in theme_files: + rel = os.path.relpath(f, images_dir).replace("\\", "/") + basename = os.path.basename(f) + lines.append(f' {rel}') + + # Add index.theme + if os.path.exists(os.path.join(images_dir, "index.theme")): + lines.append(' index.theme') + + lines.append(' ') + lines.append('') + + with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + +if __name__ == "__main__": + main() diff --git a/build-aux/make_demo.sh b/build-aux/make_demo.sh new file mode 100755 index 0000000..53d6ffa --- /dev/null +++ b/build-aux/make_demo.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# Turn a screen recording into the demo clip used in the README / gallery: +# trimmed, scaled to 1080p, 30fps, h264 (web-friendly, no audio). +# +# Usage: +# build-aux/make_demo.sh INPUT [START] [DURATION] [OUTPUT] +# +# Defaults: START=25.7s, DURATION=30s, OUTPUT=gallery/ggate-demo.mp4 +# +set -euo pipefail + +SRC="${1:?usage: make_demo.sh INPUT [START] [DURATION] [OUTPUT]}" +START="${2:-25.7}" +DURATION="${3:-30}" +OUT="${4:-gallery/ggate-demo.mp4}" + +ffmpeg -hide_banner -loglevel error \ + -ss "$START" -t "$DURATION" -i "$SRC" \ + -vf "fps=30,scale=-2:1080:flags=lanczos" \ + -c:v libx264 -crf 23 -preset slow -pix_fmt yuv420p \ + -movflags +faststart -an -y "$OUT" + +echo "wrote $OUT" diff --git a/build-aux/meson-exclude-pycache.sh b/build-aux/meson-exclude-pycache.sh new file mode 100755 index 0000000..248c91c --- /dev/null +++ b/build-aux/meson-exclude-pycache.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Emit __pycache__ dirs relative to the first arg, for meson's +# install_subdir exclude_directories property. + +# enter the package dir meson passed as $1, or exit quietly if it's missing +cd "$1" 2>/dev/null || exit 0 + +# list every __pycache__ dir, stripping the leading "./" so paths stay relative +find . -type d -name '__pycache__' | sed 's|^\./||' diff --git a/build-aux/snapcraft.yaml b/build-aux/snapcraft.yaml deleted file mode 100644 index 218a101..0000000 --- a/build-aux/snapcraft.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: ggate # you probably want to 'snapcraft register ' -base: core22 # the base snap is the execution environment for this snap -version: '4.0.0' # just for humans, typically '1.2+git' or '1.3.2' -summary: A logic gate circuit simulator # 79 char long summary -description: | - A logic gate simulator for linux developed with Gtk and python. -grade: stable # must be 'stable' to release into candidate/stable channels -confinement: strict - -architectures: - - build-on: amd64 - -plugs: - gtk-3-themes: - interface: content - target: $SNAP/data-dir/themes - default-provider: gtk-common-themes - icon-themes: - interface: content - target: $SNAP/data-dir/icons - default-provider: gtk-common-themes - -apps: - ggate: - command: ggate - plugs: [home] - -parts: - ggate: - plugin: python - source: https://github.com/astraldev/GGate.git - stage-packages: - - python3-gi - - python3-gi-cairo - diff --git a/build-aux/snapcraft/snapcraft.yaml b/build-aux/snapcraft/snapcraft.yaml new file mode 100644 index 0000000..622851c --- /dev/null +++ b/build-aux/snapcraft/snapcraft.yaml @@ -0,0 +1,76 @@ +name: ggate +base: core22 +version: '5.0.0' +summary: A digital logic circuit simulator and designer +description: | + GGate is a digital logic circuit simulator that allows you to design + and test digital circuits using standard logic gates and components. + Features include: + * Interactive circuit design + * Real-time simulation + * Timing diagram analysis + * Support for basic and complex logic gates + * Save and load circuit designs +grade: stable +confinement: strict + +architectures: + - build-on: amd64 + +plugs: + gtk-4-themes: + interface: content + target: $SNAP/data-dir/themes + default-provider: gtk-common-themes:gtk-4-themes + icon-themes: + interface: content + target: $SNAP/data-dir/icons + default-provider: gtk-common-themes + desktop: + interface: desktop + wayland: + interface: wayland + x11: + interface: x11 + +apps: + ggate: + command: bin/ggate + extensions: [gnome] + plugs: + - home + - desktop + - wayland + - x11 + - gtk-4-themes + - icon-themes + +parts: + poetry-deps: + plugin: nil + source: . + override-build: | + curl -sSL https://install.python-poetry.org | python3 - + export PATH="/root/.local/bin:$PATH" + poetry config virtualenvs.create false + poetry install --no-dev --no-interaction + build-packages: + - python3 + - curl + + ggate: + after: [poetry-deps] + plugin: meson + source: . + meson-parameters: + - --prefix=/snap/ggate/current/usr + build-packages: + - pkg-config + - python3-dev + - python3-gi-dev + - libcairo2-dev + stage-packages: + - libgtk-4-1 + - libadwaita-1-0 + - python3-gi + - python3-gi-cairo diff --git a/examples/RSFF.glc b/data/examples/RSFF.glc similarity index 100% rename from examples/RSFF.glc rename to data/examples/RSFF.glc diff --git a/examples/TFF-counter.glc b/data/examples/TFF-counter.glc similarity index 100% rename from examples/TFF-counter.glc rename to data/examples/TFF-counter.glc diff --git a/data/examples/adder.glc b/data/examples/adder.glc new file mode 100644 index 0000000..420c47c --- /dev/null +++ b/data/examples/adder.glc @@ -0,0 +1,508 @@ +version + author=5.0.0 + required=2.6.0 + +meta + zoom=0.9424371608722886 + +sw + position=410.0,750.0 + input_level=0 + output_level=1 + matrix=1,0,0,1 + properties=initstate:0 + +sw + position=410.0,700.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=initstate:1 + +sw + position=410.0,650.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=initstate:1 + +sw + position=410.0,600.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=initstate:0 + +7seg + position=430.0,300.0 + input_level=0,0,0,1 + output_level=0 + matrix=1,0,0,1 + properties=_red:2 + +7seg + position=560.0,300.0 + input_level=0,0,0,1 + output_level=0 + matrix=1,0,0,1 + properties=_red:1 + +7seg + position=990.0,300.0 + input_level=0,0,1,0 + output_level=0 + matrix=1,0,0,1 + properties=_red:0 + +sw + position=540.0,600.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=initstate:0 + +sw + position=540.0,650.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=initstate:1 + +sw + position=540.0,700.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=initstate:0 + +sw + position=540.0,750.0 + input_level=0 + output_level=1 + matrix=1,0,0,1 + properties=initstate:1 + +and + position=780.0,420.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +and + position=780.0,470.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +and + position=780.0,550.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +and + position=780.0,600.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=680.0,460.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=680.0,590.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=780.0,370.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=870.0,380.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=870.0,510.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=870.0,640.0 + input_level=0,0 + output_level=1 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +xor + position=870.0,750.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +and + position=870.0,700.0 + input_level=0,0 + output_level=1 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +or + position=870.0,590.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +or + position=870.0,460.0 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2.0,tphl:0.0,tplh:0.0 + +text + position=410.0,550.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=text:IN1 + +text + position=540.0,550.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=text:IN2 + +text + position=1010.0,320.0 + input_level=0 + output_level=0 + matrix=1,0,0,1 + properties=text:OUT \e IN1 + IN2 + +net + position=880.0,610.0,780.0,610.0 + +net + position=790.0,570.0,780.0,570.0 + +net + position=780.0,610.0,780.0,570.0 + +net + position=880.0,560.0,880.0,530.0 + +net + position=880.0,430.0,880.0,400.0 + +net + position=880.0,480.0,780.0,480.0 + +net + position=780.0,480.0,780.0,440.0 + +net + position=790.0,440.0,780.0,440.0 + +net + position=970.0,680.0,970.0,650.0 + +net + position=790.0,590.0,790.0,630.0 + +net + position=970.0,650.0,880.0,650.0 + +net + position=790.0,630.0,880.0,630.0 + +net + position=880.0,650.0,880.0,630.0 + +net + position=970.0,570.0,970.0,520.0 + +net + position=790.0,460.0,790.0,500.0 + +net + position=970.0,520.0,880.0,520.0 + +net + position=790.0,500.0,880.0,500.0 + +net + position=880.0,520.0,880.0,500.0 + +net + position=970.0,440.0,970.0,390.0 + +net + position=970.0,390.0,880.0,390.0 + +net + position=880.0,390.0,880.0,370.0 + +net + position=440.0,250.0,420.0,250.0 + +net + position=790.0,410.0,690.0,410.0 + +net + position=420.0,250.0,420.0,380.0 + +net + position=690.0,410.0,690.0,430.0 + +net + position=420.0,380.0,490.0,380.0 + +net + position=690.0,430.0,490.0,430.0 + +net + position=490.0,380.0,490.0,430.0 + +net + position=480.0,630.0,490.0,630.0 + +net + position=490.0,430.0,490.0,630.0 + +net + position=570.0,290.0,570.0,340.0 + +net + position=880.0,720.0,880.0,690.0 + +net + position=570.0,340.0,640.0,340.0 + +net + position=880.0,690.0,640.0,690.0 + +net + position=640.0,340.0,640.0,690.0 + +net + position=610.0,730.0,640.0,730.0 + +net + position=640.0,690.0,640.0,730.0 + +net + position=1000.0,230.0,970.0,230.0 + +net + position=970.0,230.0,970.0,360.0 + +net + position=1000.0,250.0,980.0,250.0 + +net + position=980.0,250.0,980.0,490.0 + +net + position=980.0,490.0,970.0,490.0 + +net + position=1000.0,270.0,990.0,270.0 + +net + position=990.0,270.0,990.0,620.0 + +net + position=990.0,620.0,970.0,620.0 + +net + position=1000.0,290.0,1000.0,730.0 + +net + position=1000.0,730.0,970.0,730.0 + +net + position=440.0,270.0,430.0,270.0 + +net + position=690.0,580.0,680.0,580.0 + +net + position=430.0,270.0,430.0,360.0 + +net + position=790.0,520.0,680.0,520.0 + +net + position=680.0,580.0,680.0,520.0 + +net + position=430.0,360.0,500.0,360.0 + +net + position=680.0,520.0,500.0,520.0 + +net + position=500.0,360.0,500.0,520.0 + +net + position=480.0,680.0,500.0,680.0 + +net + position=500.0,520.0,500.0,680.0 + +net + position=440.0,290.0,440.0,340.0 + +net + position=880.0,740.0,870.0,740.0 + +net + position=440.0,340.0,510.0,340.0 + +net + position=480.0,730.0,510.0,730.0 + +net + position=870.0,740.0,870.0,670.0 + +net + position=880.0,670.0,870.0,670.0 + +net + position=510.0,340.0,510.0,550.0 + +net + position=510.0,730.0,510.0,550.0 + +net + position=870.0,670.0,660.0,670.0 + +net + position=510.0,550.0,660.0,550.0 + +net + position=660.0,670.0,660.0,550.0 + +net + position=570.0,270.0,560.0,270.0 + +net + position=560.0,270.0,560.0,360.0 + +net + position=790.0,540.0,690.0,540.0 + +net + position=690.0,560.0,690.0,540.0 + +net + position=560.0,360.0,630.0,360.0 + +net + position=690.0,540.0,630.0,540.0 + +net + position=630.0,360.0,630.0,540.0 + +net + position=610.0,680.0,630.0,680.0 + +net + position=630.0,540.0,630.0,680.0 + +net + position=570.0,250.0,550.0,250.0 + +net + position=550.0,250.0,550.0,380.0 + +net + position=550.0,380.0,620.0,380.0 + +net + position=610.0,630.0,620.0,630.0 + +net + position=620.0,380.0,620.0,450.0 + +net + position=620.0,630.0,620.0,450.0 + +net + position=790.0,390.0,680.0,390.0 + +net + position=690.0,450.0,680.0,450.0 + +net + position=620.0,450.0,680.0,450.0 + +net + position=680.0,390.0,680.0,450.0 + +net + position=570.0,230.0,540.0,230.0 + +net + position=540.0,230.0,540.0,400.0 + +net + position=540.0,400.0,610.0,400.0 + +net + position=610.0,580.0,610.0,400.0 + +net + position=610.0,400.0,660.0,400.0 + +net + position=790.0,340.0,660.0,340.0 + +net + position=660.0,400.0,660.0,340.0 + +net + position=440.0,230.0,410.0,230.0 + +net + position=410.0,230.0,410.0,400.0 + +net + position=410.0,400.0,480.0,400.0 + +net + position=480.0,400.0,480.0,410.0 + +net + position=480.0,580.0,480.0,410.0 + +net + position=790.0,360.0,670.0,360.0 + +net + position=480.0,410.0,670.0,410.0 + +net + position=670.0,360.0,670.0,410.0 \ No newline at end of file diff --git a/data/examples/mixed.glc b/data/examples/mixed.glc new file mode 100644 index 0000000..d98a98a --- /dev/null +++ b/data/examples/mixed.glc @@ -0,0 +1,365 @@ +version + author=5.0.0 + required=2.6.0 + +sw + position=100,100 + input_level= + output_level=0 + matrix=1,0,0,1 + properties=initstate:1 + +sw + position=100,200 + input_level= + output_level=0 + matrix=1,0,0,1 + properties=initstate:0 + +osc + position=100,300 + input_level= + output_level=0 + matrix=1,0,0,1 + properties=period:2.0,shift:0.0,duration:200.0,initstate:0 + +not + position=250,200 + input_level=0 + output_level=1 + matrix=1,0,0,1 + properties=tphl:0.0,tplh:0.0 + +and + position=250,300 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2,tphl:0.0,tplh:0.0 + +xor + position=400,150 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2,tphl:0.0,tplh:0.0 + +or + position=550,150 + input_level=0,0 + output_level=0 + matrix=1,0,0,1 + properties=inputs:2,tphl:0.0,tplh:0.0 + +counter + position=700,300 + input_level=0,0 + output_level=0,0,0,0 + matrix=1,0,0,1 + properties=n:16,bits:4,trig:0,tphl:0.0,tplh:0.0 + +7seg + position=950,300 + input_level=0,0,0,0 + output_level= + matrix=1,0,0,1 + properties=_red:2 + +siso + position=700,450 + input_level=0,0 + output_level=0,1 + matrix=1,0,0,1 + properties=bits:4,trig:0,tphl:0.0,tplh:0.0 + +led + position=950,440 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=_red:2 + +dff + position=700,570 + input_level=0,0 + output_level=0,1 + matrix=1,0,0,1 + properties=trig:0,tphl:0.0,tplh:0.0 + +led + position=950,560 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=_red:2 + +jkff + position=700,700 + input_level=0,0,0 + output_level=0,1 + matrix=1,0,0,1 + properties=trig:0,tphl:0.0,tplh:0.0 + +tff + position=700,800 + input_level=0 + output_level=0,1 + matrix=1,0,0,1 + properties=trig:0,tphl:0.0,tplh:0.0 + +rsff + position=700,900 + input_level=0,0 + output_level=0,1 + matrix=1,0,0,1 + properties=tphl:0.0,tplh:0.0 + +probe + position=950,80 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:CLK_0 + +probe + position=950,140 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:ECLK_0 + +probe + position=950,200 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:RST_0 + +probe + position=950,350 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:CNT_Q0_0 + +probe + position=950,700 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:JK_Q_0 + +probe + position=950,800 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:T_Q_0 + +probe + position=950,900 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:RS_Q_0 + +net + position=170,280,200,280 + +net + position=200,70,200,280 + +net + position=200,280,200,290 + +net + position=200,290,260,290 + +net + position=200,70,960,70 + +net + position=170,80,202,80 + +net + position=202,80,202,270 + +net + position=202,270,260,270 + +net + position=170,180,210,180 + +net + position=210,180,210,190 + +net + position=210,190,210,690 + +net + position=210,690,210,870 + +net + position=210,180,260,180 + +net + position=210,690,710,690 + +net + position=210,870,710,870 + +net + position=210,190,960,190 + +net + position=340,180,370,180 + +net + position=370,140,370,180 + +net + position=370,180,370,270 + +net + position=370,140,410,140 + +net + position=370,270,710,270 + +net + position=350,280,380,280 + +net + position=380,120,380,130 + +net + position=380,130,380,250 + +net + position=380,250,380,280 + +net + position=380,280,380,440 + +net + position=380,440,380,560 + +net + position=380,560,380,670 + +net + position=380,670,380,780 + +net + position=380,120,410,120 + +net + position=380,120,560,120 + +net + position=380,250,710,250 + +net + position=380,440,710,440 + +net + position=380,560,710,560 + +net + position=380,670,710,670 + +net + position=380,780,710,780 + +net + position=380,130,960,130 + +net + position=500,130,520,130 + +net + position=520,130,520,140 + +net + position=520,140,560,140 + +net + position=650,130,670,130 + +net + position=670,130,670,420 + +net + position=670,420,670,540 + +net + position=670,540,670,650 + +net + position=670,650,670,890 + +net + position=670,420,710,420 + +net + position=670,540,710,540 + +net + position=670,650,710,650 + +net + position=670,890,710,890 + +net + position=820,230,850,230 + +net + position=850,230,850,340 + +net + position=850,230,960,230 + +net + position=850,340,960,340 + +net + position=820,250,960,250 + +net + position=820,270,960,270 + +net + position=820,290,960,290 + +net + position=820,420,960,420 + +net + position=810,540,960,540 + +net + position=810,650,860,650 + +net + position=860,650,860,690 + +net + position=860,690,960,690 + +net + position=810,770,870,770 + +net + position=870,770,870,790 + +net + position=870,790,960,790 + +net + position=800,870,880,870 + +net + position=880,870,880,890 + +net + position=880,890,960,890 \ No newline at end of file diff --git a/examples/oscillators.glc b/data/examples/oscillators.glc similarity index 100% rename from examples/oscillators.glc rename to data/examples/oscillators.glc diff --git a/data/examples/timing_polish_verify.glc b/data/examples/timing_polish_verify.glc new file mode 100644 index 0000000..ba5c351 --- /dev/null +++ b/data/examples/timing_polish_verify.glc @@ -0,0 +1,71 @@ +version + author=5.0.0 + required=2.6.0 + +osc + position=340,260 + input_level= + output_level=0 + matrix=1,0,0,1 + properties=period:5.0,shift:0.0,duration:160.0,initstate:0 + +tff + position=420,260 + input_level=0 + output_level=0,1 + matrix=1,0,0,1 + properties=trig:0,tphl:0.0,tplh:0.0 + +tff + position=540,250 + input_level=0 + output_level=0,1 + matrix=1,0,0,1 + properties=trig:0,tphl:0.0,tplh:0.0 + +tff + position=660,240 + input_level=0 + output_level=0,1 + matrix=1,0,0,1 + properties=trig:0,tphl:0.0,tplh:0.0 + +probe + position=420,250 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:CLK + +probe + position=540,240 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:Q0 + +probe + position=660,230 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:Q1 + +probe + position=780,220 + input_level=0 + output_level= + matrix=1,0,0,1 + properties=name:Q2 + +net + position=410,240,430,240 + +net + position=530,230,550,230 + +net + position=650,220,670,220 + +net + position=770,210,790,210 diff --git a/data/images/actions/flip-horizontal.svg b/data/images/actions/flip-horizontal.svg index 63e98ff..3d51541 100644 --- a/data/images/actions/flip-horizontal.svg +++ b/data/images/actions/flip-horizontal.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/data/images/actions/flip-vertical.svg b/data/images/actions/flip-vertical.svg index 3d6c75d..5bf7368 100644 --- a/data/images/actions/flip-vertical.svg +++ b/data/images/actions/flip-vertical.svg @@ -1,2 +1,6 @@ - - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/data/images/actions/rotate-left.svg b/data/images/actions/rotate-left.svg index 2dbeb2c..c901d95 100644 --- a/data/images/actions/rotate-left.svg +++ b/data/images/actions/rotate-left.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/data/images/actions/rotate-right.svg b/data/images/actions/rotate-right.svg index 8c93d43..29abcb6 100644 --- a/data/images/actions/rotate-right.svg +++ b/data/images/actions/rotate-right.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/data/images/add-component.png b/data/images/add-component.png deleted file mode 100644 index 21a6e88..0000000 Binary files a/data/images/add-component.png and /dev/null differ diff --git a/data/images/add-net.png b/data/images/add-net.png deleted file mode 100644 index e749232..0000000 Binary files a/data/images/add-net.png and /dev/null differ diff --git a/data/images/components/7seg.svg b/data/images/components/7seg.svg index d38ec2c..ba2c3aa 100644 --- a/data/images/components/7seg.svg +++ b/data/images/components/7seg.svg @@ -1,16 +1,10 @@ - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/data/images/components/adder.svg b/data/images/components/adder.svg index 0d119fb..8976284 100644 --- a/data/images/components/adder.svg +++ b/data/images/components/adder.svg @@ -1,14 +1,10 @@ - - - - - - - - - - - - - + + + + + + + + + diff --git a/data/images/components/and-iec.svg b/data/images/components/and-iec.svg new file mode 100644 index 0000000..ebfdd59 --- /dev/null +++ b/data/images/components/and-iec.svg @@ -0,0 +1,7 @@ + + + + + + & + diff --git a/data/images/components/and.png b/data/images/components/and.png deleted file mode 100644 index 6f922ac..0000000 Binary files a/data/images/components/and.png and /dev/null differ diff --git a/data/images/components/and.svg b/data/images/components/and.svg index 507ba60..d257666 100644 --- a/data/images/components/and.svg +++ b/data/images/components/and.svg @@ -1,9 +1,6 @@ - - - - - - - - + + + + + diff --git a/data/images/components/counter.svg b/data/images/components/counter.svg index 09a6c46..63d0d90 100644 --- a/data/images/components/counter.svg +++ b/data/images/components/counter.svg @@ -1,19 +1,9 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/data/images/components/dff.svg b/data/images/components/dff.svg index 23aca1c..9b462c4 100644 --- a/data/images/components/dff.svg +++ b/data/images/components/dff.svg @@ -1,17 +1,9 @@ - - - - - - - - - - - - - - - - + + + + + + + + D diff --git a/data/images/components/gnd.svg b/data/images/components/gnd.svg index a700794..6866eac 100644 --- a/data/images/components/gnd.svg +++ b/data/images/components/gnd.svg @@ -1,12 +1,6 @@ - - - - - - - - - - - + + + + + diff --git a/data/images/components/jkff.svg b/data/images/components/jkff.svg index be9fec2..edfac08 100644 --- a/data/images/components/jkff.svg +++ b/data/images/components/jkff.svg @@ -1,27 +1,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/data/images/components/led.png b/data/images/components/led.png deleted file mode 100644 index ce093ec..0000000 Binary files a/data/images/components/led.png and /dev/null differ diff --git a/data/images/components/led.svg b/data/images/components/led.svg new file mode 100644 index 0000000..a43fdba --- /dev/null +++ b/data/images/components/led.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/images/components/nand-iec.svg b/data/images/components/nand-iec.svg new file mode 100644 index 0000000..39f2b32 --- /dev/null +++ b/data/images/components/nand-iec.svg @@ -0,0 +1,8 @@ + + + + + + + & + diff --git a/data/images/components/nand.svg b/data/images/components/nand.svg index c12cbdf..41817f2 100644 --- a/data/images/components/nand.svg +++ b/data/images/components/nand.svg @@ -1,15 +1,7 @@ - - - - - - - - - - - - - - + + + + + + diff --git a/data/images/components/nor-iec.svg b/data/images/components/nor-iec.svg new file mode 100644 index 0000000..bbe5660 --- /dev/null +++ b/data/images/components/nor-iec.svg @@ -0,0 +1,8 @@ + + + + + + + ≥1 + diff --git a/data/images/components/nor.svg b/data/images/components/nor.svg index 3b79a0d..3e5c030 100644 --- a/data/images/components/nor.svg +++ b/data/images/components/nor.svg @@ -1,9 +1,7 @@ - - - - - - - - + + + + + + diff --git a/data/images/components/not-iec.svg b/data/images/components/not-iec.svg new file mode 100644 index 0000000..d819be1 --- /dev/null +++ b/data/images/components/not-iec.svg @@ -0,0 +1,7 @@ + + + + + + 1 + diff --git a/data/images/components/not.png b/data/images/components/not.png deleted file mode 100644 index 9898061..0000000 Binary files a/data/images/components/not.png and /dev/null differ diff --git a/data/images/components/not.svg b/data/images/components/not.svg new file mode 100644 index 0000000..cba9d6c --- /dev/null +++ b/data/images/components/not.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/images/components/or-iec.svg b/data/images/components/or-iec.svg new file mode 100644 index 0000000..6e4ff94 --- /dev/null +++ b/data/images/components/or-iec.svg @@ -0,0 +1,7 @@ + + + + + + ≥1 + diff --git a/data/images/components/or.png b/data/images/components/or.png deleted file mode 100644 index bcef64e..0000000 Binary files a/data/images/components/or.png and /dev/null differ diff --git a/data/images/components/or.svg b/data/images/components/or.svg index 984e4bd..031b4ed 100644 --- a/data/images/components/or.svg +++ b/data/images/components/or.svg @@ -1,11 +1,6 @@ - - - - - - - - - - + + + + + diff --git a/data/images/components/osc.png b/data/images/components/osc.png deleted file mode 100644 index fc17962..0000000 Binary files a/data/images/components/osc.png and /dev/null differ diff --git a/data/images/components/osc.svg b/data/images/components/osc.svg new file mode 100644 index 0000000..cfbfb72 --- /dev/null +++ b/data/images/components/osc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/images/components/pipo.svg b/data/images/components/pipo.svg index fcc7e46..046800d 100644 --- a/data/images/components/pipo.svg +++ b/data/images/components/pipo.svg @@ -1,23 +1,13 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/data/images/components/piso.svg b/data/images/components/piso.svg index 966ca49..65103b0 100644 --- a/data/images/components/piso.svg +++ b/data/images/components/piso.svg @@ -1,19 +1,11 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/data/images/components/probe.png b/data/images/components/probe.png deleted file mode 100644 index 0c1cd83..0000000 Binary files a/data/images/components/probe.png and /dev/null differ diff --git a/data/images/components/probe.svg b/data/images/components/probe.svg new file mode 100644 index 0000000..de26c2b --- /dev/null +++ b/data/images/components/probe.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/images/components/rsff.svg b/data/images/components/rsff.svg index f3a0ec7..e36f92c 100644 --- a/data/images/components/rsff.svg +++ b/data/images/components/rsff.svg @@ -1,14 +1,12 @@ - - - - - - - - - - - RS - - + + + + + + + + + + + diff --git a/data/images/components/sipo.svg b/data/images/components/sipo.svg index 718216c..662010a 100644 --- a/data/images/components/sipo.svg +++ b/data/images/components/sipo.svg @@ -1,24 +1,11 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/data/images/components/siso.svg b/data/images/components/siso.svg index cb97f04..e175979 100644 --- a/data/images/components/siso.svg +++ b/data/images/components/siso.svg @@ -1,15 +1,9 @@ - - - - - - - - - - - - - - + + + + + + + + diff --git a/data/images/components/spec.md b/data/images/components/spec.md new file mode 100644 index 0000000..5e951df --- /dev/null +++ b/data/images/components/spec.md @@ -0,0 +1,169 @@ +# GGate Component SVG Icon Specification + +This document defines the visual design system and technical specifications for all component SVG icons in GGate. Following these specifications ensures visual consistency, crisp pixel alignment, and native Libadwaita theme compatibility (automatic light/dark mode recoloring). + +--- + +## 1. Technical Requirements + +1. **ViewBox Grid:** All component icons must use exactly `viewBox="0 0 28 28"`. No `width` or `height` attributes on ``. +2. **Stroke/Fill Rules:** + - Standard stroke width: `2px`. Secondary detail lines may use `1.5px`. Bold cross-lines (e.g. adder "+") may use `2.5px`. + - Element stroke: `stroke="currentColor"`. + - Element fill: `fill="none"` by default; `fill="currentColor"` only for solid dots/indicators/inversion bubbles. +3. **Corner Treatment:** All strokes use `stroke-linejoin="round"` and `stroke-linecap="round"`. Use `stroke-linecap="butt"` only on seven-segment display segments where clean rectangular ends are required. +4. **Theme Recoloring:** Never use hardcoded hex colors (`#000`, `#fff`, etc.) or inline `style=` attributes. GTK4 recolors dynamically using `currentColor`. +5. **Pictorial Glyphs — No Tiny Text Labels:** The sidebar already displays each component's name as a text label next to the icon. Icons must NOT attempt to spell out the part name in small text. All icons use pictorial line-art glyphs. Avoid `` elements with `font-size` below `~10px` — they are illegible at 22px display size. The only permitted `` elements are large single-letter glyphs (e.g. `font-size="11"` for "D" or "T" in flip-flops, `font-size="18"` for "A" in text.svg). +6. **Allowed Elements:** ``, ``, ``, ``, ``. Keep markup minimal. + +--- + +## 2. Body Dimensions (enforce consistency) + +| Component class | Body rect | Pin stub length | +|-----------------|-----------|-----------------| +| Flip-flops (dff, jkff, rsff, tff) | `x=6 y=4 width=16 height=20 rx=1` | 5 units (`x=1`→`x=6` in; `x=22`→`x=27` out) | +| Block components (adder, counter, siso/sipo/piso/pipo) | `x=5 y=4 width=18 height=20 rx=1` | 4 units (`x=1`→`x=5` in; `x=23`→`x=27` out) | +| 7seg display | `x=8 y=3 width=16 height=22 rx=1.5` | 7 units (`x=1`→`x=8`) | + +--- + +## 3. Component Design Specifications + +### A. Logic Gates + +All standard gates occupy a horizontal span centered at `y = 14`. Input pins enter from `x = 1`, output pins exit to `x = 27`. + +#### 1. `and.svg` +- **Body:** Back line from `(9, 6)` to `(9, 22)`. Top/bottom flat lines to `(16, 6)` / `(16, 22)`. Front semi-circle centered at `(16, 14)` radius `8`. +- **Inputs:** Two lines at `y = 10` and `y = 18` from `x = 1` to `x = 9`. +- **Output:** One line at `y = 14` from `x = 24` to `x = 27`. + +#### 2. `or.svg` +- **Body:** Curved back `M 9,6 C 12,10 12,18 9,22`. Top/bottom curves meeting at `(24, 14)`. +- **Inputs:** Two lines at `y = 10` and `y = 18` from `x = 1` to ~`x = 10.5`. +- **Output:** One line at `y = 14` from `x = 24` to `x = 27`. + +#### 3. `not.svg` +- **Body:** Right-pointing triangle vertices `(7, 6)`, `(7, 22)`, `(18, 14)`. +- **Inversion Bubble:** `circle cx="21" cy="14" r="2.5"`. +- **Input:** `y = 14` from `x = 1` to `x = 7`. +- **Output:** `y = 14` from `x = 23.5` to `x = 27`. + +#### 4. `xor.svg` +- Same as `or.svg` plus extra back-offset curve `M 6,6 C 9,10 9,18 6,22`. Inputs shorten to ~`x = 7.5`. + +#### 5. `nand.svg` +- Same body as `and.svg` (shifted left by 1). Inversion bubble `circle cx="24.5" cy="14" r="1.5"`. + +#### 6. `nor.svg` +- Same body as `or.svg` (shifted left by 1). Inversion bubble `circle cx="24.5" cy="14" r="1.5"`. + +#### 7. `tribuff.svg` (Tri-State Buffer) +- **Body:** Right-pointing triangle `(7, 6)`, `(7, 22)`, `(19, 14)`. +- **Inputs:** Data at `y = 14` from `x = 1`; control from `(13, 1)` to `(13, 10)`. +- **Output:** `y = 14` from `x = 19` to `x = 27`. + +--- + +### B. Flip-Flops + +All flip-flops use body `rect x="6" y="4" width="16" height="20" rx="1"`. Input stubs run from `x=1` to `x=6`; output stubs from `x=22` to `x=27`. Clock-edge triangles point inward from the left wall. The combination of pin count, clock triangle position, and interior glyph distinguishes each type. Sidebar text labels provide the definitive name. + +#### 8. `dff.svg` — D Flip-Flop +- **Inputs:** D at `y = 8`; clock at `y = 20`. +- **Clock triangle:** `M 6,17 L 9,20 L 6,23` (at bottom-left, matching the clock pin at `y=20`). +- **Outputs:** Q at `y = 8`; ~Q at `y = 20`. +- **Interior glyph:** Large `"D"` centered at `(14, 18)`. + +#### 9. `jkff.svg` — JK Flip-Flop +- **Inputs:** J at `y = 8`; clock at `y = 14`; K at `y = 20`. Three left stubs. +- **Clock triangle:** `M 6,11 L 9,14 L 6,17` (at middle-left). +- **Outputs:** Q at `y = 8`; ~Q at `y = 20`. +- **Interior glyph:** Filled dot `circle r="1.5" fill="currentColor"` on the J pin (`cx=7 cy=8`) and K pin (`cx=7 cy=20`) — visually marks the two data inputs without text. + +#### 10. `rsff.svg` — RS Flip-Flop (latch) +- **Inputs:** S at `y = 8`; R at `y = 20`. Two left stubs, no clock triangle. +- **Outputs:** Q at `y = 8`; ~Q at `y = 20`. +- **Interior glyph:** Two diagonal lines `M 10,9 L 18,14` and `M 10,19 L 18,14` (`stroke-width="1.5"`) converging rightward — suggests cross-coupled NOR/NAND latch structure. + +#### 11. `tff.svg` — T Flip-Flop +- **Inputs:** Clock at `y = 14` only. One left stub. +- **Clock triangle:** `M 6,11 L 9,14 L 6,17` (at middle-left). +- **Outputs:** Q at `y = 8`; ~Q at `y = 20`. +- **Interior glyph:** Large `"T"` centered at `(15, 18)`. + +--- + +### C. Block Components + +All use body `rect x="5" y="4" width="18" height="20" rx="1"`. Pins at `x=1`/`x=27` with 4-unit stubs. + +#### 12. `adder.svg` — Arithmetic Adder +- **Pins:** 2 inputs (`y = 9`, `y = 19`); 2 outputs (`y = 9`, `y = 19`). +- **Interior glyph:** Bold "+" — vertical `line x1="14" y1="8" x2="14" y2="20"` and horizontal `line x1="8" y1="14" x2="20" y2="14"`, both `stroke-width="2.5"`. + +#### 13. `counter.svg` — Mod-N Counter +- **Pins:** 2 inputs (`y = 9`, `y = 19`); 2 outputs (`y = 9`, `y = 19`). +- **Interior glyph:** Ascending staircase polyline `M 8,20 L 8,17 L 11,17 L 11,13 L 15,13 L 15,9 L 20,9` — implies counting/incrementing. + +#### 14. `siso.svg` — Serial-In Serial-Out +- **Pins:** 1 serial input (`y = 14` left); 1 serial output (`y = 14` right). +- **Interior glyph:** Right-arrow — horizontal stem `M 9,14 L 16,14` plus chevron arrowhead `M 13,10 L 17,14 L 13,18`. + +#### 15. `sipo.svg` — Serial-In Parallel-Out +- **Pins:** 1 serial input (`y = 14` left); 3 parallel outputs (`y = 8`, `y = 14`, `y = 20` right). +- **Interior glyph:** Same right-arrow as siso. + +#### 16. `piso.svg` — Parallel-In Serial-Out +- **Pins:** 3 parallel inputs (`y = 8`, `y = 14`, `y = 20` left); 1 serial output (`y = 14` right). +- **Interior glyph:** Same right-arrow as siso. + +#### 17. `pipo.svg` — Parallel-In Parallel-Out +- **Pins:** 3 parallel inputs (`y = 8`, `y = 14`, `y = 20` left); 3 parallel outputs (`y = 8`, `y = 14`, `y = 20` right). +- **Interior glyph:** Same right-arrow as siso. + +The 1-stub vs 3-stub pin pattern is the primary visual differentiator between shift register types. The sidebar text label is the definitive identifier. + +--- + +### D. Display, Input/Output & Infrastructure Components + +#### 18. `7seg.svg` — Seven-Segment Display +- **Enclosure:** `rect x="8" y="3" width="16" height="22" rx="1.5"`. +- **Input pins:** 4 on left at `y = 6`, `y = 11`, `y = 16`, `y = 21`, each from `x=1` to `x=8`. +- **Segments:** Seven `` elements, `stroke-width="2.5"`, `stroke-linecap="butt"`, forming digit "8": + - Top horizontal: `x=13`–`x=20` at `y=6`. + - Upper-left vertical: `x=12` from `y=7.5` to `y=12.5`. + - Upper-right vertical: `x=21` from `y=7.5` to `y=12.5`. + - Middle horizontal: `x=13`–`x=20` at `y=14`. + - Lower-left vertical: `x=12` from `y=15.5` to `y=20.5`. + - Lower-right vertical: `x=21` from `y=15.5` to `y=20.5`. + - Bottom horizontal: `x=13`–`x=20` at `y=22`. +- **Decimal point:** `circle cx="23" cy="22.5" r="1.2" fill="currentColor"`. + +#### 19. `vdd.svg` — Power Supply (VDD) +- **Vertical stem:** `line x1="14" y1="27" x2="14" y2="9"` (connection point at bottom). +- **Arrowhead chevron:** `M 7,16 L 14,9 L 21,16` pointing upward. +- **Power rail bar:** `line x1="7" y1="6" x2="21" y2="6" stroke-width="2.5"` at top. +- No text label — the upward arrow + top bar is a universally recognized power symbol. + +#### 20. `gnd.svg` — Ground (unchanged) +- Vertical line `(14, 1)` to `(14, 14)`. Three narrowing horizontal bars at `y=14` (full), `y=18` (medium), `y=22` (narrow). + +#### 21. `osc.svg` — Clock Oscillator (unchanged) +- Rounded-rect enclosure `x="3" y="5" width="22" height="18" rx="3"`. +- Square-wave path inside: `M 6,17 L 10,17 L 10,11 L 15,11 L 15,17 L 19,17 L 19,11 L 22,11`. +- Output stub `x=25` to `x=27` at `y=14`. + +#### 22. `probe.svg` — Voltage Probe (unchanged) +- Outer circle `cx="16" cy="14" r="8"`. Center filled dot `r="3"`. Lead stub `x=1` to `x=8` at `y=14`. + +#### 23. `sw.svg` — Switch (unchanged) +- Two filled contact dots. Lever line from `(6, 14)` to `(20, 6)`. Input/output stubs. + +#### 24. `led.svg` — LED Indicator (unchanged) +- Rect enclosure `x="9" y="5" width="18" height="18" rx="1.5"`. Inner circle `cx="18" cy="14" r="4.5"`. Input stub at `y=14`. + +#### 25. `text.svg` — Text Label Component (unchanged) +- Large serif `"A"` centered at `(14, 20)`. No enclosure — the letter is the icon. diff --git a/data/images/components/sw.png b/data/images/components/sw.png deleted file mode 100644 index c7c3e16..0000000 Binary files a/data/images/components/sw.png and /dev/null differ diff --git a/data/images/components/sw.svg b/data/images/components/sw.svg new file mode 100644 index 0000000..624ee44 --- /dev/null +++ b/data/images/components/sw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/data/images/components/text.png b/data/images/components/text.png deleted file mode 100644 index fd4bd08..0000000 Binary files a/data/images/components/text.png and /dev/null differ diff --git a/data/images/components/text.svg b/data/images/components/text.svg index 9668015..c64248c 100644 --- a/data/images/components/text.svg +++ b/data/images/components/text.svg @@ -1,755 +1,4 @@ - - - -CRSSIPO + + + + diff --git a/data/images/components/tff.svg b/data/images/components/tff.svg index 4a03980..e1db7e4 100644 --- a/data/images/components/tff.svg +++ b/data/images/components/tff.svg @@ -1,14 +1,8 @@ - - - - - - - - - - - - - + + + + + + + T diff --git a/data/images/components/tribuff.png b/data/images/components/tribuff.png deleted file mode 100644 index a9e06f0..0000000 Binary files a/data/images/components/tribuff.png and /dev/null differ diff --git a/data/images/components/tribuff.svg b/data/images/components/tribuff.svg new file mode 100644 index 0000000..f91fc32 --- /dev/null +++ b/data/images/components/tribuff.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/images/components/vdd.png b/data/images/components/vdd.png deleted file mode 100644 index 1637ca4..0000000 Binary files a/data/images/components/vdd.png and /dev/null differ diff --git a/data/images/components/vdd.svg b/data/images/components/vdd.svg new file mode 100644 index 0000000..a9e719f --- /dev/null +++ b/data/images/components/vdd.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/images/components/xor-iec.svg b/data/images/components/xor-iec.svg new file mode 100644 index 0000000..dfb2189 --- /dev/null +++ b/data/images/components/xor-iec.svg @@ -0,0 +1,7 @@ + + + + + + =1 + diff --git a/data/images/components/xor.png b/data/images/components/xor.png deleted file mode 100644 index fbfa6ea..0000000 Binary files a/data/images/components/xor.png and /dev/null differ diff --git a/data/images/components/xor.svg b/data/images/components/xor.svg new file mode 100644 index 0000000..ca600d7 --- /dev/null +++ b/data/images/components/xor.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/data/images/ggate.png b/data/images/ggate.png deleted file mode 100644 index df7a0d3..0000000 Binary files a/data/images/ggate.png and /dev/null differ diff --git a/data/images/index.theme b/data/images/index.theme new file mode 100644 index 0000000..479776c --- /dev/null +++ b/data/images/index.theme @@ -0,0 +1,19 @@ +[Icon Theme] +Name=Hicolor +Comment=Fallback icon theme +Hidden=true +Directories=scalable/actions,scalable/devices + +[scalable/actions] +Size=16 +MinSize=8 +MaxSize=512 +Context=Actions +Type=Scalable + +[scalable/devices] +Size=16 +MinSize=8 +MaxSize=512 +Context=Devices +Type=Scalable diff --git a/data/images/meson.build b/data/images/meson.build new file mode 100644 index 0000000..35c9651 --- /dev/null +++ b/data/images/meson.build @@ -0,0 +1,44 @@ +icon_files = [ + # Components + 'components/7seg.svg', + 'components/adder.svg', + 'components/counter.svg', + 'components/dff.svg', + 'components/jkff.svg', + 'components/pipo.svg', + 'components/piso.svg', + 'components/rsff.svg', + 'components/sipo.svg', + 'components/siso.svg', + 'components/tff.svg', + + # Actions + 'actions/flip-horizontal.svg', + 'actions/flip-vertical.svg', + 'actions/rotate-left.svg', + 'actions/rotate-right.svg', +] + +resources_xml = configure_file( + output: 'resources.xml', + command: [ + py_installation, + meson.project_source_root() / 'build-aux/generate-resources-xml.py', + '--prefix', gresource_prefix, + '--output', '@OUTPUT@', + '--source-dir', meson.current_source_dir() + ] +) + +ggate_resources = gnome.compile_resources( + 'ggate-resources', + resources_xml, + source_dir: [meson.current_source_dir(), meson.current_source_dir() / '..'], + gresource_bundle: true, + install: true, + install_dir: pkgdatadir, +) + +install_subdir('actions', install_dir: pkgdatadir / 'icons') +install_subdir('components', install_dir: pkgdatadir / 'icons') +install_subdir('../themes', install_dir: pkgdatadir) \ No newline at end of file diff --git a/data/org.astralco.ggate.desktop b/data/org.astralco.ggate.desktop.in old mode 100755 new mode 100644 similarity index 65% rename from data/org.astralco.ggate.desktop rename to data/org.astralco.ggate.desktop.in index d421acf..1d51375 --- a/data/org.astralco.ggate.desktop +++ b/data/org.astralco.ggate.desktop.in @@ -4,7 +4,7 @@ Exec=ggate %F Name=GGate Terminal=false Type=Application -Icon=ggate -Categories=Science +Icon=@APP_ID@ +Categories=Science;Education;Utility; StartupNotify=true -MimeType=text/glc +MimeType=text/glc; diff --git a/data/org.astralco.ggate.metainfo.xml.in b/data/org.astralco.ggate.metainfo.xml.in new file mode 100644 index 0000000..8108010 --- /dev/null +++ b/data/org.astralco.ggate.metainfo.xml.in @@ -0,0 +1,19 @@ + + + @APP_ID@ + CC0-1.0 + GPL-3.0-or-later + GGate + Logic circuit simulator developed with GTK 4 and Python + +

+ GGate is a user-friendly logic circuit simulator that lets you design, + wire, and simulate digital logic circuits in real time. +

+ + @APP_ID@.desktop + Koichi Akabe + https://github.com/vbkaisetsu/ggate + https://github.com/vbkaisetsu/ggate/issues + + diff --git a/data/themes/Classic.json b/data/themes/Classic.json new file mode 100644 index 0000000..36b5480 --- /dev/null +++ b/data/themes/Classic.json @@ -0,0 +1,29 @@ +{ + "name": "Classic", + "accent_hex": "#ff8000", + "colors": { + "bg_color": "#000000", + "bg_color_running": "#ffffff", + "grid_color": "#261f26", + "cursor_color": "#ffffff", + "component_color": "#00ff00", + "component_high_color": "#80ff80", + "component_color_running": "#000000", + "picked_color": "#ff8000", + "preadd_color": "#ffbf00", + "selected_color": "#ffffff", + "net_color": "#0000ff", + "net_high_color": "#8080ff", + "net_color_running": "#000000", + "highlevel_color": "#ff0000", + "lowlevel_color": "#0000ff", + "terminal_color": "#ff0000", + "terminal_color_running": "#000000", + "selection_box": "#ffbf0040", + "selection_box_border": "#ffbf00", + "_red": "#ff0000", + "_green": "#00ff00", + "_blue": "#0000ff", + "_yellow": "#ffff00" + } +} diff --git a/data/themes/FrappeDark.json b/data/themes/FrappeDark.json new file mode 100644 index 0000000..b00da68 --- /dev/null +++ b/data/themes/FrappeDark.json @@ -0,0 +1,29 @@ +{ + "name": "Frappé (Dark)", + "accent_hex": "#ca9ee6", + "colors": { + "bg_color": "#303446", + "bg_color_running": "#292c3c", + "grid_color": "#414559", + "cursor_color": "#babbf1", + "component_color": "#8caaee", + "component_high_color": "#85c1dc", + "component_color_running": "#c6d0f5", + "picked_color": "#ef9f76", + "preadd_color": "#eebebe", + "selected_color": "#ca9ee6", + "net_color": "#838ba7", + "net_high_color": "#99d1db", + "net_color_running": "#737994", + "highlevel_color": "#a6d189", + "lowlevel_color": "#8caaee", + "terminal_color": "#e78284", + "terminal_color_running": "#232634", + "selection_box": "#ca9ee633", + "selection_box_border": "#ca9ee6", + "_red": "#e78284", + "_green": "#a6d189", + "_blue": "#8caaee", + "_yellow": "#e5c890" + } +} diff --git a/data/themes/FrappeLight.json b/data/themes/FrappeLight.json new file mode 100644 index 0000000..5dcc4dc --- /dev/null +++ b/data/themes/FrappeLight.json @@ -0,0 +1,29 @@ +{ + "name": "Frappé (Light)", + "accent_hex": "#8839ef", + "colors": { + "bg_color": "#eff1f5", + "bg_color_running": "#e6e9ef", + "grid_color": "#ccd0da", + "cursor_color": "#7287fd", + "component_color": "#1e66f5", + "component_high_color": "#209fb5", + "component_color_running": "#4c4f69", + "picked_color": "#fe640b", + "preadd_color": "#dc8a78", + "selected_color": "#8839ef", + "net_color": "#6c6f93", + "net_high_color": "#04a5e5", + "net_color_running": "#5c5f77", + "highlevel_color": "#40a02b", + "lowlevel_color": "#1e66f5", + "terminal_color": "#d20f39", + "terminal_color_running": "#dce0e8", + "selection_box": "#8839ef33", + "selection_box_border": "#8839ef", + "_red": "#d20f39", + "_green": "#40a02b", + "_blue": "#1e66f5", + "_yellow": "#df8e1d" + } +} diff --git a/data/themes/MochaDark.json b/data/themes/MochaDark.json new file mode 100644 index 0000000..d3b0432 --- /dev/null +++ b/data/themes/MochaDark.json @@ -0,0 +1,29 @@ +{ + "name": "Mocha (Dark)", + "accent_hex": "#cba6f7", + "colors": { + "bg_color": "#1e1e2e", + "bg_color_running": "#181825", + "grid_color": "#313244", + "cursor_color": "#b4befe", + "component_color": "#89b4fa", + "component_high_color": "#74c7ec", + "component_color_running": "#cdd6f4", + "picked_color": "#fab387", + "preadd_color": "#f2cdcd", + "selected_color": "#cba6f7", + "net_color": "#7f849c", + "net_high_color": "#89dceb", + "net_color_running": "#6c7086", + "highlevel_color": "#a6e3a1", + "lowlevel_color": "#89b4fa", + "terminal_color": "#f38ba8", + "terminal_color_running": "#11111b", + "selection_box": "#cba6f733", + "selection_box_border": "#cba6f7", + "_red": "#f38ba8", + "_green": "#a6e3a1", + "_blue": "#89b4fa", + "_yellow": "#f9e2af" + } +} diff --git a/data/themes/MochaLight.json b/data/themes/MochaLight.json new file mode 100644 index 0000000..0805432 --- /dev/null +++ b/data/themes/MochaLight.json @@ -0,0 +1,29 @@ +{ + "name": "Mocha (Light)", + "accent_hex": "#8839ef", + "colors": { + "bg_color": "#eff1f5", + "bg_color_running": "#e6e9ef", + "grid_color": "#ccd0da", + "cursor_color": "#7287fd", + "component_color": "#1e66f5", + "component_high_color": "#209fb5", + "component_color_running": "#4c4f69", + "picked_color": "#fe640b", + "preadd_color": "#dc8a78", + "selected_color": "#8839ef", + "net_color": "#6c6f93", + "net_high_color": "#04a5e5", + "net_color_running": "#5c5f77", + "highlevel_color": "#40a02b", + "lowlevel_color": "#1e66f5", + "terminal_color": "#d20f39", + "terminal_color_running": "#dce0e8", + "selection_box": "#8839ef33", + "selection_box_border": "#8839ef", + "_red": "#d20f39", + "_green": "#40a02b", + "_blue": "#1e66f5", + "_yellow": "#df8e1d" + } +} diff --git a/data/themes/SpaceDark.json b/data/themes/SpaceDark.json new file mode 100644 index 0000000..bcbf5c1 --- /dev/null +++ b/data/themes/SpaceDark.json @@ -0,0 +1,29 @@ +{ + "name": "Space (Dark)", + "accent_hex": "#00f2fe", + "colors": { + "bg_color": "#07080d", + "bg_color_running": "#0c0e16", + "grid_color": "#1b1e2c", + "cursor_color": "#e5e8f4", + "component_color": "#3d64ff", + "component_high_color": "#00f2fe", + "component_color_running": "#8992a8", + "picked_color": "#ff8000", + "preadd_color": "#ff007f", + "selected_color": "#39ff14", + "net_color": "#5b637a", + "net_high_color": "#00f2fe", + "net_color_running": "#44495b", + "highlevel_color": "#39ff14", + "lowlevel_color": "#3d64ff", + "terminal_color": "#ff3b30", + "terminal_color_running": "#07080d", + "selection_box": "#00f2fe26", + "selection_box_border": "#00f2fe", + "_red": "#ff3b30", + "_green": "#39ff14", + "_blue": "#007aff", + "_yellow": "#ffcc00" + } +} diff --git a/data/themes/SpaceLight.json b/data/themes/SpaceLight.json new file mode 100644 index 0000000..6467042 --- /dev/null +++ b/data/themes/SpaceLight.json @@ -0,0 +1,29 @@ +{ + "name": "Space (Light)", + "accent_hex": "#0d9488", + "colors": { + "bg_color": "#f4f5f8", + "bg_color_running": "#eaecee", + "grid_color": "#d1d5db", + "cursor_color": "#1b1e2c", + "component_color": "#2563eb", + "component_high_color": "#06b6d4", + "component_color_running": "#4b5563", + "picked_color": "#ea580c", + "preadd_color": "#db2777", + "selected_color": "#16a34a", + "net_color": "#6b7280", + "net_high_color": "#0d9488", + "net_color_running": "#374151", + "highlevel_color": "#16a34a", + "lowlevel_color": "#2563eb", + "terminal_color": "#dc2626", + "terminal_color_running": "#e5e7eb", + "selection_box": "#06b6d426", + "selection_box_border": "#06b6d4", + "_red": "#dc2626", + "_green": "#16a34a", + "_blue": "#2563eb", + "_yellow": "#ca8a04" + } +} diff --git a/examples/adder.glc b/examples/adder.glc deleted file mode 100644 index a4e2187..0000000 --- a/examples/adder.glc +++ /dev/null @@ -1,506 +0,0 @@ -version - author=3.0.0 - required=2.6 - -sw - position=30,550 - input_level= - output_level=1 - matrix=1,0,0,1 - properties=initstate:0 - -sw - position=30,500 - input_level= - output_level=0 - matrix=1,0,0,1 - properties=initstate:1 - -sw - position=30,450 - input_level= - output_level=0 - matrix=1,0,0,1 - properties=initstate:1 - -sw - position=30,400 - input_level= - output_level=0 - matrix=1,0,0,1 - properties=initstate:0 - -7seg - position=50,100 - input_level=0,0,0,1 - output_level= - matrix=1,0,0,1 - properties=_red:2 - -7seg - position=180,100 - input_level=0,0,0,1 - output_level= - matrix=1,0,0,1 - properties=_red:1 - -7seg - position=610,100 - input_level=0,0,1,0 - output_level= - matrix=1,0,0,1 - properties=_red:0 - -sw - position=160,400 - input_level= - output_level=0 - matrix=1,0,0,1 - properties=initstate:0 - -sw - position=160,450 - input_level= - output_level=0 - matrix=1,0,0,1 - properties=initstate:1 - -sw - position=160,500 - input_level= - output_level=0 - matrix=1,0,0,1 - properties=initstate:0 - -sw - position=160,550 - input_level= - output_level=1 - matrix=1,0,0,1 - properties=initstate:1 - -and - position=400,220 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -and - position=400,270 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -and - position=400,350 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -and - position=400,400 - input_level=0,1 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=300,260 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=300,390 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=400,170 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=490,180 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=490,310 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=490,440 - input_level=0,1 - output_level=1 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -xor - position=490,550 - input_level=1,1 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -and - position=490,500 - input_level=1,1 - output_level=1 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -or - position=490,390 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -or - position=490,260 - input_level=0,0 - output_level=0 - matrix=1,0,0,1 - properties=inputs:2,tphl:0.0,tplh:0.0 - -text - position=30,350 - input_level= - output_level= - matrix=1,0,0,1 - properties=text:IN1 - -text - position=160,350 - input_level= - output_level= - matrix=1,0,0,1 - properties=text:IN2 - -text - position=630,120 - input_level= - output_level= - matrix=1,0,0,1 - properties=text:OUT \e IN1 + IN2 - -net - position=500,410,400,410 - -net - position=400,370,410,370 - -net - position=400,410,400,370 - -net - position=500,360,500,330 - -net - position=500,230,500,200 - -net - position=500,280,400,280 - -net - position=400,280,400,240 - -net - position=410,240,400,240 - -net - position=590,480,590,450 - -net - position=410,430,410,390 - -net - position=590,450,500,450 - -net - position=500,430,410,430 - -net - position=500,450,500,430 - -net - position=590,370,590,320 - -net - position=410,300,410,260 - -net - position=590,320,500,320 - -net - position=500,300,410,300 - -net - position=500,320,500,300 - -net - position=590,240,590,190 - -net - position=590,190,500,190 - -net - position=500,190,500,170 - -net - position=60,50,40,50 - -net - position=410,210,310,210 - -net - position=40,180,40,50 - -net - position=310,210,310,230 - -net - position=40,180,110,180 - -net - position=310,230,110,230 - -net - position=110,180,110,230 - -net - position=100,430,110,430 - -net - position=110,230,110,430 - -net - position=190,50,170,50 - -net - position=190,90,190,140 - -net - position=500,520,500,490 - -net - position=190,140,260,140 - -net - position=500,490,260,490 - -net - position=260,140,260,490 - -net - position=230,530,260,530 - -net - position=260,490,260,530 - -net - position=620,30,590,30 - -net - position=590,30,590,160 - -net - position=620,50,600,50 - -net - position=600,50,600,290 - -net - position=600,290,590,290 - -net - position=620,70,610,70 - -net - position=610,70,610,420 - -net - position=610,420,590,420 - -net - position=620,90,620,530 - -net - position=620,530,590,530 - -net - position=50,70,60,70 - -net - position=300,380,310,380 - -net - position=50,70,50,160 - -net - position=410,320,300,320 - -net - position=300,320,300,380 - -net - position=50,160,120,160 - -net - position=300,320,120,320 - -net - position=120,160,120,320 - -net - position=100,480,120,480 - -net - position=120,320,120,480 - -net - position=60,140,60,90 - -net - position=500,540,490,540 - -net - position=60,140,130,140 - -net - position=100,530,130,530 - -net - position=490,540,490,470 - -net - position=500,470,490,470 - -net - position=130,140,130,350 - -net - position=130,350,130,530 - -net - position=490,470,280,470 - -net - position=130,350,280,350 - -net - position=280,470,280,350 - -net - position=180,70,190,70 - -net - position=180,160,180,70 - -net - position=410,340,310,340 - -net - position=310,340,310,360 - -net - position=250,160,180,160 - -net - position=310,340,250,340 - -net - position=250,160,250,340 - -net - position=230,480,250,480 - -net - position=250,340,250,480 - -net - position=170,50,170,180 - -net - position=170,180,240,180 - -net - position=230,430,240,430 - -net - position=240,180,240,250 - -net - position=240,250,240,430 - -net - position=410,190,300,190 - -net - position=300,250,310,250 - -net - position=300,250,240,250 - -net - position=300,190,300,250 - -net - position=190,30,160,30 - -net - position=160,30,160,200 - -net - position=160,200,230,200 - -net - position=230,200,230,380 - -net - position=230,200,280,200 - -net - position=410,140,280,140 - -net - position=280,200,280,140 - -net - position=30,30,60,30 - -net - position=30,30,30,200 - -net - position=100,200,30,200 - -net - position=100,200,100,210 - -net - position=100,210,100,380 - -net - position=410,160,290,160 - -net - position=100,210,290,210 - -net - position=290,160,290,210 - diff --git a/gallery/README.md b/gallery/README.md new file mode 100644 index 0000000..b09d7e9 --- /dev/null +++ b/gallery/README.md @@ -0,0 +1,63 @@ +# GGate gallery + +Previews of GGate. The stills are captured procedurally: + +```bash +python3 build-aux/capture_screenshots.py # the stills below +``` + +The demo clip is a screen recording cleaned up with ffmpeg (1080p, 30fps, 30s). + +## Demo + +A short tour — opening properties, changing an LED's colour, running the simulation: + +[![GGate demo (click to play)](./ggate-playback.png)](./ggate-demo.mp4) + +▶ [ggate-demo.mp4](./ggate-demo.mp4) + +## Simulation + +![Simulating an oscillator circuit with the playback bar](./ggate-playback.png) + +## Timing diagram + +![Timing diagram of the simulated signals](./ggate-timing-diagram.png) + +## Dialogs + +| Preferences | Component properties | +|---|---| +| ![Preferences dialog](./ggate-preferences.png) | ![Properties dialog](./ggate-properties.png) | + +## Themes + +Each theme ships in light and dark variants; the libadwaita chrome matches the variant. + +**Classic** + +![Classic](./ggate-theme-classic.png) + +**Space (Light)** + +![Space Light](./ggate-theme-space-light.png) + +**Space (Dark)** + +![Space Dark](./ggate-theme-space-dark.png) + +**Frappé (Light)** + +![Frappé Light](./ggate-theme-frappe-light.png) + +**Frappé (Dark)** + +![Frappé Dark](./ggate-theme-frappe-dark.png) + +**Mocha (Light)** + +![Mocha Light](./ggate-theme-mocha-light.png) + +**Mocha (Dark)** + +![Mocha Dark](./ggate-theme-mocha-dark.png) diff --git a/gallery/ggate-demo.mp4 b/gallery/ggate-demo.mp4 new file mode 100644 index 0000000..db23484 Binary files /dev/null and b/gallery/ggate-demo.mp4 differ diff --git a/gallery/ggate-playback.png b/gallery/ggate-playback.png new file mode 100644 index 0000000..db06359 Binary files /dev/null and b/gallery/ggate-playback.png differ diff --git a/gallery/ggate-preferences.png b/gallery/ggate-preferences.png new file mode 100644 index 0000000..7e38b22 Binary files /dev/null and b/gallery/ggate-preferences.png differ diff --git a/gallery/ggate-properties.png b/gallery/ggate-properties.png new file mode 100644 index 0000000..4fd34d2 Binary files /dev/null and b/gallery/ggate-properties.png differ diff --git a/gallery/ggate-theme-classic.png b/gallery/ggate-theme-classic.png new file mode 100644 index 0000000..01330a0 Binary files /dev/null and b/gallery/ggate-theme-classic.png differ diff --git a/gallery/ggate-theme-frappe-dark.png b/gallery/ggate-theme-frappe-dark.png new file mode 100644 index 0000000..839f82f Binary files /dev/null and b/gallery/ggate-theme-frappe-dark.png differ diff --git a/gallery/ggate-theme-frappe-light.png b/gallery/ggate-theme-frappe-light.png new file mode 100644 index 0000000..52687f3 Binary files /dev/null and b/gallery/ggate-theme-frappe-light.png differ diff --git a/gallery/ggate-theme-mocha-dark.png b/gallery/ggate-theme-mocha-dark.png new file mode 100644 index 0000000..d68705b Binary files /dev/null and b/gallery/ggate-theme-mocha-dark.png differ diff --git a/gallery/ggate-theme-mocha-light.png b/gallery/ggate-theme-mocha-light.png new file mode 100644 index 0000000..712c3fd Binary files /dev/null and b/gallery/ggate-theme-mocha-light.png differ diff --git a/gallery/ggate-theme-space-dark.png b/gallery/ggate-theme-space-dark.png new file mode 100644 index 0000000..22cd5fd Binary files /dev/null and b/gallery/ggate-theme-space-dark.png differ diff --git a/gallery/ggate-theme-space-light.png b/gallery/ggate-theme-space-light.png new file mode 100644 index 0000000..bd73f51 Binary files /dev/null and b/gallery/ggate-theme-space-light.png differ diff --git a/gallery/ggate-timing-diagram.png b/gallery/ggate-timing-diagram.png new file mode 100644 index 0000000..0489b1e Binary files /dev/null and b/gallery/ggate-timing-diagram.png differ diff --git a/ggate/Animation.py b/ggate/Animation.py new file mode 100644 index 0000000..b09f0ed --- /dev/null +++ b/ggate/Animation.py @@ -0,0 +1,94 @@ + +def ease_out_cubic(t: float) -> float: + return 1.0 - (1.0 - t) ** 3 + +class Vector2DAnimation: + def __init__(self, start_pos: tuple[float, float], target_pos: tuple[float, float], duration_ms: float): + self.start_pos = list(start_pos) + self.target_pos = list(target_pos) + self.current_pos = list(start_pos) + self.duration_us = duration_ms * 1000.0 + self.elapsed_us = 0.0 + self.completed = False + + def update(self, delta_us: float) -> None: + self.elapsed_us += delta_us + if self.elapsed_us >= self.duration_us: + self.current_pos = list(self.target_pos) + self.completed = True + else: + t = self.elapsed_us / self.duration_us + eased_t = ease_out_cubic(t) + self.current_pos[0] = self.start_pos[0] + (self.target_pos[0] - self.start_pos[0]) * eased_t + self.current_pos[1] = self.start_pos[1] + (self.target_pos[1] - self.start_pos[1]) * eased_t + +class CanvasAnimationController: + def __init__(self, draw_area): + self.draw_area = draw_area + self.active_animations = {} + self.tick_id = None + self.last_frame_time = 0 + + def start_offset_animation(self, item_ref, start_offset: tuple[float, float], duration_ms: float = 150.0) -> None: + actual_start = start_offset + if item_ref in self.active_animations: + actual_start = self.active_animations[item_ref].current_pos + + if actual_start[0] == 0.0 and actual_start[1] == 0.0: + if item_ref in self.active_animations: + del self.active_animations[item_ref] + return + + self.active_animations[item_ref] = Vector2DAnimation(actual_start, (0.0, 0.0), duration_ms) + + if self.tick_id is None: + self.last_frame_time = 0 + self.tick_id = self.draw_area.drawingarea.add_tick_callback(self._on_tick) + + def cancel_animation(self, item_ref) -> None: + if item_ref in self.active_animations: + del self.active_animations[item_ref] + if not self.active_animations: + self._stop_tick() + + def clear(self) -> None: + self.active_animations.clear() + self._stop_tick() + + def get_visual_offset(self, item_ref) -> tuple[float, float]: + if item_ref in self.active_animations: + pos = self.active_animations[item_ref].current_pos + return pos[0], pos[1] + return 0.0, 0.0 + + def _on_tick(self, widget, frame_clock, user_data=None) -> bool: + frame_time = frame_clock.get_frame_time() + if self.last_frame_time == 0: + self.last_frame_time = frame_time + return True + + delta_us = frame_time - self.last_frame_time + self.last_frame_time = frame_time + + completed_keys = [] + for key, anim in self.active_animations.items(): + anim.update(delta_us) + if anim.completed: + completed_keys.append(key) + + for key in completed_keys: + del self.active_animations[key] + + self.draw_area.queue_draw() + + if not self.active_animations: + self.tick_id = None + self.last_frame_time = 0 + return False + return True + + def _stop_tick(self) -> None: + if self.tick_id is not None: + self.draw_area.drawingarea.remove_tick_callback(self.tick_id) + self.tick_id = None + self.last_frame_time = 0 diff --git a/ggate/CircuitManager.py b/ggate/CircuitManager.py index d821f2f..850d1f2 100644 --- a/ggate/CircuitManager.py +++ b/ggate/CircuitManager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- from __future__ import annotations from collections import Counter from typing import TYPE_CHECKING, Tuple @@ -6,22 +5,30 @@ if TYPE_CHECKING: from ggate.MainFrame import MainFrame -import igraph -import copy import os +import re +import copy +import time +import igraph from gi.repository import GObject from shapely.geometry import LineString, Point -from shapely.ops import split +from ggate.CircuitPlaybackAnimation import CircuitPlaybackAnimation from ggate.Components.LogicGates.SystemComponents import BaseComponent from ggate.Components.LogicGates import logic_gates from ggate.const import definitions +from ggate import config from ggate import Preference -from ggate.Utils import encode_text, decode_text, fit_components, get_components_rect, get_duplicate_points, multiply_matrix, rotate_left_90, rotate_right_90 +from ggate.Utils import encode_text, decode_text, fit_components, get_components_rect, multiply_matrix, rotate_left_90, rotate_right_90, logger from gettext import gettext as _ -from ggate import config CircuitComponent = Tuple[definitions, BaseComponent] +# warn if a simulation run takes longer than this (seconds) +_SLOW_SIM_SECONDS = 15 +# max time each compute idle-tick may run before yielding to keep the UI responsive +_COMPUTE_SLICE_SECONDS = 0.008 + + class CircuitConverter(): def __init__(self, circuit: CircuitManager): self.circuit = circuit @@ -34,6 +41,11 @@ def _start_write(self): ("required", [config.compatibility["required"]]), ]) + zoom = self.circuit.mainframe.drawarea.zoom + self._add_data("meta", [ + ("zoom", [zoom]), + ]) + def _add_data(self, key: str, attributes: list[tuple[str, list[str]]]) -> None: self.data += f"{key}" @@ -45,26 +57,29 @@ def _add_data(self, key: str, attributes: list[tuple[str, list[str]]]) -> None: self.data += "\n\n" - def components_to_string(self): + def components_to_string(self, components=None): + self.data = "" self._start_write() - for c in self.circuit.components: + target_components = components if components is not None else self.circuit.components + for c in target_components: if c[0] == definitions.component_net: self._add_data("net", [ - ("position", [c[1], c[2], c[3], c[4]]), + ("position", [int(round(float(x))) for x in c[1:5]]), ]) else: + formatted_props = [] + for i, p in enumerate(c[1].values): + val_str = str(p) + if isinstance(p, float) and p.is_integer(): + val_str = str(int(p)) + formatted_props.append(f"{c[1].prop_names[i]}:{encode_text(val_str)}") + self._add_data(c[0], [ - ("position", [c[1].pos_x, c[1].pos_y]), + ("position", [int(round(float(x))) for x in (c[1].pos_x, c[1].pos_y)]), ("input_level", [str(int(p)) for p in c[1].input_level]), ("output_level", [str(int(p)) for p in c[1].output_level]), ("matrix", [str(int(p)) for p in c[1].matrix]), - ( - "properties", - [ - f"{c[1].prop_names[i]}:{encode_text(str(p))}" \ - for i, p in enumerate(c[1].values) - ] - ), + ("properties", formatted_props), ]) self.data = self.data.strip() return self.data @@ -87,7 +102,7 @@ def parse_section(self, section: str): content = self._parse_section_content(lines[1:]) if which == "net": - content["position"] = [int(x) for x in content["position"]] + content["position"] = [int(float(x)) for x in content["position"]] if len(content["position"]) != 4: content["position"] = [0, 10, 10, 10] @@ -96,8 +111,8 @@ def parse_section(self, section: str): elif which in logic_gates: component: CircuitComponent = [which, copy.deepcopy(logic_gates[which])] if (position := content.get("position", None)) and len(position) == 2: - component[1].pos_x = int(position[0]) - component[1].pos_y = int(position[1]) + component[1].pos_x = int(float(position[0])) + component[1].pos_y = int(float(position[1])) if (input_level := content.get("input_level", None)): component[1].input_level = [ @@ -121,20 +136,32 @@ def parse_section(self, section: str): name, value = property.split(":") value = decode_text(value) - if name in component[1].prop_names: - index = component[1].prop_names.index(name) + if name not in component[1].prop_names: + continue - if component[1].properties[index][1] == definitions.property_int: - component[1].values[index] = int(value) + index = component[1].prop_names.index(name) + real_props = [p for p in component[1].properties if p[1] is not None] + if index >= len(real_props): + component[1].values[index] = value + continue - elif component[1].properties[index][1] == definitions.property_float: - component[1].values[index] = float(value) - - elif component[1].properties[index][1] == definitions.property_select: - component[1].values[index] = int(value) + prop_type = real_props[index][1] + if isinstance(prop_type, tuple): + prop_type = prop_type[0] + + if prop_type == definitions.property_int: + component[1].values[index] = int(float(value)) + elif prop_type == definitions.property_float: + component[1].values[index] = float(value) + elif prop_type == definitions.property_select: + component[1].values[index] = int(float(value)) + elif prop_type == definitions.property_bool: + component[1].values[index] = bool(value) or value == "1" + else: + component[1].values[index] = value - else: - component[1].values[index] = value + component[1].propertyChanged(component[1].values) + component[1].set_rot_props() return component @@ -152,12 +179,23 @@ def parse_section(self, section: str): % { "creator": authored_version, "this": current_version, "minimum": current_required } return None + + elif which == "meta": + if (zoom_val := content.get("zoom", None)): + zoom = float(zoom_val[0]) + if self.circuit.mainframe: + self.circuit.mainframe.drawarea.zoom = zoom + self.circuit.mainframe.drawarea.drawingarea.set_size_request( + int(self.circuit.mainframe.drawarea.width * zoom), + int(self.circuit.mainframe.drawarea.height * zoom) + ) + return None return None def string_to_components(self, str_data) -> list[CircuitComponent]: components: list[CircuitComponent] = [] - for section in str_data.split("\n\n"): + for section in re.split(r'\n[ \t\r]*\n', str_data): section = section.strip() if not section or section == "": continue @@ -177,7 +215,7 @@ class CircuitManager(GObject.GObject): __gsignals__ = { 'currenttime-changed': (GObject.SIGNAL_RUN_FIRST, None, (float,)), 'title-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)), - 'message-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)), + 'message-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, bool)), 'item-unselected': (GObject.SIGNAL_RUN_FIRST, None, ()), 'alert': (GObject.SIGNAL_RUN_FIRST, None, (str,)) } @@ -194,6 +232,10 @@ def __init__(self, mainframe: MainFrame): self.filepath = "" self.mainframe = mainframe self.converter = CircuitConverter(self) + self.playback = CircuitPlaybackAnimation(self) + self.sim_generator = None + self.sim_cancelled = False + self.sim_start_time = None self.net_connections = [] self.net_levels = [] @@ -215,7 +257,7 @@ def save_file(self, filepath): self.simple_change = True self.save_point = self.action_count self.emit("title-changed", "%s - %s" % (os.path.basename(filepath), definitions.app_name)) - self.emit('message-changed', _('Saved File ')+os.path.basename(filepath)) + self.emit('message-changed', _('Saved File ')+os.path.basename(filepath), True) return True @@ -236,7 +278,7 @@ def open_file(self, filepath): self.components_history = [copy.deepcopy(self.components)] self.filepath = filepath self.emit("title-changed", "%s - %s" % (os.path.basename(filepath), definitions.app_name)) - self.emit('message-changed', _('Opened file ' + os.path.basename(filepath))) + self.emit('message-changed', _('Opened file ' + os.path.basename(filepath)), True) return False @@ -338,6 +380,7 @@ def analyze_net_connections(self): self.net_no_dot = net_no_dots self.net_connections = connections + self.net_levels = [-1] * len(connections) def split_nets(self, x, y): @@ -499,7 +542,7 @@ def set_netlevels(self): for j,net in enumerate(self.net_connections): if (c[1].pos_x + p[0], c[1].pos_y + p[1]) in net: if self.net_levels[j] != -1 and self.net_levels[j] != c[1].output_level[i]: - self.emit("message-changed", _("Output port is short circuit!")) + self.emit("message-changed", _("Output port is short circuit!"), False) return True self.net_levels[j] = c[1].output_level[i] return False @@ -511,6 +554,18 @@ def initialize_logic(self): for c in self.components: if c[0] != definitions.component_net: c[1].initialize() + self.net_levels = [-1]*len(self.net_connections) + + def _apply_recorded_frame(self, t): + i = 2 + for c in self.components: + if c[0] != definitions.component_net: + c[1].input_level = self.component_state_history[t][i][0] + c[1].output_level = self.component_state_history[t][i][1] + c[1].output_stack = self.component_state_history[t][i][2] + c[1].store = self.component_state_history[t][i][3] + i += 1 + self.net_levels = self.component_state_history[t][1] def revert_state(self): if self.component_state_history: @@ -519,109 +574,231 @@ def revert_state(self): if comp_state[0] <= self.current_time: t = i break - i = 2 - for c in self.components: - if c[0] != definitions.component_net: - c[1].input_level = self.component_state_history[t][i][0] - c[1].output_level = self.component_state_history[t][i][1] - c[1].output_stack = self.component_state_history[t][i][2] - c[1].store = self.component_state_history[t][i][3] - i += 1 - self.net_levels = self.component_state_history[t][1] + self._apply_recorded_frame(t) self.emit("currenttime-changed", self.current_time) - def analyze_logic(self): + def cancel_simulation(self): + self.playback.cancel() + + def _rewind_history(self): + # re-running from current_time: drop any recorded state newer than it + if not self.component_state_history: + return + t = 0 + for i, comp_state in reversed(list(enumerate(self.component_state_history))): + if comp_state[0] <= self.current_time: + t = i + break + self.current_time = self.component_state_history[t][0] + self.component_state_history = self.component_state_history[:t] + self.probe_levels_history = self.probe_levels_history[:t] + + def _net_index_at(self, x, y): + for j, net in enumerate(self.net_connections): + if (x, y) in net: + return j + return None + + def _gather_inputs(self, comp): + # input level per connected input pin; None if any pin sits on an open net + inputs = [] + for px, py in comp.rot_input_pins: + j = self._net_index_at(comp.pos_x + px, comp.pos_y + py) + if j is None: + continue + if self.net_levels[j] == -1: + return None + inputs.append(self.net_levels[j]) + return inputs + + def _apply_output_stack(self, comp): + for i in range(len(comp.rot_output_pins)): + stack = comp.output_stack[i] + if not stack: + continue + if stack[0][0] == self.current_time: + comp.output_level[i] = stack.pop(0)[1] + elif comp.output_level[i] == stack[0][1]: + stack.pop(0) + + def _evaluate_components(self): + # evaluate every non-net component at current_time; True if an input is open + for c in self.components: + if c[0] == definitions.component_net: + continue + inputs = self._gather_inputs(c[1]) + if inputs is None: + self.emit("message-changed", _("Input port is open circuit!"), False) + return True + c[1].calculate(inputs, self.current_time) + c[1].input_level = inputs[:] + self._apply_output_stack(c[1]) + return False + + def _settle_state(self, net_levels_history): + # "settled" (stable), "oscillate" (repeating cycle), or "continue" + if self.net_levels in net_levels_history[:-1]: + if self.net_levels == net_levels_history[-1]: + return "settled" + return "oscillate" + return "continue" + + def _record_state(self): + probe_levels = [self.current_time] + for c in self.components: + if c[0] == definitions.component_probe: + probe_levels.append(c[1].input_level[0]) + self.probe_levels_history.append(probe_levels) + + comp_state = [self.current_time, self.net_levels[:]] + for c in self.components: + if c[0] != definitions.component_net: + comp_state.append((c[1].input_level[:], c[1].output_level[:], copy.deepcopy(c[1].output_stack), copy.deepcopy(c[1].store))) + self.component_state_history.append(comp_state) + + def _advance_to_next_event(self): + # jump current_time to the soonest pending output event; False if none + timelist = [] + for c in self.components: + if c[0] == definitions.component_net: + continue + for s in c[1].output_stack: + if s and s[0][0] != self.current_time: + timelist.append(s[0][0]) + if not timelist or min(timelist) == self.current_time: + return False + self.current_time = min(timelist) + for c in self.components: + if c[0] == definitions.component_net: + continue + for i, s in enumerate(c[1].output_stack): + if s and s[0][0] == self.current_time: + c[1].output_level[i] = s.pop(0)[1] + return True + + def _settle(self, counter, max_iters): + # run the per-timestep settle loop; yields ("progress", t) each iteration and + # returns the updated counter when settled, or None on error (message emitted) + net_levels_history = [] + while True: + if self.set_netlevels(): + return None + if self._evaluate_components(): + return None + + state = self._settle_state(net_levels_history) + if state == "settled": + self.emit("message-changed", "", False) + return counter + if state == "oscillate": + self.emit("message-changed", _("This circuit oscillates on infinite frequency!"), False) + return None + + net_levels_history.append(self.net_levels) + if counter >= max_iters: + self.emit("message-changed", _("Calculation exceeded %d iterations — raise the limit in Preferences.") % max_iters, False) + return None + counter += 1 + yield ("progress", self.current_time) + + def analyze_logic_generator(self): + self._rewind_history() counter = 0 stop_time = Preference.max_calc_duration max_iters = Preference.max_calc_iters - if self.component_state_history: - t = 0 - for i, comp_state in reversed(list(enumerate(self.component_state_history))): - if comp_state[0] <= self.current_time: - t = i - break - self.current_time = self.component_state_history[t][0] - self.component_state_history = self.component_state_history[:t] - self.probe_levels_history = self.probe_levels_history[:t] - while self.current_time < stop_time: - net_levels_history = [] - - while True: - if self.set_netlevels(): - return True - - for c in self.components: - if c[0] != definitions.component_net: - input_datas = [] - for p in c[1].rot_input_pins: - for j, net in enumerate(self.net_connections): - if (c[1].pos_x + p[0], c[1].pos_y + p[1]) in net: - if self.net_levels[j] == -1: - self.emit("message-changed", _("Input port is open circuit!")) - return True - input_datas.append(self.net_levels[j]) - - c[1].calculate(input_datas, self.current_time) - c[1].input_level = input_datas[:] - - for i, p in enumerate(c[1].rot_output_pins): - if c[1].output_stack[i]: - if c[1].output_stack[i][0][0] == self.current_time: - c[1].output_level[i] = c[1].output_stack[i].pop(0)[1] - elif c[1].output_level[i] == c[1].output_stack[i][0][1]: - c[1].output_stack[i].pop(0) - - if self.net_levels in net_levels_history[:-1]: - if self.net_levels == net_levels_history[len(net_levels_history) - 1]: - self.emit("message-changed", "") - break - else: - self.emit("message-changed", _("This circuit oscillates on infinite frequency!")) - return True - - net_levels_history.append(self.net_levels) - - if counter >= max_iters: - self.emit("message-changed", _("This logic is complexity! (iters > %d)") % max_iters) - return True - - counter += 1 - - probe_levels = [self.current_time] - for c in self.components: - if c[0] == definitions.component_probe: - probe_levels.append(c[1].input_level[0]) - self.probe_levels_history.append(probe_levels) - - comp_state = [self.current_time, self.net_levels[:]] - for c in self.components: - if c[0] != definitions.component_net: - comp_state.append((c[1].input_level[:], c[1].output_level[:], copy.deepcopy(c[1].output_stack), copy.deepcopy(c[1].store))) - self.component_state_history.append(comp_state) - - tmptime = self.current_time - timelist = [] - for c in self.components: - if c[0] != definitions.component_net: - for s in c[1].output_stack: - if s and s[0][0] != self.current_time: - timelist.append(s[0][0]) - if timelist: - self.current_time = min(timelist) - if tmptime == self.current_time: + counter = yield from self._settle(counter, max_iters) + if counter is None: + yield ("error", True) + return + self._record_state() + if not self._advance_to_next_event(): break + yield ("progress", self.current_time) - for c in self.components: - if c[0] != definitions.component_net: - for i, s in enumerate(c[1].output_stack): - if s and s[0][0] == self.current_time: - c[1].output_level[i] = s.pop(0)[1] + self.emit("currenttime-changed", self.current_time) + yield ("success", False) + def _run_sim_sync(self): + result = None + try: + while True: + step_type, val = next(self.sim_generator) + if step_type in ("success", "error"): + result = val + break + except StopIteration: + pass + self.sim_generator = None + self._log_if_slow() + return result + + def _log_if_slow(self): + if self.sim_start_time is None: + return + elapsed = time.time() - self.sim_start_time + self.sim_start_time = None + if elapsed > _SLOW_SIM_SECONDS: + logger.warning("simulation took %.1fs (%d frames)", elapsed, len(self.probe_levels_history)) + + def analyze_logic(self, callback=None): + self.cancel_simulation() + if callback is None: + self.sim_cancelled = False + self.sim_generator = self.analyze_logic_generator() + self.sim_start_time = time.time() + return self._run_sim_sync() + self.playback.run(callback) + + def begin_compute(self): + self.sim_cancelled = False + self.sim_generator = self.analyze_logic_generator() + self.sim_start_time = time.time() + + def compute_slice(self): + start = time.time() + while time.time() - start < _COMPUTE_SLICE_SECONDS: + try: + step_type, val = next(self.sim_generator) + if step_type in ("success", "error"): + self._end_compute() + return ("done", val) + except StopIteration: + self._end_compute() + return ("done", False) + return ("running", None) + + def _end_compute(self): + self._log_if_slow() + self.sim_generator = None + + def cancel_compute(self): + self.sim_cancelled = True + self.sim_generator = None + + def apply_frame_at(self, index): + self.current_time = self.component_state_history[index][0] + self._apply_recorded_frame(index) self.emit("currenttime-changed", self.current_time) - return False + + def frame_count(self): + return len(self.component_state_history) + + def total_time(self): + if not self.component_state_history: + return 0.0 + return self.component_state_history[-1][0] + + def is_animated(self): + return len(self.component_state_history) > 1 and self.total_time() > 0 + + def finalize_playback(self, error, callback): + if not error: + self.emit("message-changed", "", False) + if callback: + callback(error) def remove_selected_component(self): for c in self.selected_components: diff --git a/ggate/CircuitPlaybackAnimation.py b/ggate/CircuitPlaybackAnimation.py new file mode 100644 index 0000000..065beb4 --- /dev/null +++ b/ggate/CircuitPlaybackAnimation.py @@ -0,0 +1,138 @@ +from gi.repository import GObject, GLib +from ggate import Preference + +# floor on playback frame interval (~60fps cap) +_PLAYBACK_MIN_INTERVAL_MS = 16 +# ceiling on playback frame interval (stops few-frame circuits crawling) +_PLAYBACK_MAX_INTERVAL_MS = 200 + +class CircuitPlaybackAnimation(GObject.GObject): + __gsignals__ = { + "playback-state-changed": (GObject.SIGNAL_RUN_FIRST, None, (str,)) + } + + def __init__(self, circuit): + GObject.GObject.__init__(self) + self.circuit = circuit + self.source_id = None + self.playback_index = 0 + self.is_playing = False + self.state = "idle" + self.callback = None + self.compute_error = False + + def _update_state(self, state): + self.state = state + self.emit("playback-state-changed", state) + + def run(self, callback): + self.cancel() + self.callback = callback + self.circuit.begin_compute() + self._update_state("computing") + self.source_id = GLib.idle_add(self._compute_step) + + def _compute_step(self): + if self.circuit.sim_cancelled: + return GLib.SOURCE_REMOVE + status, err = self.circuit.compute_slice() + if status == "done": + self.compute_error = err + if self.circuit.is_animated(): + self._start_playback() + else: + self._finish_static() + return GLib.SOURCE_REMOVE + return GLib.SOURCE_CONTINUE + + def _start_playback(self): + self.playback_index = 0 + self.is_playing = True + self._update_state("playing") + self.source_id = GLib.timeout_add(self._interval(), self._play_frame) + + def _play_frame(self): + if not self.is_playing: + return GLib.SOURCE_REMOVE + if self.playback_index >= self.circuit.frame_count(): + self._finish() + return GLib.SOURCE_REMOVE + self.circuit.apply_frame_at(self.playback_index) + self.playback_index += 1 + return GLib.SOURCE_CONTINUE + + def _finish(self): + self.is_playing = False + self.source_id = None + self._update_state("ended") + self.circuit.finalize_playback(self.compute_error, self.callback) + + def _finish_static(self): + frame_count = self.circuit.frame_count() + if frame_count: + self.circuit.apply_frame_at(frame_count - 1) + self.is_playing = False + self.source_id = None + self._update_state("idle") + self.circuit.finalize_playback(self.compute_error, self.callback) + + def pause(self): + if not self.is_playing: + return + if self.source_id is not None: + GLib.source_remove(self.source_id) + self.source_id = None + self.is_playing = False + self._update_state("paused") + + def play(self): + if self.state not in ("paused", "ended"): + return + if self.playback_index >= self.circuit.frame_count(): + self.playback_index = 0 + self.is_playing = True + self._update_state("playing") + self.source_id = GLib.timeout_add(self._interval(), self._play_frame) + + def toggle_play(self): + if self.state == "playing": + self.pause() + elif self.state in ("paused", "ended"): + self.play() + + def restart(self): + if not self.circuit.frame_count(): + return + self.playback_index = 0 + self.circuit.apply_frame_at(0) + if self.state != "playing": + self.is_playing = True + self._update_state("playing") + self.source_id = GLib.timeout_add(self._interval(), self._play_frame) + + def seek(self, index): + frame_count = self.circuit.frame_count() + if not frame_count: + return + index = max(0, min(index, frame_count - 1)) + self.playback_index = index + self.circuit.apply_frame_at(index) + + def stop(self): + self.cancel() + + def cancel(self): + if self.source_id is not None: + GLib.source_remove(self.source_id) + self.source_id = None + self.circuit.cancel_compute() + self.is_playing = False + if self.state != "idle": + self._update_state("idle") + + def frame_count(self): + return self.circuit.frame_count() + + def _interval(self): + frame_count = self.circuit.frame_count() + return max(_PLAYBACK_MIN_INTERVAL_MS, min(int(Preference.playback_duration * 1000 / max(1, frame_count)), _PLAYBACK_MAX_INTERVAL_MS)) diff --git a/ggate/ComponentView.py b/ggate/ComponentView.py index b86c188..36827f7 100644 --- a/ggate/ComponentView.py +++ b/ggate/ComponentView.py @@ -1,31 +1,49 @@ -import os -from ggate import config +from ggate import config, Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent from ggate.const import definitions from gettext import gettext as _ from gi.repository import Gtk, GdkPixbuf, GObject -def _get_icon_path(icon: str): - svg_path = os.path.join(config.DATADIR, "images", "components", f"{icon}.svg") - png_path = os.path.join(config.DATADIR, "images", "components", f"{icon}.png") - return svg_path if os.path.exists(svg_path) else png_path - class ComponentViewListBoxRow(Gtk.ListBoxRow): type = BaseComponent + icon = None -class ComponentView(Gtk.ScrolledWindow): +class ComponentView(Gtk.Box): __gsignals__ = { "component-checked": (GObject.SIGNAL_RUN_FIRST, None, (str,)), "window-hidden": (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self): - super().__init__() - self.set_margin_bottom(4) + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_margin_start(6) + self.set_margin_end(6) + self.set_margin_top(6) + self.set_margin_bottom(6) self.set_size_request(200, -1) self.components: list[ComponentViewListBoxRow] = [] - self.listBox = Gtk.ListBox() + self.listboxes: list[Gtk.ListBox] = [] + self.category_groups = [] + + self.search_entry = Gtk.SearchEntry() + self.search_entry.set_placeholder_text(_("Search gates")) + self.search_entry.set_margin_start(4) + self.search_entry.set_margin_end(4) + self.search_entry.set_margin_top(4) + self.search_entry.set_margin_bottom(4) + self.search_entry.connect("search-changed", self.on_search_changed) + self.append(self.search_entry) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_vexpand(True) + scrolled.set_hexpand(True) + self.append(scrolled) + + self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + self.main_box.set_margin_start(4) + self.main_box.set_margin_end(4) + scrolled.set_child(self.main_box) # Create toggle buttons self.button_names = { @@ -64,69 +82,85 @@ def __init__(self): ], } - for button_types in self.button_names.keys(): - section = Gtk.ListBoxRow() - section_title = Gtk.Box() - - section_label = Gtk.Label() - section_label.set_markup("" + _(button_types).capitalize() + "") - section_label.set_margin_top(5) - section_label.set_margin_bottom(5) - section_label.add_css_class("title") - - section_title.append(section_label) - section_title.set_margin_start(5) - - section.set_size_request(-1, 35) - section.set_child(section_title) - section.set_activatable(False) - section.set_selectable(False) - - self.listBox.append(section) - self.listBox.set_show_separators(True) - - # The logic gates in that section - + group_label = Gtk.Label() + group_label.set_markup("" + _(button_types).capitalize() + "") + group_label.add_css_class("dimmed") + group_label.set_halign(Gtk.Align.START) + group_label.set_margin_start(4) + group_label.set_margin_top(4) + group_label.set_margin_bottom(2) + self.main_box.append(group_label) + + # ListBox for this category + lb = Gtk.ListBox() + lb.add_css_class("navigation-sidebar") + lb.connect("row-activated", self.on_row_activated) + self.main_box.append(lb) + self.listboxes.append(lb) + + group_rows = [] for gate in self.button_names[button_types]: - gate_row: ComponentViewListBoxRow = Gtk.ListBoxRow() - content_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + gate_row: ComponentViewListBoxRow = ComponentViewListBoxRow() + content_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) + content_box.set_valign(Gtk.Align.CENTER) - icon = Gtk.Image.new_from_file(_get_icon_path(gate)) + icon = Gtk.Image() + icon.set_margin_end(4) + icon.set_margin_start(4) + icon.set_pixel_size(22) - # Label designs - label = Gtk.Label(label=str(gate).capitalize()) - label.set_margin_start(5) + label = Gtk.Label(label=str(gate).upper()) label.add_css_class("subtitle") - # Icon organization - icon.set_margin_end(5) - icon.set_margin_start(10) - icon.set_size_request(24, 24) - content_box.append(icon) content_box.append(label) # Setup row gate_row.type = gate + gate_row.icon = icon gate_row.set_child(content_box) gate_row.set_activatable(True) - self.listBox.append(gate_row) + lb.append(gate_row) self.components.append(gate_row) + group_rows.append(gate_row) - self.listBox.connect("row-activated", self.on_row_activated) - self.set_child(self.listBox) + self.category_groups.append((group_label, lb, group_rows)) + + self.refresh_icons() + + def refresh_icons(self): + suffix = "-iec" if Preference.symbol_type == 1 else "" + for row in self.components: + row.icon.set_from_icon_name(f"gate-{row.type}{suffix}-symbolic") def set_all_sensitive(self, state, *args): - [row.set_selectable(bool(state)) for row in self.components] + [row.set_sensitive(bool(state)) for row in self.components] - def on_row_activated(self, _, row, *args): - gates = [] - gate = row.type + def on_row_activated(self, listbox, row, *args): + for lb in self.listboxes: + if lb != listbox: + lb.unselect_all() + gate = row.type + gates = [] for item in self.button_names.values(): gates += item if gate in gates: self.emit("component-checked", gate) + + def on_search_changed(self, entry): + query = entry.get_text().strip().lower() + + for group_label, lb, gates in self.category_groups: + visible_count = 0 + for row in gates: + match = not query or (query in str(row.type).lower()) + row.set_visible(match) + if match: + visible_count += 1 + + lb.set_visible(visible_count > 0) + group_label.set_visible(visible_count > 0) diff --git a/ggate/Components/LogicGates/Calculator.py b/ggate/Components/LogicGates/Calculator.py index 41bf978..a1f6fd1 100644 --- a/ggate/Components/LogicGates/Calculator.py +++ b/ggate/Components/LogicGates/Calculator.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- from gettext import gettext as _ -from ggate.const import definitions as const from ggate import Preference +from ggate.const import definitions as const from ggate.Utils import cairo_draw_text, cairo_paths, stack_with_tphl_lh from .SystemComponents import BaseComponent diff --git a/ggate/Components/LogicGates/Counters/ModNCounter.py b/ggate/Components/LogicGates/Counters/ModNCounter.py index f345e66..91f9d67 100644 --- a/ggate/Components/LogicGates/Counters/ModNCounter.py +++ b/ggate/Components/LogicGates/Counters/ModNCounter.py @@ -1,6 +1,6 @@ from ggate import Preference -from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const, stack_with_tphl_lh +from ggate.Components.LogicGates.SystemComponents import BaseComponent, PropertyError +from ggate.Utils import cairo_draw_text, cairo_paths, stack_with_tphl_lh from ggate.const import definitions as const import math @@ -38,13 +38,14 @@ def __init__(self, *args, **kwds): def propertyChanged(self, prop): if prop[0] > 2 ** prop[1]: - return True + return PropertyError(f"N must be less than 2^bits ({2 ** prop[1]})", [0]) + self.outpin_t = -40 - (prop[1] - 1) * 10 self.outpin_b = -40 + (prop[1] - 1) * 10 self.comp_rect = [10, min((self.outpin_t - 10, -60)), 120, max((self.outpin_b + 10, -20))] self.output_pins = [(120, y) for y in range(self.outpin_t, self.outpin_b + 1, 20)] - self.output_pins_dir = [const.direction_W for i in range(prop[1])] - self.output_level = [False for i in range(prop[1])] + self.output_pins_dir = [const.direction_W for _ in range(prop[1])] + self.output_level = [False for _ in range(prop[1])] self.tp_hl = prop[3] * 0.000001 self.tp_lh = prop[4] * 0.000001 return False @@ -102,11 +103,11 @@ def isMouseOvered(self, x, y): def initialize(self): self.store[0] = 0 - self.output_stack = [[[0.0, False]] for i in range(self.values[1])] + self.output_stack = [[[0.0, False]] for _ in range(self.values[1])] def calculate(self, input_datas, time): if input_datas[1]: - output_data = [False for i in range(self.values[1])] + output_data = [False for _ in range(self.values[1])] self.store[0] = 0 elif (self.values[2] == 0 and not self.input_level[0] and input_datas[0]) or (self.values[2] == 1 and self.input_level[0] and not input_datas[0]): # trigger self.store[0] += 1 diff --git a/ggate/Components/LogicGates/Counters/PIPOShiftRegister.py b/ggate/Components/LogicGates/Counters/PIPOShiftRegister.py index e33b446..dc284c3 100644 --- a/ggate/Components/LogicGates/Counters/PIPOShiftRegister.py +++ b/ggate/Components/LogicGates/Counters/PIPOShiftRegister.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const, stack_with_tphl_lh +from ggate.Utils import cairo_draw_text, cairo_paths, stack_with_tphl_lh from ggate.const import definitions as const @@ -39,11 +39,11 @@ def propertyChanged(self, prop): self.outpin_b = -60 + (prop[0] - 1) * 10 self.comp_rect = [10, min((self.outpin_t - 90, -90)), 120, max((self.outpin_b + 50, -30))] self.input_pins = [(10, y) for y in range(self.outpin_t - 60, self.outpin_b + 41, 20)] - self.input_pins_dir = [const.direction_E for i in range(prop[0] + 5)] - self.input_level = [False for i in range(prop[0] + 5)] + self.input_pins_dir = [const.direction_E for _ in range(prop[0] + 5)] + self.input_level = [False for _ in range(prop[0] + 5)] self.output_pins = [(120, y) for y in range(self.outpin_t, self.outpin_b + 1, 20)] - self.output_pins_dir = [const.direction_W for i in range(prop[0])] - self.output_level = [False for i in range(prop[0])] + self.output_pins_dir = [const.direction_W for _ in range(prop[0])] + self.output_level = [False for _ in range(prop[0])] self.tp_hl = prop[2] * 0.000001 self.tp_lh = prop[3] * 0.000001 return False @@ -51,7 +51,7 @@ def propertyChanged(self, prop): def drawComponent(self, cr, layout): cr.rectangle(30, self.comp_rect[1], 70, self.comp_rect[3] - self.comp_rect[1]) if self.values[1] == 1: - cr.arc(25, self.outpin_b + 20, 5, 0, 2 * math.pi) + cr.arc(25, self.outpin_b + 40, 5, 0, 2 * math.pi) cairo_paths(cr, (30, self.outpin_b + 35), (40, self.outpin_b + 40), (30, self.outpin_b + 45)) cr.stroke() cairo_draw_text(cr, layout, "PIPO", 65, self.comp_rect[1] + 10, 0.5, 0.5) @@ -110,8 +110,8 @@ def isMouseOvered(self, x, y): return False def initialize(self): - self.store = [False for i in range(self.values[0])] - self.output_stack = [[[0.0, False]] for i in range(self.values[0])] + self.store = [False for _ in range(self.values[0])] + self.output_stack = [[[0.0, False]] for _ in range(self.values[0])] def calculate(self, input_datas, time): if (self.values[1] == 0 and not self.input_level[-1] and input_datas[-1]) or (self.values[1] == 1 and self.input_level[-1] and not input_datas[-1]): # trigger diff --git a/ggate/Components/LogicGates/Counters/PISOShiftRegister.py b/ggate/Components/LogicGates/Counters/PISOShiftRegister.py index d478f28..bab4585 100644 --- a/ggate/Components/LogicGates/Counters/PISOShiftRegister.py +++ b/ggate/Components/LogicGates/Counters/PISOShiftRegister.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const, stack_with_tphl_lh +from ggate.Utils import cairo_draw_text, cairo_paths, stack_with_tphl_lh from ggate.const import definitions as const @@ -39,8 +39,8 @@ def propertyChanged(self, prop): self.inpin_b = -60 + (prop[0] - 1) * 10 self.comp_rect = [10, min((self.inpin_t - 50, -90)), 120, max((self.inpin_b + 30, -30))] self.input_pins = [(10, y) for y in range(self.inpin_t - 20, self.inpin_b + 21, 20)] - self.input_pins_dir = [const.direction_E for i in range(prop[0] + 2)] - self.input_level = [False for i in range(prop[0] + 2)] + self.input_pins_dir = [const.direction_E for _ in range(prop[0] + 2)] + self.input_level = [False for _ in range(prop[0] + 2)] self.tp_hl = prop[2] * 0.000001 self.tp_lh = prop[3] * 0.000001 return False @@ -108,7 +108,7 @@ def isMouseOvered(self, x, y): return False def initialize(self): - self.store = [False for i in range(self.values[0])] + self.store = [False for _ in range(self.values[0])] self.output_stack = [[[0.0, False]], [[0.0, True]]] def calculate(self, input_datas, time): diff --git a/ggate/Components/LogicGates/Counters/SIPOShiftRegister.py b/ggate/Components/LogicGates/Counters/SIPOShiftRegister.py index 5e6d6fb..0cf5623 100644 --- a/ggate/Components/LogicGates/Counters/SIPOShiftRegister.py +++ b/ggate/Components/LogicGates/Counters/SIPOShiftRegister.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const, stack_with_tphl_lh +from ggate.Utils import cairo_draw_text, cairo_paths, stack_with_tphl_lh from ggate.const import definitions as const @@ -40,11 +40,11 @@ def propertyChanged(self, prop): self.outpin_b = -40 + (prop[0] - 1) * 10 self.comp_rect = [10, min((self.outpin_t - 30, -90)), 120, max((self.outpin_b + 10, -10))] self.output_pins = [(120, y) for y in range(self.outpin_t, self.outpin_b + 1, 20)] - self.output_pins_dir = [const.direction_W for i in range(prop[0])] - self.output_level = [False for i in range(prop[0])] + self.output_pins_dir = [const.direction_W for _ in range(prop[0])] + self.output_level = [False for _ in range(prop[0])] self.tp_hl = prop[2] * 0.000001 self.tp_lh = prop[3] * 0.000001 - self.store = [False for i in range(prop[0])] + self.store = [False for _ in range(prop[0])] return False def drawComponent(self, cr, layout): @@ -108,12 +108,12 @@ def isMouseOvered(self, x, y): return False def initialize(self): - self.store = [False for i in range(self.values[0])] - self.output_stack = [[[0.0, False]] for i in range(self.values[0])] + self.store = [False for _ in range(self.values[0])] + self.output_stack = [[[0.0, False]] for _ in range(self.values[0])] def calculate(self, input_datas, time): if input_datas[2]: - self.store = [False for i in range(self.values[0])] + self.store = [False for _ in range(self.values[0])] elif (self.values[1] == 0 and not self.input_level[1] and input_datas[1]) or (self.values[1] == 1 and self.input_level[1] and not input_datas[1]): # trigger self.store.insert(0, input_datas[0]) self.store.pop() diff --git a/ggate/Components/LogicGates/Counters/SISOShiftRegister.py b/ggate/Components/LogicGates/Counters/SISOShiftRegister.py index 29dc443..7eee1cd 100644 --- a/ggate/Components/LogicGates/Counters/SISOShiftRegister.py +++ b/ggate/Components/LogicGates/Counters/SISOShiftRegister.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const, stack_with_tphl_lh +from ggate.Utils import cairo_draw_text, cairo_paths, stack_with_tphl_lh from ggate.const import definitions as const @@ -97,7 +97,7 @@ def isMouseOvered(self, x, y): def initialize(self): self.store[0] = 0 - self.stack = [False for i in range(self.values[0])] + self.stack = [False for _ in range(self.values[0])] self.output_stack = [[[0.0, False]], [[0.0, True]]] def calculate(self, input_datas, time): diff --git a/ggate/Components/LogicGates/Miscellaneous/Probe.py b/ggate/Components/LogicGates/Miscellaneous/Probe.py index ee46dc6..65cf69c 100644 --- a/ggate/Components/LogicGates/Miscellaneous/Probe.py +++ b/ggate/Components/LogicGates/Miscellaneous/Probe.py @@ -1,5 +1,5 @@ from ggate import Preference -from ggate.Components.LogicGates.SystemComponents import BaseComponent +from ggate.Components.LogicGates.SystemComponents import BaseComponent, PropertyError from ggate.Utils import cairo_draw_text, cairo_paths from ggate.const import definitions from gi.repository import Pango @@ -25,16 +25,14 @@ def __init__(self, *args, **kwds): self.width = 12 def propertyChanged(self, prop): - if prop[0] == "": - return True - return False + return PropertyError("Name cannot be empty.", [0]) if prop[0] == "" else False def drawComponent(self, cr, layout): cr.arc(40, -10, 10, 0, 2 * math.pi) cr.stroke() cairo_draw_text(cr, layout, "V", 40, -10, 0.5, 0.5) cairo_draw_text(cr, layout, self.values[0], 55, -10, 0.0, 0.5) - (w, h) = layout.get_size() + (w, _) = layout.get_size() self.width = w / Pango.SCALE if self.width < 12: self.width = 12 diff --git a/ggate/Components/LogicGates/Miscellaneous/Text.py b/ggate/Components/LogicGates/Miscellaneous/Text.py index 2282794..cb85be1 100644 --- a/ggate/Components/LogicGates/Miscellaneous/Text.py +++ b/ggate/Components/LogicGates/Miscellaneous/Text.py @@ -1,5 +1,5 @@ -from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import Pango, cairo_draw_text +from ggate.Components.LogicGates.SystemComponents import BaseComponent, PropertyError +from ggate.Utils import cairo_draw_text from ggate.const import definitions from gi.repository import Pango @@ -19,9 +19,7 @@ def __init__(self, *args, **kwds): self.height = 12 def propertyChanged(self, prop): - if prop[0] == "": - return True - return False + return PropertyError("Name cannot be empty.", [0]) if prop[0] == "" else False def drawComponent(self, cr, layout): cairo_draw_text(cr, layout, self.values[0], 10, -10, 0.0, 0.5) diff --git a/ggate/Components/LogicGates/Standard/AND.py b/ggate/Components/LogicGates/Standard/AND.py index 2a3a07b..6d8ae52 100644 --- a/ggate/Components/LogicGates/Standard/AND.py +++ b/ggate/Components/LogicGates/Standard/AND.py @@ -103,7 +103,7 @@ def isMouseOvered(self, x, y): return False def calculate(self, input_datas, time): - new_output = input_datas[0] and input_datas[1] if self.values[0] == 2 else input_datas[0] and input_datas[1] and input_datas[2] + new_output = all(input_datas) if new_output != self.output_level[0]: if self.output_level[0]: self.output_stack = [[[time + self.tp_hl, new_output]]] diff --git a/ggate/Components/LogicGates/Standard/GND.py b/ggate/Components/LogicGates/Standard/GND.py index 606f859..7867b79 100644 --- a/ggate/Components/LogicGates/Standard/GND.py +++ b/ggate/Components/LogicGates/Standard/GND.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const +from ggate.Utils import cairo_draw_text, cairo_paths from ggate.const import definitions as const diff --git a/ggate/Components/LogicGates/Standard/NAND.py b/ggate/Components/LogicGates/Standard/NAND.py index da828b4..d8801b6 100644 --- a/ggate/Components/LogicGates/Standard/NAND.py +++ b/ggate/Components/LogicGates/Standard/NAND.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const +from ggate.Utils import cairo_draw_text, cairo_paths from ggate.const import definitions as const @@ -98,7 +98,7 @@ def isMouseOvered(self, x, y): return False def calculate(self, input_datas, time): - new_output = not (input_datas[0] and input_datas[1]) if self.values[0] == 2 else not (input_datas[0] and input_datas[1] and input_datas[2]) + new_output = not all(input_datas) if new_output != self.output_level[0]: if self.output_level[0]: self.output_stack = [[[time + self.tp_hl, new_output]]] diff --git a/ggate/Components/LogicGates/Standard/NOR.py b/ggate/Components/LogicGates/Standard/NOR.py index 790f6f7..06d1c03 100644 --- a/ggate/Components/LogicGates/Standard/NOR.py +++ b/ggate/Components/LogicGates/Standard/NOR.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_bezier, cairo_draw_text, cairo_paths, const +from ggate.Utils import cairo_bezier, cairo_draw_text, cairo_paths from ggate.const import definitions as const @@ -114,7 +114,7 @@ def isMouseOvered(self, x, y): return False def calculate(self, input_datas, time): - new_output = not (input_datas[0] or input_datas[1]) if self.values[0] == 2 else not (input_datas[0] or input_datas[1] or input_datas[2]) + new_output = not any(input_datas) if new_output != self.output_level[0]: if self.output_level[0]: self.output_stack = [[[time + self.tp_hl, new_output]]] diff --git a/ggate/Components/LogicGates/Standard/OR.py b/ggate/Components/LogicGates/Standard/OR.py index 95802b1..7b37958 100644 --- a/ggate/Components/LogicGates/Standard/OR.py +++ b/ggate/Components/LogicGates/Standard/OR.py @@ -124,7 +124,7 @@ def isMouseOvered(self, x, y): return False def calculate(self, input_datas, time): - new_output = input_datas[0] or input_datas[1] if self.values[0] == 2 else input_datas[0] or input_datas[1] or input_datas[2] + new_output = any(input_datas) if new_output != self.output_level[0]: if self.output_level[0]: self.output_stack = [[[time + self.tp_hl, new_output]]] diff --git a/ggate/Components/LogicGates/Standard/OSC.py b/ggate/Components/LogicGates/Standard/OSC.py index 8877195..7bab7ef 100644 --- a/ggate/Components/LogicGates/Standard/OSC.py +++ b/ggate/Components/LogicGates/Standard/OSC.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_paths, const +from ggate.Utils import cairo_paths from ggate.const import definitions as const diff --git a/ggate/Components/LogicGates/Standard/SW.py b/ggate/Components/LogicGates/Standard/SW.py index 829d490..0a592e7 100644 --- a/ggate/Components/LogicGates/Standard/SW.py +++ b/ggate/Components/LogicGates/Standard/SW.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_draw_text, cairo_paths, const +from ggate.Utils import cairo_draw_text, cairo_paths from ggate.const import definitions as const @@ -62,6 +62,7 @@ def initialize(self): def click(self, x, y, time): self.store[0] = not self.store[0] + self.values[0] = int(self.store[0]) self.clicked_time = time return True diff --git a/ggate/Components/LogicGates/Standard/TSB.py b/ggate/Components/LogicGates/Standard/TSB.py index c17dfb5..06b9427 100644 --- a/ggate/Components/LogicGates/Standard/TSB.py +++ b/ggate/Components/LogicGates/Standard/TSB.py @@ -25,9 +25,9 @@ def __init__(self, *args, **kwds): self.active_high = True self.prop_names = ['active', 'inverted'] - self.properties.append((_("Active :"), (definitions.property_select, _("Low"), _("High")), "")) + self.properties.append((_("Active:"), (definitions.property_select, _("Low"), _("High")), "")) self.values.append(1) - self.properties.append((_("Inverted :"), (definitions.property_select, _("True"), _("False")), "")) + self.properties.append((_("Inverted:"), (definitions.property_select, _("True"), _("False")), "")) self.values.append(1) def propertyChanged(self, prop): diff --git a/ggate/Components/LogicGates/Standard/XOR.py b/ggate/Components/LogicGates/Standard/XOR.py index 32a68a1..cbea51f 100644 --- a/ggate/Components/LogicGates/Standard/XOR.py +++ b/ggate/Components/LogicGates/Standard/XOR.py @@ -1,6 +1,6 @@ from ggate import Preference from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.Utils import cairo_bezier, cairo_draw_text, cairo_paths, const +from ggate.Utils import cairo_bezier, cairo_draw_text, cairo_paths from ggate.const import definitions as const @@ -110,8 +110,7 @@ def isMouseOvered(self, x, y): return False def calculate(self, input_datas, time): - print(input_datas) - new_output = (input_datas[0] + input_datas[1]) % 2 if self.values[0] == 2 else (input_datas[0] + input_datas[1] + input_datas[2]) % 2 + new_output = sum(input_datas) % 2 if new_output != self.output_level[0]: if self.output_level[0]: self.output_stack = [[[time + self.tp_hl, new_output]]] diff --git a/ggate/Components/LogicGates/StateViewers/LED.py b/ggate/Components/LogicGates/StateViewers/LED.py index 4f2907b..c17f785 100644 --- a/ggate/Components/LogicGates/StateViewers/LED.py +++ b/ggate/Components/LogicGates/StateViewers/LED.py @@ -19,7 +19,7 @@ def __init__(self, *args, **kwds): self.output_level = [] self.prop_names = ['_red', '_blue', '_green', '_yellow'] - self.properties.append((_("Color :"), (definitions.property_select, _('Red'), _('Blue'), _('Green'), _('Yellow')), '')) + self.properties.append((_("Color:"), (definitions.property_select, _('Red'), _('Blue'), _('Green'), _('Yellow')), '')) self.values.append(2) def drawComponent(self, cr, layout): diff --git a/ggate/Components/LogicGates/StateViewers/SevenSegment.py b/ggate/Components/LogicGates/StateViewers/SevenSegment.py index bb0f6f1..1704b0c 100644 --- a/ggate/Components/LogicGates/StateViewers/SevenSegment.py +++ b/ggate/Components/LogicGates/StateViewers/SevenSegment.py @@ -18,8 +18,7 @@ def __init__(self, *args, **kwds): self.output_level = [] self.prop_names = ['_red', '_blue', '_green', '_yellow'] - self.properties.append((_("Color of 7 Segment LEDs :"), None, '')) - self.properties.append((_("Color :"), (definitions.property_select, _('Red'), _('Blue'), _('Green'), _('Yellow')), '')) + self.properties.append((_("Color:"), (definitions.property_select, _('Red'), _('Blue'), _('Green'), _('Yellow')), '')) self.values.append(2) def set_7seg(self, cr: cairo.Context, a, b, c, d, e, f, g): diff --git a/ggate/Components/LogicGates/SystemComponents.py b/ggate/Components/LogicGates/SystemComponents.py index 07c7370..65fc0aa 100644 --- a/ggate/Components/LogicGates/SystemComponents.py +++ b/ggate/Components/LogicGates/SystemComponents.py @@ -1,10 +1,14 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- from abc import ABC, abstractmethod -from gettext import gettext as _ +from typing import List from gi.repository import Pango import cairo +class PropertyError: + def __init__(self, message: str, positions: List[int] = None): + self.positions = positions if positions is not None else [] + self.message = message + class BaseComponent(ABC): def __init__(self, *args, **kwds): self.description = "" @@ -73,14 +77,14 @@ def set_rot_props(self): max((self.rot_comp_rect[1], self.rot_comp_rect[3]))] self.rot_input_pins = self.input_pins[:] self.rot_input_pins_dir = self.input_pins_dir[:] - for i, p in enumerate(self.input_pins): + for i, _ in enumerate(self.input_pins): self.rot_input_pins[i] = (self.matrix[0] * self.rot_input_pins[i][0] + self.matrix[1] * self.rot_input_pins[i][1], self.matrix[2] * self.rot_input_pins[i][0] + self.matrix[3] * self.rot_input_pins[i][1]) self.rot_input_pins_dir[i] = (self.matrix[0] * self.rot_input_pins_dir[i][0] + self.matrix[1] * self.rot_input_pins_dir[i][1], self.matrix[2] * self.rot_input_pins_dir[i][0] + self.matrix[3] * self.rot_input_pins_dir[i][1]) self.rot_output_pins = self.output_pins[:] self.rot_output_pins_dir = self.output_pins_dir[:] - for i, p in enumerate(self.output_pins): + for i, _ in enumerate(self.output_pins): self.rot_output_pins[i] = (self.matrix[0] * self.rot_output_pins[i][0] + self.matrix[1] * self.rot_output_pins[i][1], self.matrix[2] * self.rot_output_pins[i][0] + self.matrix[3] * self.rot_output_pins[i][1]) self.rot_output_pins_dir[i] = (self.matrix[0] * self.rot_output_pins_dir[i][0] + self.matrix[1] * self.rot_output_pins_dir[i][1], @@ -89,7 +93,7 @@ def set_rot_props(self): def click(self, x, y, time): return False - def propertyChanged(self, prop): + def propertyChanged(self, prop) -> bool | PropertyError: return False def initialize(self): diff --git a/ggate/Components/LogicGates/__init__.py b/ggate/Components/LogicGates/__init__.py index 099ef90..a363482 100644 --- a/ggate/Components/LogicGates/__init__.py +++ b/ggate/Components/LogicGates/__init__.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- -from gettext import gettext as _ from ggate.const import definitions from . import Calculator diff --git a/ggate/Components/Managers/Alerts.py b/ggate/Components/Managers/Alerts.py index 01fabe3..6d2e43c 100644 --- a/ggate/Components/Managers/Alerts.py +++ b/ggate/Components/Managers/Alerts.py @@ -3,13 +3,12 @@ if TYPE_CHECKING: from ggate.MainFrame import MainFrame -from gi.repository import Gtk, Adw +from gi.repository import Adw # TODO: translations -class AlertDialogs(Adw.AlertDialog): - def __init__(self, mainframe: MainFrame, *args, **kwargs): - super().__init__(*args, **kwargs) +class AlertDialogs: + def __init__(self, mainframe: MainFrame): self.mainframe = mainframe self.responses = [("cancel", "Cancel"), ("yes", "Yes"), ("no", "No")] @@ -17,71 +16,63 @@ def __init__(self, mainframe: MainFrame, *args, **kwargs): self.YES_RESPONSE = self.responses[1][0] self.NO_RESPONSE = self.responses[2][0] - def __get_unsaved_buffer_alert_result(self, allow, deny): - def internal(dialog, result): - result = self.choose_finish(result) - if result == self.CANCEL_RESPONSE: + def _create_dialog(self, heading: str, body: str) -> Adw.AlertDialog: + dialog = Adw.AlertDialog() + dialog.set_heading(heading) + dialog.set_body(body) + for response_id, label in self.responses: + dialog.add_response(response_id, label) + dialog.set_default_response(self.YES_RESPONSE) + dialog.set_close_response(self.CANCEL_RESPONSE) + return dialog + + def __get_unsaved_buffer_alert_result(self, dialog, allow, deny): + def internal(dialog_obj, result): + res = dialog.choose_finish(result) + if res == self.CANCEL_RESPONSE: return - elif result == self.YES_RESPONSE: + elif res == self.YES_RESPONSE: allow() else: deny() - return internal - - def alert_open_unsaved_buffer(self): - self.set_heading("Unsaved changes") - self.set_body("Buffer was modified, save changes?") - self.add_responses(*self.responses) - self.set_default_response(self.YES_RESPONSE) - self.set_close_response(self.CANCEL_RESPONSE) - - self.choose( + def alert_open_unsaved_buffer(self): + dialog = self._create_dialog("Unsaved changes", "Buffer was modified, save changes?") + dialog.choose( parent=self.mainframe, callback=self.__get_unsaved_buffer_alert_result( + dialog, self.mainframe.on_action_save_pressed, self.mainframe.file_manager.open_activated, ) ) def alert_new_unsaved_buffer(self): - self.set_heading("Unsaved changes") - self.set_body("Buffer was modified, save changes before creating new buffer?") - - self.add_responses(*self.responses) - self.set_default_response(self.YES_RESPONSE) - self.set_close_response(self.CANCEL_RESPONSE) - - self.set_response_appearance( + dialog = self._create_dialog("Unsaved changes", "Buffer was modified, save changes before creating new buffer?") + dialog.set_response_appearance( self.NO_RESPONSE, Adw.ResponseAppearance.DESTRUCTIVE ) - - self.choose( + dialog.choose( parent=self.mainframe, callback=self.__get_unsaved_buffer_alert_result( + dialog, self.mainframe.on_action_save_pressed, self.mainframe.create_new_buffer, ) ) def alert_close_unsaved_buffer(self): - self.set_heading("Unsaved changes") - self.set_body("Buffer was modified, save changes before closing?") - - self.add_responses(*self.responses) - self.set_default_response(self.YES_RESPONSE) - self.set_close_response(self.CANCEL_RESPONSE) - - self.set_response_appearance( + dialog = self._create_dialog("Unsaved changes", "Buffer was modified, save changes before closing?") + dialog.set_response_appearance( self.NO_RESPONSE, Adw.ResponseAppearance.DESTRUCTIVE ) - - self.choose( + dialog.choose( parent=self.mainframe, callback=self.__get_unsaved_buffer_alert_result( + dialog, self.mainframe.on_action_save_pressed, self.mainframe.application.quit, ) diff --git a/ggate/Components/Managers/Exporter.py b/ggate/Components/Managers/Exporter.py deleted file mode 100644 index d1d5204..0000000 --- a/ggate/Components/Managers/Exporter.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from ggate.MainFrame import MainFrame - -class Exporter: - def __init__(self, mainframe: MainFrame): - self.mainframe = mainframe - - \ No newline at end of file diff --git a/ggate/Components/Managers/FileManager.py b/ggate/Components/Managers/FileManager.py index 2039d40..79a1e64 100644 --- a/ggate/Components/Managers/FileManager.py +++ b/ggate/Components/Managers/FileManager.py @@ -100,6 +100,7 @@ def for_img(cls, mainframe: MainFrame): return fm + @classmethod def for_schematics(cls, mainframe: MainFrame): # TODO: Implement schematics pass diff --git a/ggate/Components/Windows/About.py b/ggate/Components/Windows/About.py new file mode 100644 index 0000000..2f7e271 --- /dev/null +++ b/ggate/Components/Windows/About.py @@ -0,0 +1,27 @@ +from ggate.const import Definitions +from ggate.config import APP_PREFIX, VERSION +from gi.repository import Adw + +class AboutGGate: + @staticmethod + def create() -> Adw.AboutDialog: + about_dialog = Adw.AboutDialog.new() + about_dialog.set_version(VERSION) + about_dialog.set_application_icon(APP_PREFIX) + about_dialog.set_application_name(Definitions.app_name) + about_dialog.set_comments(Definitions.description) + about_dialog.set_copyright(Definitions.copyright) + about_dialog.set_website(Definitions.website) + about_dialog.set_license_type(Definitions.license) + about_dialog.set_developers(Definitions.developer) + about_dialog.set_issue_url(Definitions.devel_bug) + about_dialog.set_developer_name(Definitions.developer_name) + + # TODO: Add support for release notes and other features + # about_dialog.set_website() + # about_dialog.set_release_notes() + # about_dialog.set_support_url() + # about_dialog.set_debug_info() + # about_dialog.set_debug_info_filename() + + return about_dialog diff --git a/ggate/Components/Windows/Preferences.py b/ggate/Components/Windows/Preferences.py new file mode 100644 index 0000000..87b9523 --- /dev/null +++ b/ggate/Components/Windows/Preferences.py @@ -0,0 +1,299 @@ +from ggate import Preference +from ggate.Themes import THEME_REGISTRY, apply_theme +from gi.repository import Gtk, Gdk, Adw, Pango +from gettext import gettext as _ + + +class PreferencesWindow(Adw.PreferencesDialog): + def __init__(self, parent): + super().__init__() + self.main_frame = parent + self.color_buttons = {} + self._suppress_custom = False # guard: don't mark Custom while theme is loading + + self._build_ui() + self._populate_values() + self._connect_signals() + + def _build_ui(self): + self.add(self._build_appearance_page()) + self.add(self._build_simulation_page()) + self.add(self._build_canvas_page()) + + def _build_appearance_page(self): + page = Adw.PreferencesPage() + page.set_title(_("Appearance")) + page.set_icon_name("preferences-desktop-wallpaper-symbolic") + + # ── Theme group ─────────────────────────────────────────────────────── + group_theme = Adw.PreferencesGroup() + group_theme.set_title(_("Theme")) + + theme_names = list(THEME_REGISTRY.keys()) + ["Custom"] + self.theme_row = Adw.ComboRow() + self.theme_row.set_title(_("Theme Style")) + self.theme_row.set_model(Gtk.StringList.new(theme_names)) + group_theme.add(self.theme_row) + page.add(group_theme) + + # ── Typography & Style ──────────────────────────────────────────────── + group_style = Adw.PreferencesGroup() + group_style.set_title(_("Typography & Style")) + + font_row = Adw.ActionRow() + font_row.set_title(_("Font")) + + font_dialog = Gtk.FontDialog() + monospace_filter = Gtk.CustomFilter.new(self._monospace_filter_func) + font_dialog.set_filter(monospace_filter) + + self.drawing_font_btn = Gtk.FontDialogButton.new(font_dialog) + self.drawing_font_btn.set_use_font(True) + self.drawing_font_btn.set_use_size(True) + self.drawing_font_btn.set_halign(Gtk.Align.END) + self.drawing_font_btn.set_valign(Gtk.Align.CENTER) + font_row.add_suffix(self.drawing_font_btn) + group_style.add(font_row) + + self.symbol_type_row = Adw.ComboRow() + self.symbol_type_row.set_title(_("Symbol style")) + self.symbol_type_row.set_model(Gtk.StringList.new([_("MIL/ANSI"), _("IEC")])) + group_style.add(self.symbol_type_row) + page.add(group_style) + + # ── Canvas Colors ───────────────────────────────────────────────────── + group_canvas = Adw.PreferencesGroup() + group_canvas.set_title(_("Canvas Colors")) + group_canvas.add(self._create_color_row(_("Background (edit mode):"), "bg_color")) + group_canvas.add(self._create_color_row(_("Background (running):"), "bg_color_running")) + group_canvas.add(self._create_color_row(_("Grid:"), "grid_color")) + group_canvas.add(self._create_color_row(_("Cursor:"), "cursor_color")) + page.add(group_canvas) + + group_comp = Adw.PreferencesGroup() + group_comp.set_title(_("Component Colors")) + group_comp.add(self._create_color_row(_("Component (default):"), "component_color")) + group_comp.add(self._create_color_row(_("Component (highlighted):"), "component_high_color")) + group_comp.add(self._create_color_row(_("Component (running):"), "component_color_running")) + group_comp.add(self._create_color_row(_("Component (picked):"), "picked_color")) + group_comp.add(self._create_color_row(_("Component (pre-added):"), "preadd_color")) + group_comp.add(self._create_color_row(_("Component (selected):"), "selected_color")) + page.add(group_comp) + + group_net = Adw.PreferencesGroup() + group_net.set_title(_("Net Colors")) + group_net.add(self._create_color_row(_("Net (default):"), "net_color")) + group_net.add(self._create_color_row(_("Net (highlighted):"), "net_high_color")) + group_net.add(self._create_color_row(_("Net (running):"), "net_color_running")) + group_net.add(self._create_color_row(_("Net (high level):"), "highlevel_color")) + group_net.add(self._create_color_row(_("Net (low level):"), "lowlevel_color")) + page.add(group_net) + + group_term = Adw.PreferencesGroup() + group_term.set_title(_("Terminals")) + group_term.add(self._create_color_row(_("Terminal (edit):"), "terminal_color")) + group_term.add(self._create_color_row(_("Terminal (running):"), "terminal_color_running")) + page.add(group_term) + + return page + + def _build_simulation_page(self): + page = Adw.PreferencesPage() + page.set_title(_("Simulation")) + page.set_icon_name("media-playback-start-symbolic") + + group = Adw.PreferencesGroup() + group.set_title(_("Performance & Constraints")) + + self.calc_iter_row = Adw.SpinRow() + self.calc_iter_row.set_title(_("Max calculation iterations")) + group.add(self.calc_iter_row) + + self.calc_duration_row = Adw.SpinRow() + self.calc_duration_row.set_title(_("Max calculation duration (µs)")) + group.add(self.calc_duration_row) + + page.add(group) + + playback_group = Adw.PreferencesGroup() + playback_group.set_title(_("Playback")) + + self.playback_duration_row = Adw.SpinRow() + self.playback_duration_row.set_title(_("Animation duration (s)")) + playback_group.add(self.playback_duration_row) + + page.add(playback_group) + return page + + def _build_canvas_page(self): + page = Adw.PreferencesPage() + page.set_title(_("Canvas")) + page.set_icon_name("input-mouse-symbolic") + + group = Adw.PreferencesGroup() + group.set_title(_("Layout & Grid")) + + self.autocenter_row = Adw.SwitchRow() + self.autocenter_row.set_title(_("Auto-center content on resize")) + group.add(self.autocenter_row) + + page.add(group) + return page + + def _create_color_row(self, title, key): + row = Adw.ActionRow() + row.set_title(title) + + color_dialog = Gtk.ColorDialog() + btn = Gtk.ColorDialogButton.new(color_dialog) + btn.set_halign(Gtk.Align.END) + btn.set_valign(Gtk.Align.CENTER) + + self.color_buttons[key] = btn + row.add_suffix(btn) + return row + + def _monospace_filter_func(self, item, user_data=None): + if isinstance(item, Pango.FontFace): + family = item.get_family() + elif isinstance(item, Pango.FontFamily): + family = item + else: + return False + return family.is_monospace() + + def _populate_values(self): + self.drawing_font_btn.set_font_desc(Preference.drawing_font) + self.symbol_type_row.set_selected(Preference.symbol_type) + + active_theme = Preference.theme + theme_names = list(THEME_REGISTRY.keys()) + ["Custom"] + idx = theme_names.index(active_theme) if active_theme in theme_names else len(theme_names) - 1 + + self._suppress_custom = True + self.theme_row.set_selected(idx) + for key in self.color_buttons: + self._sync_button_from_preference(key) + self._suppress_custom = False + + adj_iter = Gtk.Adjustment.new(float(Preference.max_calc_iters), 10.0, 1000000.0, 1.0, 10.0, 0.0) + self.calc_iter_row.set_adjustment(adj_iter) + self.calc_iter_row.set_digits(0) + + adj_dur = Gtk.Adjustment.new(Preference.max_calc_duration * 1000000.0, 0.0, 100000.0, 1.0, 10.0, 0.0) + self.calc_duration_row.set_adjustment(adj_dur) + self.calc_duration_row.set_digits(3) + + adj_playback = Gtk.Adjustment.new(float(Preference.playback_duration), 0.5, 60.0, 0.5, 1.0, 0.0) + self.playback_duration_row.set_adjustment(adj_playback) + self.playback_duration_row.set_digits(1) + + self.autocenter_row.set_active(bool(Preference.autocenter_resize)) + + def _sync_button_from_preference(self, key): + pattern = Preference.__getattr__(key) + r, g, b, a = pattern.get_rgba() + rgba = Gdk.RGBA() + rgba.red, rgba.green, rgba.blue, rgba.alpha = r, g, b, a + btn = self.color_buttons.get(key) + if btn: + btn.set_rgba(rgba) + + def _connect_signals(self): + self.theme_row.connect("notify::selected", self._on_theme_changed) + self.drawing_font_btn.connect("notify::font-desc", self._on_font_changed) + self.symbol_type_row.connect("notify::selected", self._on_symbol_type_changed) + + for key, btn in self.color_buttons.items(): + btn.connect("notify::rgba", self._on_color_changed, key) + + self.calc_iter_row.connect("notify::value", self._on_calc_iters_changed) + self.calc_duration_row.connect("notify::value", self._on_calc_duration_changed) + self.playback_duration_row.connect("notify::value", self._on_playback_duration_changed) + self.autocenter_row.connect("notify::active", self._on_autocenter_changed) + + # ── signal handlers ─────────────────────────────────────────────────────── + + def _on_theme_changed(self, row, pspec): + theme_names = list(THEME_REGISTRY.keys()) + ["Custom"] + selected = theme_names[row.get_selected()] + if selected == "Custom": + Preference.__setattr__("theme", "Custom") + Preference.save_settings() + return + + # load all palette colors into Preference + save + update chrome + # suppress through the whole load: apply_theme + button sync both emit + # notify::rgba, which would otherwise flip the row back to "Custom" + self._suppress_custom = True + apply_theme(selected, Preference) + for key in self.color_buttons: + self._sync_button_from_preference(key) + self._suppress_custom = False + + self._apply_chrome(selected) + self._trigger_canvas_redraw() + + def _on_font_changed(self, button, pspec): + font_desc = button.get_font_desc() + if font_desc: + Preference.drawing_font = font_desc.to_string() + Preference.save_settings() + self._trigger_canvas_redraw() + + def _on_symbol_type_changed(self, row, pspec): + Preference.symbol_type = row.get_selected() + Preference.save_settings() + if self.main_frame and hasattr(self.main_frame, "comp_window"): + self.main_frame.comp_window.refresh_icons() + self._trigger_canvas_redraw() + + def _on_color_changed(self, button, pspec, key): + if self._suppress_custom: + return + rgba = button.get_rgba() + if rgba: + Preference.__setattr__(key, f"{rgba.red},{rgba.green},{rgba.blue},{rgba.alpha}") + Preference.save_settings() + # any individual tweak → switch combo to "Custom" + self._set_theme_row_custom() + self._trigger_canvas_redraw() + + def _on_calc_iters_changed(self, row, pspec): + Preference.max_calc_iters = int(row.get_value()) + Preference.save_settings() + self._trigger_canvas_redraw() + + def _on_calc_duration_changed(self, row, pspec): + Preference.max_calc_duration = row.get_value() * 0.000001 + Preference.save_settings() + self._trigger_canvas_redraw() + + def _on_autocenter_changed(self, row, pspec): + Preference.autocenter_resize = int(row.get_active()) + Preference.save_settings() + self._trigger_canvas_redraw() + + def _on_playback_duration_changed(self, row, pspec): + Preference.playback_duration = row.get_value() + Preference.save_settings() + + # ── helpers ─────────────────────────────────────────────────────────────── + + def _set_theme_row_custom(self): + theme_names = list(THEME_REGISTRY.keys()) + ["Custom"] + custom_idx = len(theme_names) - 1 + self._suppress_custom = True + self.theme_row.set_selected(custom_idx) + self._suppress_custom = False + Preference.__setattr__("theme", "Custom") + Preference.save_settings() + + def _apply_chrome(self, theme_name: str): + from ggate.Themes import apply_chrome + apply_chrome(theme_name, Gdk.Display.get_default()) + + def _trigger_canvas_redraw(self): + if self.main_frame and hasattr(self.main_frame, "drawarea"): + self.main_frame.drawarea.redraw = True + self.main_frame.drawarea.queue_draw() diff --git a/ggate/Components/Windows/Properties.py b/ggate/Components/Windows/Properties.py new file mode 100644 index 0000000..ff2dcd1 --- /dev/null +++ b/ggate/Components/Windows/Properties.py @@ -0,0 +1,211 @@ + +from typing import Optional +from ggate.Components.LogicGates.SystemComponents import BaseComponent, PropertyError +from ggate.const import Definitions, definitions as const +from gi.repository import Gtk, GObject, Adw +from gettext import gettext as _ + +class PropertyWindow(Adw.Dialog): + __gsignals__ = { + "property-changed": (GObject.SIGNAL_RUN_FIRST, None, ()), + "window-hidden": (GObject.SIGNAL_RUN_LAST, None, ()), + } + + def on_window_delete(self, *args): + self._is_presented = False + self.emit("window-hidden") + + def dismiss(self): + if self._is_presented: + self.close() + + def __init__(self): + self.title = _("Properties") + + Adw.Dialog.__init__(self, title=self.title) + self.set_size_request(310, -1) + + self.prop_controls = [] + self.old_values = [] + self.component = None + self._is_presented = False + + self.set_content_width(350) + self.connect("closed", self.on_window_delete) + + # Build persistent UI hierarchy once to prevent widget leaks + self.header_bar = Adw.HeaderBar() + self.banner = Adw.Banner() + self.banner.set_revealed(False) + + self.toolbar_view = Adw.ToolbarView() + self.toolbar_view.add_top_bar(self.header_bar) + self.toolbar_view.add_top_bar(self.banner) + self.set_child(self.toolbar_view) + + def _set_invalid_state(self, positions): + for idx, ctrl in enumerate(self.prop_controls): + if idx in positions: + ctrl.add_css_class("error") + else: + ctrl.remove_css_class("error") + + def on_apply_btn_clicked(self, *widget): + values = [] + property_index = 0 + + if self.component is None: + return + + for property in self.component.properties: + if isinstance(property[1], tuple): + if property[1][0] == const.property_select: + values.append(self.prop_controls[property_index].get_selected()) + + elif property[1][0] == const.property_int: + values.append(int(self.prop_controls[property_index].get_value())) + + elif property[1][0] == const.property_float: + values.append(self.prop_controls[property_index].get_value()) + + else: + values.append(self.prop_controls[property_index].get_text()) + + elif property[1] == const.property_bool: + values.append(self.prop_controls[property_index].get_active()) + + else: + property_index -= 1 + + property_index += 1 + + valid_property_spec = self.component.propertyChanged(values) + + if valid_property_spec is not False and isinstance(valid_property_spec, PropertyError) and self.banner is not None: + message = valid_property_spec.message + property_pos = valid_property_spec.positions + self.banner.set_title(message) + self.banner.set_revealed(True) + self._set_invalid_state(property_pos) + else: + self.component.values = values + self.component.set_rot_props() + self.banner.set_revealed(False) + self._set_invalid_state([]) + + self.emit("property-changed") + + def show_properties(self, component: Optional[BaseComponent], parent: Optional[Gtk.Widget] = None): + if component is None: + self.dismiss() + return + + self.component = component + self.set_title("%s - %s" % (self.title, component.description)) + self.banner.set_revealed(False) + + if len(component.properties) == 0: + status_page = Adw.StatusPage( + title=_("No properties"), + description=_("This component has no editable properties."), + ) + + self.toolbar_view.set_content(status_page) + if parent is not None: + self.present(parent) + self._is_presented = True + return + + property_idx = 0 + self.prop_controls = [] + groups = [] + group_counts = {} + current_group = None + + for property in component.properties: + has_property = True + + if property[1] == Definitions.property_bool: + ctrl = Adw.SwitchRow(title=property[0]) + ctrl.set_active(component.values[property_idx]) + ctrl.connect("notify::active", self.on_apply_btn_clicked) + + elif isinstance(property[1], tuple): + if property[1][0] == const.property_select: + ctrl = Adw.ComboRow(title=property[0]) + model = Gtk.StringList.new(property[1][1:]) + + ctrl.set_model(model) + ctrl.set_selected(self.component.values[property_idx]) + ctrl.connect("notify::selected", self.on_apply_btn_clicked) + + elif property[1][0] == const.property_int: + ctrl = Adw.SpinRow.new_with_range( + property[1][1], + property[1][2], + 1.0 + ) + + ctrl.set_digits(0) + ctrl.set_numeric(True) + ctrl.set_title(property[0]) + ctrl.set_value(component.values[property_idx]) + ctrl.connect("notify::value", self.on_apply_btn_clicked) + + elif property[1][0] == const.property_float: + floating_points = property[1][3] if property[1][3] < 3 else 2 + step = 10.0 ** -floating_points if floating_points > 0 else 1.0 + ctrl = Adw.SpinRow.new_with_range( + property[1][1], + property[1][2], + step + ) + + ctrl.set_digits(floating_points) + ctrl.set_numeric(True) + ctrl.set_title(property[0]) + ctrl.set_value(component.values[property_idx]) + ctrl.connect("notify::value", self.on_apply_btn_clicked) + + else: + ctrl = Adw.EntryRow( + title=property[0], + text=component.values[property_idx] + ) + + ctrl.set_width_chars(property[1][1]) + ctrl.connect("changed", self.on_apply_btn_clicked) + + else: + property_idx -= 1 + has_property = False + + if has_property: + if current_group is None: + current_group = Adw.PreferencesGroup.new() + groups.append(current_group) + group_counts[current_group] = 0 + + # self.prop_controls stays 1:1 and in-order with non-separator properties + self.prop_controls.append(ctrl) + current_group.add(ctrl) + group_counts[current_group] += 1 + else: + current_group = Adw.PreferencesGroup.new() + current_group.set_title(property[0]) + groups.append(current_group) + group_counts[current_group] = 0 + + property_idx += 1 + + preference_page = Adw.PreferencesPage.new() + + for group in groups: + if group_counts.get(group, 0) > 0: + preference_page.add(group) + + self.toolbar_view.set_content(preference_page) + + if parent is not None: + self.present(parent) + self._is_presented = True \ No newline at end of file diff --git a/ggate/Components/Windows/TimingGraph/Diagram.py b/ggate/Components/Windows/TimingGraph/Diagram.py new file mode 100644 index 0000000..3cd4f04 --- /dev/null +++ b/ggate/Components/Windows/TimingGraph/Diagram.py @@ -0,0 +1,339 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ggate.MainFrame import MainFrame + +import math +import cairo +from decimal import Decimal +from gettext import gettext as _ +from ggate import Preference +from ggate.Utils import cairo_draw_text, cairo_paths +from ggate.const import Definitions +from gi.repository import Gtk, Gdk, PangoCairo + +# Ruler strip height in pixels; each probe row is also _ROW_H px. +_ROW_H = 40 +# max chart width in px (Cairo surface limit) +_MAX_DIAGRAM_WIDTH = 32767 + +class TimingGraphDiagram(Gtk.Box): + def __init__(self, parent: MainFrame): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + # expand to fill all space given by the ToolbarView content area + self.set_hexpand(True) + self.set_vexpand(True) + + self._circuit = parent.circuit + self._parent_drawarea = parent.drawarea + + self.name_width = 150 + self.diagram_width = 1 + self.img_height = 1 + + self.start_time = 0.0 + self.end_time = 0.0002 + self.scale = 5000000.0 + self.timing_unit = 1 + self.gra_pix = 150 + + self.show_cursor = False + self.cursor_x = 0 + self.mouse_down = False + + self.name_area = Gtk.DrawingArea() + self.name_area.set_draw_func(self.name_area_draw_fn) + + self.chart_area = Gtk.DrawingArea() + self.chart_area.set_draw_func(self.chart_area_draw_fn) + self.chart_area.set_hexpand(True) # fills viewport width before scrolling kicks in + + self.name_scroll = Gtk.ScrolledWindow() + # EXTERNAL: no scrollbar but still scrolls via the shared vadjustment + self.name_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.EXTERNAL) + self.name_scroll.set_hexpand(False) + self.name_scroll.set_vexpand(True) + self.name_scroll.set_child(self.name_area) + + self.diagram_scroll = Gtk.ScrolledWindow() + self.diagram_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.diagram_scroll.set_hexpand(True) + self.diagram_scroll.set_vexpand(True) + self.diagram_scroll.set_child(self.chart_area) + + # share diagram's vertical adjustment so both columns scroll in sync + self.name_scroll.set_vadjustment(self.diagram_scroll.get_vadjustment()) + + self.append(self.name_scroll) + self.append(self.diagram_scroll) + + click_gesture = Gtk.GestureClick() + click_gesture.set_button(Gdk.BUTTON_PRIMARY) + click_gesture.connect("pressed", self.on_timing_diagram_click) + click_gesture.connect("released", self.on_timing_diagram_release) + self.chart_area.add_controller(click_gesture) + + motion_controller = Gtk.EventControllerMotion() + motion_controller.connect("motion", self.on_timing_diagram_motion) + motion_controller.connect("leave", self.on_timing_diagram_leave) + self.chart_area.add_controller(motion_controller) + + self.update_layout_sizes() + + def __get_probes(self): + return [ + cmp + for cmp in self._circuit.components + if cmp[0] == Definitions.component_probe + ] + + def update_layout_sizes(self): + n = len(self.__get_probes()) + self.img_height = _ROW_H + n * _ROW_H # ruler row + one row per probe + raw_width = int(max(1, (self.end_time - self.start_time) * self.scale)) + self.width_capped = raw_width > _MAX_DIAGRAM_WIDTH + self.diagram_width = min(raw_width, _MAX_DIAGRAM_WIDTH) + # set_size_request only ever called here — never inside a draw callback (avoids layout loop) + self.name_area.set_size_request(self.name_width, self.img_height) + self.chart_area.set_size_request(self.diagram_width, self.img_height) + + def draw(self): + self.update_layout_sizes() + self.name_area.queue_draw() + self.chart_area.queue_draw() + + # ── draw callbacks ──────────────────────────────────────────────────────── + + def name_area_draw_fn(self, drawing_area, cr: cairo.Context, width, height, *args): + self.draw_names(cr, height) + + def chart_area_draw_fn(self, drawing_area, cr: cairo.Context, width, height, *args): + # snapshot so iterating is safe while the worker thread appends frames + history = list(self._circuit.probe_levels_history) + if not history: + cr.set_source(Preference.bg_color_running) + cr.rectangle(0, 0, width, height) + cr.fill() + layout = PangoCairo.create_layout(cr) + layout.set_font_description(Preference.drawing_font) + msg = _("No simulation steps recorded. Operate inputs to advance simulation.") + cr.set_source(Preference.component_color_running) + cairo_draw_text(cr, layout, msg, width / 2, height / 2, 0.5, 0.5) + cr.fill() + return + + self.draw_diagrams(cr, width, height) + + current_time_x = (self._circuit.current_time - self.start_time) * self.scale + if 0 <= current_time_x <= self.diagram_width: + cr.set_source(Preference.terminal_color) + cr.set_line_width(1.0) + cairo_paths(cr, (current_time_x + 0.5, _ROW_H), (current_time_x + 0.5, self.img_height)) # +0.5 aligns 1px stroke to pixel grid + cr.stroke() + + if self.show_cursor and 0 <= self.cursor_x <= self.diagram_width: + cr.set_source(Preference.cursor_color) + cr.set_line_width(1.0) + cairo_paths(cr, (self.cursor_x + 0.5, _ROW_H), (self.cursor_x + 0.5, self.img_height)) + cr.stroke() + + # ── exporter-compatible public draw methods ─────────────────────────────── + + def draw_names(self, cr: cairo.Context, allocated_height: int = None): + h = allocated_height if allocated_height is not None else self.img_height + probes = self.__get_probes() + name_layout = PangoCairo.create_layout(cr) + name_layout.set_font_description(Preference.drawing_font) + + # background + cr.set_source(Preference.bg_color_running) + cr.rectangle(0, 0, self.name_width, h) + cr.fill() + + # outer border + row separators + cr.set_source(Preference.grid_color) + cr.set_line_width(1.0) + cr.rectangle(0.5, 0.5, self.name_width - 1, self.img_height - 1) + cr.stroke() + + # ruler / header bottom divider + cairo_paths(cr, (0, _ROW_H - 0.5), (self.name_width, _ROW_H - 0.5)) + cr.stroke() + + # probe row separators + for j in range(len(probes)): + sep_y = _ROW_H + j * _ROW_H + _ROW_H - 0.5 + cairo_paths(cr, (0, sep_y), (self.name_width, sep_y)) + cr.stroke() + + # header label ("Time [µs]") right-aligned into the ruler strip + unit_str = ["ns", "µs", "ms"][self.timing_unit] + cr.set_source(Preference.component_color_running) + time_label = _("Time") + cairo_draw_text(cr, name_layout, f"{time_label} [{unit_str}]", self.name_width - 6, _ROW_H // 2, 1.0, 0.5) + cr.fill() + + # probe name labels, vertically centred in each row + for j, probe in enumerate(probes): + row_mid = _ROW_H + j * _ROW_H + _ROW_H // 2 + cairo_draw_text(cr, name_layout, probe[1].values[0], 8, row_mid, 0, 0.5) + cr.fill() + + def draw_diagrams(self, cr: cairo.Context, allocated_width: int = None, allocated_height: int = None): + w = max(self.diagram_width, allocated_width) if allocated_width is not None else self.diagram_width + h = allocated_height if allocated_height is not None else self.img_height + probes = self.__get_probes() + diagram_layout = PangoCairo.create_layout(cr) + diagram_layout.set_font_description(Preference.drawing_font) + + # background + cr.set_source(Preference.bg_color_running) + cr.rectangle(0, 0, w, h) + cr.fill() + + # outer border + cr.set_source(Preference.grid_color) + cr.set_line_width(1.0) + cairo_paths(cr, (0, 0.5), (w, 0.5)) + cairo_paths(cr, (w - 0.5, 0), (w - 0.5, self.img_height)) + cairo_paths(cr, (0, self.img_height - 0.5), (w, self.img_height - 0.5)) + cr.stroke() + + # ruler bottom divider + row separators + cairo_paths(cr, (0, _ROW_H - 0.5), (w, _ROW_H - 0.5)) + for j in range(len(probes)): + sep_y = _ROW_H + j * _ROW_H + _ROW_H - 0.5 + cairo_paths(cr, (0, sep_y), (w, sep_y)) + cr.stroke() + + history = list(self._circuit.probe_levels_history) + if not history: + return + + self._draw_graduations(cr, diagram_layout) + self._draw_waveforms(cr, history, probes) + + def _draw_graduations(self, cr: cairo.Context, layout): + if self.scale <= 0: + return + gra_p = Decimal(self.gra_pix) / Decimal(self.scale) + if gra_p <= 0: + return + order = 10 ** Decimal(int(math.log10(gra_p))) + grasec_top = gra_p / order + # round to nearest "nice" step: 0.1, 0.2, or 0.5 × the leading power-of-10 + if 0.15 <= grasec_top < 0.35: + gra_p = order * Decimal("0.2") + elif 0.35 <= grasec_top <= 0.75: + gra_p = order * Decimal("0.5") + else: + gra_p = order * Decimal("0.1") + + unit_multipliers = {0: 1000000000.0, 1: 1000000.0, 2: 1000.0} + mult = unit_multipliers.get(self.timing_unit, 1000000.0) + u_start = self.start_time * mult + u_end = self.end_time * mult + u_gra_p = float(gra_p) * mult + + ruler_mid = _ROW_H // 2 + + # start / end boundary labels + cr.set_source(Preference.component_color_running) + cairo_draw_text(cr, layout, str(Decimal(u_start).quantize(Decimal(".00"))), 4, ruler_mid, 0.0, 0.5) + cairo_draw_text(cr, layout, str(Decimal(u_end).quantize(Decimal(".00"))), self.diagram_width - 4, ruler_mid, 1.0, 0.5) + cr.fill() + + u_gra_sec = Decimal(u_gra_p) + gra_sec = Decimal(gra_p) + u_gra_p_dec = Decimal(u_gra_p) + u_end_dec = Decimal(u_end) + + while True: + if u_gra_sec + u_gra_p_dec / 4 >= u_end_dec: + break + x = int(float(gra_sec) * self.scale) + 0.5 + cr.set_source(Preference.grid_color) + cairo_paths(cr, (x, _ROW_H), (x, self.img_height - 1)) + cr.stroke() + cr.set_source(Preference.component_color_running) + cairo_draw_text(cr, layout, str(u_gra_sec.quantize(Decimal(".00"))), float(gra_sec) * self.scale, ruler_mid, 0.5, 0.5) + cr.fill() + u_gra_sec += u_gra_p_dec + gra_sec += gra_p + + def _draw_waveforms(self, cr: cairo.Context, history, probes): + cr.set_line_width(1.5) + + def _row_y(j, high): + # each row spans _ROW_H px starting at _ROW_H offset; high at +6, low at +34 + base = _ROW_H + j * _ROW_H + return (base + 6 + 0.5) if high else (base + 34 + 0.5) # +0.5 aligns 1px stroke to pixel grid + + def _draw_segment(j, x0, x1, data, next_data=None): + if data == -1: + return # open-circuit state: leave a gap, don't draw + high = data is True or data == 1 + cr.set_source(Preference.highlevel_color if high else Preference.lowlevel_color) + y = _row_y(j, high) + cairo_paths(cr, (x0, y), (x1, y)) + cr.stroke() + if next_data is not None and next_data != -1 and data != next_data: + # vertical transition line between high and low + cr.set_source(Preference.highlevel_color if high else Preference.lowlevel_color) + tx = x1 - 0.5 # +0.5 aligns 1px stroke to pixel grid + base = _ROW_H + j * _ROW_H + cairo_paths(cr, (tx, base + 7), (tx, base + 33)) + cr.stroke() + + if len(history) > 1: + for i, frame in enumerate(history[:-1]): + t0, t1 = frame[0], history[i + 1][0] + if t1 <= self.start_time or self.end_time < t0: + continue + x0 = round((t0 - self.start_time) * self.scale) + x1 = round((t1 - self.start_time) * self.scale) + 1 + for j, data in enumerate(frame[1:]): + _draw_segment(j, x0, x1, data, history[i + 1][j + 1]) + + last = history[-1] + if last[0] < self.end_time: + x0 = round((last[0] - self.start_time) * self.scale) + for j, data in enumerate(last[1:]): + _draw_segment(j, x0, self.diagram_width, data) + else: + last = history[0] + x0 = round((last[0] - self.start_time) * self.scale) + for j, data in enumerate(last[1:]): + _draw_segment(j, x0, self.diagram_width, data) + + # ── time scrubbing ──────────────────────────────────────────────────────── + + def update_current_time(self, x): + if self.scale <= 0: + return + selected_time = max(self.start_time, min(x / self.scale + self.start_time, self.end_time)) + self._circuit.current_time = selected_time + self._circuit.revert_state() + self.chart_area.queue_draw() + if self._parent_drawarea: + self._parent_drawarea.queue_draw() + + def on_timing_diagram_click(self, gesture, n_press, x, y, *args): + self.mouse_down = True + self.update_current_time(x) + + def on_timing_diagram_release(self, gesture, n_press, x, y, *args): + self.mouse_down = False + self.chart_area.queue_draw() + + def on_timing_diagram_motion(self, controller, x, y, *args): + self.cursor_x = x + self.show_cursor = True + if self.mouse_down: + self.update_current_time(x) + self.chart_area.queue_draw() + + def on_timing_diagram_leave(self, controller, *args): + self.show_cursor = False + self.chart_area.queue_draw() diff --git a/ggate/Components/Windows/TimingGraph/Display.py b/ggate/Components/Windows/TimingGraph/Display.py new file mode 100644 index 0000000..a8ddd41 --- /dev/null +++ b/ggate/Components/Windows/TimingGraph/Display.py @@ -0,0 +1,152 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ggate.MainFrame import MainFrame + +from gi.repository import Gtk, Adw +from gettext import gettext as _ +from ggate.Components.Windows.TimingGraph.Diagram import TimingGraphDiagram +from ggate import Preference + +class TimingGraphDisplayWindow(Adw.Dialog): + def __init__(self, parent: MainFrame): + Adw.Dialog.__init__(self, can_close=True) + self._parent = parent + self._is_presented = False + + self.set_title(_("Timing Graph")) + self.set_content_width(650) + self.set_content_height(450) + + toolbar_view = Adw.ToolbarView() + + header_bar = Adw.HeaderBar() + toolbar_view.add_top_bar(header_bar) + + sub_toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + sub_toolbar.set_margin_start(12) + sub_toolbar.set_margin_end(12) + sub_toolbar.set_margin_top(6) + sub_toolbar.set_margin_bottom(6) + + scale_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + scale_box.append(Gtk.Label(label=_("Scale:"))) + + self.scale_spin = Gtk.SpinButton.new_with_range(0.1, 1000.0, 0.5) + self.scale_spin.set_value(5.0) + self.scale_spin.set_digits(1) + self.scale_spin.connect("value-changed", self.on_parameters_changed) + scale_box.append(self.scale_spin) + + self.scale_unit = Gtk.DropDown.new_from_strings(["ns", "µs", "ms"]) + self.scale_unit.set_selected(1) + self.scale_unit.connect("notify::selected", self.on_parameters_changed) + scale_box.append(self.scale_unit) + + sub_toolbar.append(scale_box) + + spacer = Gtk.Box(hexpand=True) + sub_toolbar.append(spacer) + + range_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + range_box.append(Gtk.Label(label=_("Range From:"))) + + self.from_spin = Gtk.SpinButton.new_with_range(0.0, 100000.0, 1.0) + self.from_spin.set_value(0.0) + self.from_spin.set_digits(2) + self.from_spin.connect("value-changed", self.on_parameters_changed) + range_box.append(self.from_spin) + + range_box.append(Gtk.Label(label=_("To:"))) + + self.to_spin = Gtk.SpinButton.new_with_range(0.0, 100000.0, 10.0) + max_dur = getattr(Preference, "max_calc_duration", 0.0002) + self.to_spin.set_value(max_dur * 1e6) + self.to_spin.set_digits(2) + self.to_spin.connect("value-changed", self.on_parameters_changed) + range_box.append(self.to_spin) + + self.range_unit = Gtk.DropDown.new_from_strings(["ns", "µs", "ms"]) + self.range_unit.set_selected(1) + self.range_unit.connect("notify::selected", self.on_parameters_changed) + range_box.append(self.range_unit) + + sub_toolbar.append(range_box) + + save_button = Gtk.Button(label=_("Save Image")) + save_button.connect("clicked", self.on_save_clicked) + sub_toolbar.append(save_button) + + toolbar_view.add_top_bar(sub_toolbar) + + self._draw_area = TimingGraphDiagram(parent) + toolbar_view.set_content(self._draw_area) + + self.set_child(toolbar_view) + self.connect("closed", self.on_closed) + + def on_parameters_changed(self, *args): + if getattr(self, "_blocking_updates", False): + return + # spin value is in display-unit (ns/μs/ms); divide by seconds-per-unit to get px/s + multipliers = {0: 1e-9, 1: 1e-6, 2: 1e-3} + scale_val = self.scale_spin.get_value() + scale_mult = multipliers.get(self.scale_unit.get_selected(), 1e-6) + + self._draw_area.scale = scale_val / scale_mult + + range_mult = multipliers.get(self.range_unit.get_selected(), 1e-6) + self._draw_area.start_time = self.from_spin.get_value() * range_mult + self._draw_area.end_time = self.to_spin.get_value() * range_mult + self._draw_area.timing_unit = self.range_unit.get_selected() + + self._draw_area.draw() + + def on_save_clicked(self, button): + from ggate.Exporter import save_timing_diagram_as_image + save_timing_diagram_as_image(self._draw_area, self._parent) + + def on_closed(self, dialog): + self._is_presented = False + + def display(self): + self._is_presented = True + + history = self._parent.circuit.probe_levels_history + if not history or len(history) <= 1: + self.on_parameters_changed() + self.present(self._parent) + return + + start_t = history[0][0] + end_t = history[-1][0] + duration = end_t - start_t + if duration <= 1e-12: + self.on_parameters_changed() + self.present(self._parent) + return + + self._blocking_updates = True + + multipliers = {0: 1e9, 1: 1e6, 2: 1e3} + selected_unit = self.range_unit.get_selected() + mult = multipliers.get(selected_unit, 1e6) + + self.from_spin.set_value(start_t * mult) + self.to_spin.set_value(end_t * mult) + + # Saner default zoom: scale so that the duration fits ~500 pixels wide + scale_unit_mult = multipliers.get(self.scale_unit.get_selected(), 1e6) + ideal_scale_val = (500.0 / duration) / scale_unit_mult + clamped_scale_val = max(0.1, min(ideal_scale_val, 1000.0)) + self.scale_spin.set_value(clamped_scale_val) + + self._blocking_updates = False + + self.on_parameters_changed() + self.present(self._parent) + + # Adw.Dialog has no usable get_visible(); return our own tracking flag instead + def get_visible(self) -> bool: + return self._is_presented \ No newline at end of file diff --git a/ggate/Components/__init__.py b/ggate/Components/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ggate/DiagramArea.py b/ggate/DiagramArea.py deleted file mode 100644 index ffe4585..0000000 --- a/ggate/DiagramArea.py +++ /dev/null @@ -1,410 +0,0 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- - -import cairo -import math -from decimal import Decimal -from ggate.const import definitions as const -from ggate.Utils import * -from ggate import Preference -from gi.repository import Gtk, Gdk, PangoCairo -from gettext import gettext as _ - - -class DiagramArea(Gtk.Box): - def __init__(self, circuit, drawarea): - Gtk.Box.__init__(self) - - self.circuit = circuit - self.drawarea = drawarea - self.scale = 5000000 # pix/sec - self.start_time = 0.0 - self.end_time = 0.0002 - self.diagram_unit = 1 # 0: ns, 1: µs, 2: ms - - self.gra_pix = 120 - - self.name_width = 150 - self.img_height = 0 - - self.diagram_scroll = Gtk.ScrolledWindow() - self.diagram_scroll.set_hexpand(True) - self.diagram_scroll.set_vexpand(True) - self.diagram_area = Gtk.DrawingArea() - self.diagram_scroll.set_child(self.diagram_area) - self.diagram_scroll_hadj = self.diagram_scroll.get_hadjustment() - self.diagram_scroll_vadj = self.diagram_scroll.get_vadjustment() - - self.name_scroll = Gtk.ScrolledWindow( - vadjustment=self.diagram_scroll.get_vadjustment()) - self.name_scroll.set_size_request(150, -1) - self.namearea = Gtk.DrawingArea() - self.name_scroll.set_child(self.namearea) - self.name_scroll_hadj = self.name_scroll.get_hadjustment() - self.name_scroll_vadj = self.name_scroll.get_vadjustment() - - self.append(self.name_scroll) - self.append(self.diagram_scroll) - - self.cursor_x = 0 - self.cursor_y = 0 - self.show_cursor = False - self.mouse_down = False - self.middle_move_enabled = False - - self.name_pixbuf = None - self.diagram_pixbuf = None - - self.namearea.set_draw_func(self.on_namearea_draw) - self.diagram_area.set_draw_func(self.on_diagram_area_draw) - - controller = Gtk.EventControllerMotion() - controller.connect('leave', self.on_diagram_area_leave) - self.diagram_area.add_controller(controller) - - controller = Gtk.EventControllerMotion() - controller.connect('motion', self.on_diagram_area_motion) - self.diagram_area.add_controller(controller) - - self.diagram_area.set_draw_func(self.on_diagram_area_draw) - - controller = Gtk.GestureClick() - # Primary Button - controller.set_button(Gdk.BUTTON_PRIMARY) - controller.connect('pressed', self.on_diagram_area_button_press_primary) - self.diagram_area.add_controller(controller) - - controller = Gtk.GestureClick() - # Secondary click (right click) - controller.set_button(Gdk.BUTTON_SECONDARY) - controller.connect('pressed', self.on_diagram_area_button_press_secondary) - self.diagram_area.add_controller(controller) - - controller = Gtk.GestureClick() - # Left click release - controller.set_button(Gdk.BUTTON_PRIMARY) - controller.connect('released', self.on_diagram_area_button_release_primary) - self.diagram_area.add_controller(controller) - - controller = Gtk.GestureClick() - # Right click release - controller.set_button(Gdk.BUTTON_SECONDARY) - controller.connect('released', self.on_diagram_area_button_release_secondary) - self.diagram_area.add_controller(controller) - - controller = Gtk.GestureClick() - controller.set_button(Gdk.BUTTON_SECONDARY) - controller.connect('pressed', self.on_namearea_button_press) - self.namearea.add_controller(controller) - - controller = Gtk.GestureClick() - controller.set_button(Gdk.BUTTON_SECONDARY) - controller.connect('released', self.on_namearea_button_release) - self.namearea.add_controller(controller) - - controller = Gtk.EventControllerMotion() - controller.connect('motion', self.on_namearea_motion) - self.namearea.add_controller(controller) - - def on_namearea_button_press(self, widget, *event): - self.middle_move_enabled = True - self.move_start_x = event[1] - self.move_start_y = event[2] - - def on_namearea_button_release(self, widget, *event): - self.middle_move_enabled = False - - def on_namearea_motion(self, widget, *event): - - x_axis, y_axis = event[0], event[1] - - if self.middle_move_enabled: - delta_x = self.move_start_x - x_axis - delta_y = self.move_start_y - y_axis - if -1 < delta_x < 1 and -1 < delta_y < 1: - return - self.name_scroll_hadj.set_value( - self.name_scroll_hadj.get_value() + delta_x) - self.name_scroll_vadj.set_value( - self.name_scroll_vadj.get_value() + delta_y) - - def on_diagram_area_button_press_primary(self, widget, *event): - event_button, x_axis, y_axis = event - - if event_button == Gdk.BUTTON_PRIMARY: - self.cursor_x = x_axis - - if self.cursor_x < 0.0: - self.cursor_x = 0.0 - if self.diagram_width < self.cursor_x: - self.cursor_x = self.diagram_width - self.cursor_y = y_axis - if 39 <= self.cursor_y < self.img_height: - self.mouse_down = True - self.queue_draw() - - def on_diagram_area_button_press_secondary(self, controller, *events): - event_button, x_axis, y_axis = events - if event_button == Gdk.BUTTON_SECONDARY: - self.middle_move_enabled = True - self.move_start_x = x_axis - self.move_start_y = y_axis - - def on_diagram_area_button_release_primary(self, widget, *event): - - if event[0] == Gdk.BUTTON_PRIMARY: - self.mouse_down = False - if 39 <= self.cursor_y < self.img_height: - self.circuit.current_time = self.cursor_x / self.scale + self.start_time - self.circuit.revert_state() - self.drawarea.redraw = True - self.drawarea.queue_draw() - - def on_diagram_area_button_release_secondary(self, controller, *event): - self.middle_move_enabled = False - - def on_diagram_area_motion(self, controller, x_axis, y_axis, *args): - widget = controller.get_widget() - - if self.middle_move_enabled: - delta_x = self.move_start_x - x_axis - delta_y = self.move_start_y - y_axis - if -1 < delta_x < 1 and -1 < delta_y < 1: - return - self.diagram_scroll_hadj.set_value( - self.diagram_scroll_hadj.get_value() + delta_x) - self.diagram_scroll_vadj.set_value( - self.diagram_scroll_vadj.get_value() + delta_y) - - self.cursor_x_old = self.cursor_x - self.cursor_y_old = self.cursor_y - self.cursor_x = x_axis - try: - self.diagram_width - except: - return - if self.cursor_x < 0.0: - self.cursor_x = 0.0 - if self.diagram_width < self.cursor_x: - self.cursor_x = self.diagram_width - self.cursor_y = y_axis - if 39 <= self.cursor_y < self.img_height: - int(min((self.cursor_x_old, self.cursor_x)) - 1) - int(abs(self.cursor_x_old - self.cursor_x) + 4) - if self.show_cursor: - widget.queue_draw() # _area(left, 0, width, self.img_height) - else: - widget.queue_draw() - self.show_cursor = True - elif self.show_cursor: - self.show_cursor = False - widget.queue_draw() - - def set_time(self, time): - old_x = (self.circuit.current_time - self.start_time) * self.scale - self.circuit.current_time = time - new_x = (self.circuit.current_time - self.start_time) * self.scale - - # TODO: Figure out what this is used for - int(min((old_x, new_x)) - 1) - int(abs(old_x - new_x) + 4) - self.diagram_area.queue_draw() - - def on_diagram_area_leave(self, controller, *event): - - widget = controller.get_widget() - - self.show_cursor = False - widget.queue_draw() #int(self.cursor_x - 1), 0, 3, self.img_height) - - def on_namearea_draw(self, widget, cr, *args): - if self.name_pixbuf is not None: - cr.set_source_surface(self.name_pixbuf, 0, 0) - cr.paint() - - def on_diagram_area_draw(self, widget, cr, *args): - if self.diagram_pixbuf is not None: - cr.set_source_surface(self.diagram_pixbuf, 0, 0) - cr.paint() - cr.set_line_width(1.0) - if self.show_cursor: - if self.mouse_down: - cr.set_source_rgba(0.0, 1.0, 0.0, 0.8) - else: - cr.set_source_rgba(0.0, 0.0, 1.0, 0.8) - cairo_paths(cr, (self.cursor_x, 39), - (self.cursor_x, self.img_height)) - cr.stroke() - if not self.mouse_down or not self.show_cursor: - cr.set_source_rgba(1.0, 0.0, 0.0, 0.8) - current_time_x = (self.circuit.current_time - - self.start_time) * self.scale - cairo_paths(cr, (current_time_x, 39), - (current_time_x, self.img_height)) - cr.stroke() - - def draw_names(self, cr): - name_layout = PangoCairo.create_layout(cr) - name_layout.set_font_description(Preference.drawing_font) - # draw borders - cr.set_source_rgb(1.0, 1.0, 1.0) - cr.rectangle(1, 1, self.name_width-1, 38) - cr.fill() - cr.set_source_rgb(0.875, 0.875, 0.875) - cr.rectangle(1, 40, self.name_width - 1, self.img_height - 41) - cr.fill() - cr.set_source_rgb(0.5, 0.5, 0.5) - cr.rectangle(0.5, 0.5, self.name_width - 1, self.img_height - 1) - i = 0 - for c in self.circuit.components: - if c[0] == const.component_probe: - cairo_paths(cr, (1, i * 40 + 39.5), - (self.name_width - 1, i * 40 + 39.5)) - i += 1 - cr.stroke() - - # draw probe names - cr.set_source_rgb(0.0, 0.0, 0.0) - i = 0 - for c in self.circuit.components: - if c[0] == const.component_probe: - cairo_draw_text(cr, name_layout, - c[1].values[0], 10, i * 40 + 60, 0, 0.5) - i += 1 - cr.fill() - - if self.diagram_unit == 0: - cairo_draw_text(cr, name_layout, "%s [ns]" % _( - "Time"), 130, 35, 1.0, 1.0) - elif self.diagram_unit == 1: - cairo_draw_text(cr, name_layout, "%s [µs]" % _( - "Time"), 130, 35, 1.0, 1.0) - else: - cairo_draw_text(cr, name_layout, "%s [ms]" % _( - "Time"), 130, 35, 1.0, 1.0) - - cr.fill() - - def draw_diagrams(self, cr): - diagram_layout = PangoCairo.create_layout(cr) - diagram_layout.set_font_description(Preference.drawing_font) - - # draw borders - cr.set_source_rgb(1.0, 1.0, 1.0) - cr.rectangle(0, 1, self.diagram_width - 1, self.img_height - 2) - cr.fill() - cr.set_source_rgb(0.5, 0.5, 0.5) - cairo_paths(cr, (0, 0.5), (self.diagram_width, 0.5)) - cairo_paths(cr, (self.diagram_width - 0.5, 1), - (self.diagram_width - 0.5, self.img_height - 1)) - cairo_paths(cr, (0, self.img_height - 0.5), - (self.diagram_width, self.img_height - 0.5)) - i = 0 - for c in self.circuit.components: - if c[0] == const.component_probe: - cairo_paths(cr, (0, i * 40 + 39.5), - (self.diagram_width - 1, i * 40 + 39.5)) - i += 1 - cr.stroke() - - # draw graduaton - gra_p = Decimal(self.gra_pix) / Decimal(self.scale) - order = 10 ** Decimal(int(math.log10(gra_p))) - grasec_top = gra_p / order - if 0.15 <= grasec_top < 0.35: - gra_p = order * Decimal("0.2") - elif 0.35 <= grasec_top <= 0.75: - gra_p = order * Decimal("0.5") - else: - gra_p = order * Decimal("0.1") - cr.set_source_rgb(0.0, 0.0, 0.0) - - if self.diagram_unit == 0: - u_start_time = self.start_time * 1000000000 - u_end_time = self.end_time * 1000000000 - u_gra_p = gra_p * 1000000000 - elif self.diagram_unit == 1: - u_start_time = self.start_time * 1000000 - u_end_time = self.end_time * 1000000 - u_gra_p = gra_p * 1000000 - else: - u_start_time = self.start_time * 1000 - u_end_time = self.end_time * 1000 - u_gra_p = gra_p * 1000 - - cairo_draw_text(cr, diagram_layout, str( - Decimal(u_start_time).quantize(Decimal(".00"))), 5, 35, 0.0, 1.0) - cairo_draw_text(cr, diagram_layout, str(Decimal(u_end_time).quantize( - Decimal(".00"))), self.diagram_width - 5, 35, 1.0, 1.0) - cr.fill() - - u_gra_sec = u_gra_p - gra_sec = gra_p - while True: - if u_gra_sec + u_gra_p / 4 >= u_end_time: - break - cr.set_source_rgb(0.5, 0.5, 0.5) - cairo_paths(cr, (int(float(gra_sec) * self.scale) + 0.5, 39), - (int(float(gra_sec) * self.scale) + 0.5, self.img_height - 1)) - cr.stroke() - cr.set_source_rgb(0.0, 0.0, 0.0) - cairo_draw_text(cr, diagram_layout, str(Decimal(u_gra_sec).quantize( - Decimal(".00"))), float(gra_sec) * self.scale, 35, 0.5, 1.0) - u_gra_sec += u_gra_p - gra_sec += gra_p - cr.fill() - - # plot graphs - history = self.circuit.probe_levels_history - for i, probe_lebels in enumerate(history[:-1]): - if history[i + 1][0] <= self.start_time or self.end_time < probe_lebels[0]: - continue - for j, data in enumerate(probe_lebels[1:]): - if data is True: - cairo_paths(cr, (round((probe_lebels[0] - self.start_time) * self.scale), j * 40 + 55.5), (round( - (history[i + 1][0] - self.start_time) * self.scale) + 1, j * 40 + 55.5)) - else: - cairo_paths(cr, (round((probe_lebels[0] - self.start_time) * self.scale), j * 40 + 65.5), (round( - (history[i + 1][0] - self.start_time) * self.scale) + 1, j * 40 + 65.5)) - if data != history[i + 1][j + 1]: - cairo_paths(cr, (round((history[i + 1][0] - self.start_time) * self.scale) + 0.5, j * 40 + 56), (round( - (history[i + 1][0] - self.start_time) * self.scale) + 0.5, j * 40 + 65)) - - if history[-1][0] < self.end_time: - for j, data in enumerate(history[-1][1:]): - if data is True: - cairo_paths(cr, (round( - (history[-1][0] - self.start_time) * self.scale), j * 40 + 55.5), (self.diagram_width, j * 40 + 55.5)) - else: - cairo_paths(cr, (round( - (history[-1][0] - self.start_time) * self.scale), j * 40 + 65.5), (self.diagram_width, j * 40 + 65.5)) - - cr.stroke() - - def createDiagram(self): - i = 0 - for c in self.circuit.components: - if c[0] == const.component_probe: - i += 1 - self.img_height = 40 * i + 40 - - self.diagram_width = int( - (self.end_time - self.start_time) * self.scale) - if self.diagram_width == 0: - self.diagram_width = 1 - - self.name_pixbuf = cairo.ImageSurface( - cairo.FORMAT_RGB24, self.name_width, self.img_height) - self.namearea.set_size_request(self.name_width, self.img_height) - ncr = cairo.Context(self.name_pixbuf) - ncr.set_line_width(1.0) - self.draw_names(ncr) - - self.diagram_pixbuf = cairo.ImageSurface( - cairo.FORMAT_RGB24, self.diagram_width, self.img_height) - self.diagram_area.set_size_request(self.diagram_width, self.img_height) - ccr = cairo.Context(self.diagram_pixbuf) - ccr.set_line_width(1.0) - self.draw_diagrams(ccr) - self.queue_draw() - - \ No newline at end of file diff --git a/ggate/DrawArea.py b/ggate/DrawArea.py index 88630f2..39cb6a1 100644 --- a/ggate/DrawArea.py +++ b/ggate/DrawArea.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- from __future__ import annotations from typing import TYPE_CHECKING @@ -14,25 +13,46 @@ from ggate.Utils import cairo_paths, inv_matrix, multiply_matrix, create_component_matrix, get_components_rect, draw_rounded_rectangle, clamp from ggate.Components.LogicGates import logic_gates from ggate import Preference -from gi.repository import Gtk, Gdk, Pango, PangoCairo +from gi.repository import Gtk, Gdk, Pango, PangoCairo, GLib +from ggate.Animation import CanvasAnimationController +from ggate.PlaybackControls import PlaybackControls -class DrawArea(Gtk.ScrolledWindow): +class DrawArea(Gtk.Overlay): def __init__(self, parent: MainFrame): - Gtk.ScrolledWindow.__init__(self) + Gtk.Overlay.__init__(self) self.set_hexpand(True) self.set_vexpand(True) + + self.scroller = Gtk.ScrolledWindow() + self.scroller.set_hexpand(True) + self.scroller.set_vexpand(True) + self.width = 1920 self.height = 1080 - self.vadj = self.get_vadjustment() - self.hadj = self.get_hadjustment() + self.zoom = 1.0 + self.vadj = self.scroller.get_vadjustment() + self.hadj = self.scroller.get_hadjustment() self.netstarted = False self.drawingarea = Gtk.DrawingArea() self.drawingarea.set_size_request(self.width, self.height) - self.set_child(self.drawingarea) + self.scroller.set_child(self.drawingarea) + self.set_child(self.scroller) self.drawingarea.set_draw_func(self.on_draw) + self.playback_controls = PlaybackControls(parent) + self.add_overlay(self.playback_controls) + + scroll_controller = Gtk.EventControllerScroll.new(Gtk.EventControllerScrollFlags.BOTH_AXES) + scroll_controller.connect("scroll", self.on_scroll) + self.drawingarea.add_controller(scroll_controller) + + zoom_gesture = Gtk.GestureZoom() + zoom_gesture.connect("begin", self.on_zoom_begin) + zoom_gesture.connect("scale-changed", self.on_zoom_scale_changed) + self.drawingarea.add_controller(zoom_gesture) + self.vadj.set_value(self.height / 2) self.hadj.set_value(self.width / 2) @@ -110,16 +130,122 @@ def __init__(self, parent: MainFrame): self.rect_select_enabled = False self.middle_move_enabled = False self.mouse_down = False - self.mbitmap = None self.circuit: CircuitManager = None self.redraw = True self.netstarted = False - self.mpixbuf = cairo.ImageSurface(cairo.FORMAT_RGB24, self.width, self.height) self._pasted_components = None self._pushed_component_name = const.component_none self._pushed_component = logic_gates[const.component_none] + self._last_pw = 0 + self._last_ph = 0 + self._initial_centering_done = False + self.hadj.connect("changed", self._on_adjustment_changed) + self.vadj.connect("changed", self._on_adjustment_changed) + self.glide_duration_ms = 150 + self.animation_controller = CanvasAnimationController(self) + self.drag_initial_offsets = {} + + def reset_initial_centering(self): + self._initial_centering_done = False + + def _on_adjustment_changed(self, adj): + pw = self.hadj.get_page_size() + ph = self.vadj.get_page_size() + if pw > 0 and ph > 0: + if pw != self._last_pw or ph != self._last_ph: + self._last_pw = pw + self._last_ph = ph + if not self._initial_centering_done or Preference.autocenter_resize: + self.center_viewport() + + def center_viewport(self): + pw = self.hadj.get_page_size() + ph = self.vadj.get_page_size() + if pw <= 0 or ph <= 0: + return False + if self.circuit and self.circuit.components: + rect = get_components_rect(self.circuit.components) + c_x = ((rect[0] + rect[2]) / 2) * self.zoom + c_y = ((rect[1] + rect[3]) / 2) * self.zoom + else: + c_x = (self.width / 2) * self.zoom + c_y = (self.height / 2) * self.zoom + self.hadj.set_value(c_x - pw / 2) + self.vadj.set_value(c_y - ph / 2) + self._initial_centering_done = True + return False + + + def update_canvas_size(self): + min_width = 1920 + min_height = 1080 + margin = 300 + if self.circuit and self.circuit.components: + rect = get_components_rect(self.circuit.components) + if rect: + req_width = max(min_width, int(rect[2] + margin)) + req_height = max(min_height, int(rect[3] + margin)) + else: + req_width = min_width + req_height = min_height + else: + req_width = min_width + req_height = min_height + req_width = min(req_width, 32767) + req_height = min(req_height, 32767) + if req_width != self.width or req_height != self.height: + self.width = req_width + self.height = req_height + self.drawingarea.set_size_request(int(self.width * self.zoom), int(self.height * self.zoom)) + + def on_scroll(self, controller, dx, dy): + state = controller.get_current_event_state() + if state & Gdk.ModifierType.CONTROL_MASK: + zoom_step = 1.1 + if dy < 0: + new_zoom = self.zoom * zoom_step + elif dy > 0: + new_zoom = self.zoom / zoom_step + else: + return False + new_zoom = clamp(new_zoom, 0.5, 5.0) + if new_zoom != self.zoom: + old_zoom = self.zoom + cx = self.cursor_smooth_x + cy = self.cursor_smooth_y + self.zoom = new_zoom + self.drawingarea.set_size_request(int(self.width * self.zoom), int(self.height * self.zoom)) + new_hadj = self.hadj.get_value() + cx * (new_zoom - old_zoom) + new_vadj = self.vadj.get_value() + cy * (new_zoom - old_zoom) + self.hadj.set_value(new_hadj) + self.vadj.set_value(new_vadj) + self.queue_draw() + return True + return False + + def on_zoom_begin(self, gesture, sequence): + self.zoom_start_factor = self.zoom + + def on_zoom_scale_changed(self, gesture, scale): + if not hasattr(self, "zoom_start_factor"): + self.zoom_start_factor = self.zoom + new_zoom = clamp(self.zoom_start_factor * scale, 0.5, 20.0) + if new_zoom != self.zoom: + success, gx, gy = gesture.get_bounding_box_center() + if success: + old_zoom = self.zoom + cx = gx / old_zoom + cy = gy / old_zoom + self.zoom = new_zoom + self.drawingarea.set_size_request(int(self.width * self.zoom), int(self.height * self.zoom)) + new_hadj = self.hadj.get_value() + cx * (new_zoom - old_zoom) + new_vadj = self.vadj.get_value() + cy * (new_zoom - old_zoom) + self.hadj.set_value(new_hadj) + self.vadj.set_value(new_vadj) + self.queue_draw() def queue_draw(self, *args): + self.update_canvas_size() self.show() self.drawingarea.queue_draw() super().queue_draw(*args) @@ -141,10 +267,33 @@ def menu_activated(self, widget, action_name, *args): elif action == 'flip_verti': self.parent.on_action_flip_vertically() - elif action == 'properties': + elif action == "properties": self.set_selected_component_to_prop_window() - self.parent.prop_window.present() - + + def clear_animations(self): + self.animation_controller.clear() + self.drag_initial_offsets.clear() + + def get_component_visual_offset(self, cmp) -> tuple[float, float]: + if self.parent.running_mode: + return 0.0, 0.0 + + if cmp[0] == const.component_net: + if self.component_dragged and cmp in self.circuit.selected_components: + dx = self.cursor_smooth_x - self.select_start_x - self.drag_delta_x + dy = self.cursor_smooth_y - self.select_start_y - self.drag_delta_y + return dx, dy + return 0.0, 0.0 + + comp_inst = cmp[1] + if self.component_dragged and cmp in self.circuit.selected_components: + init_x, init_y = self.drag_initial_offsets.get(comp_inst, (0.0, 0.0)) + dx = init_x + (self.cursor_smooth_x - self.select_start_x - self.drag_delta_x) + dy = init_y + (self.cursor_smooth_y - self.select_start_y - self.drag_delta_y) + return dx, dy + + return self.animation_controller.get_visual_offset(comp_inst) + def draw_net(self, net, mcr: cairo.Context): """ This method draws all states of the net. @@ -164,14 +313,18 @@ def draw_net(self, net, mcr: cairo.Context): is_hovered = net == self.nearest_component and self.cursor_over \ and self._pushed_component_name == const.component_none + offset_x, offset_y = self.get_component_visual_offset(net) + x1, y1 = net[1] + offset_x, net[2] + offset_y + x2, y2 = net[3] + offset_x, net[4] + offset_y + # Get net level if self.parent.running_mode: # Draw net terminal mcr.set_source(Preference.terminal_color_running) if (net[1], net[2]) not in self.circuit.net_no_dot: - mcr.rectangle(net[1]-1.5, net[2]-1.5, 3, 3) + mcr.rectangle(x1-1.5, y1-1.5, 3, 3) elif (net[3], net[4]) not in self.circuit.net_no_dot: - mcr.rectangle(net[3]-1.5, net[4]-1.5, 3, 3) + mcr.rectangle(x2-1.5, y2-1.5, 3, 3) mcr.fill() # Get the color of the net @@ -187,7 +340,7 @@ def draw_net(self, net, mcr: cairo.Context): # Draw net mcr.set_source(net_level_color) - cairo_paths(mcr, (net[1], net[2]), (net[3], net[4])) + cairo_paths(mcr, (x1, y1), (x2, y2)) mcr.stroke() else: @@ -199,13 +352,13 @@ def draw_net(self, net, mcr: cairo.Context): mcr.set_source(Preference.net_high_color) # Draw net - cairo_paths(mcr, (net[1], net[2]), (net[3], net[4])) + cairo_paths(mcr, (x1, y1), (x2, y2)) mcr.stroke() # Draw net terminal mcr.set_source(Preference.terminal_color) - mcr.rectangle(net[1]-1.5, net[2]-1.5, 3, 3) - mcr.rectangle(net[3]-1.5, net[4]-1.5, 3, 3) + mcr.rectangle(x1-1.5, y1-1.5, 3, 3) + mcr.rectangle(x2-1.5, y2-1.5, 3, 3) mcr.fill() @@ -229,14 +382,16 @@ def draw_component(self, cmp, mcr: cairo.Context, layout: Pango.Layout): else: mcr.set_source(Preference.terminal_color) + offset_x, offset_y = self.get_component_visual_offset(cmp) + # Draw component terminal if not self.parent.running_mode: for p in cmp[1].rot_input_pins + cmp[1].rot_output_pins: - mcr.rectangle(cmp[1].pos_x+p[0]-1.5, cmp[1].pos_y+p[1]-1.5, 3, 3) + mcr.rectangle(cmp[1].pos_x + offset_x + p[0] - 1.5, cmp[1].pos_y + offset_y + p[1] - 1.5, 3, 3) mcr.fill() # Draw component - mcr.translate(cmp[1].pos_x, cmp[1].pos_y) + mcr.translate(cmp[1].pos_x + offset_x, cmp[1].pos_y + offset_y) cmp_matrix = create_component_matrix(cmp) mcr.set_matrix(cmp_matrix.multiply(mcr.get_matrix())) @@ -320,31 +475,34 @@ def draw_pushed_component(self, where, cmp, cr: cairo.Context, layout: Pango.Lay cr.set_matrix(matrix) def on_draw(self, widget, cr: cairo.Context, width, height, *args): - # Rerender whole screen - if self.redraw: - mcr = cairo.Context(self.mpixbuf) - if self.parent.running_mode: - mcr.set_source(Preference.bg_color_running) - else: - mcr.set_source(Preference.bg_color) - - # Setup the background color - mcr.rectangle(0, 0, self.width, self.height) - mcr.fill() + if self.parent.running_mode: + cr.set_source(Preference.bg_color_running) + else: + cr.set_source(Preference.bg_color) + cr.rectangle(0, 0, width, height) + cr.fill() - # Draw grids - if not self.parent.running_mode: - mcr.set_source(Preference.grid_color) - for x in range(0, self.width, self.grid_step): - cairo_paths(mcr, (x, 0), (x, self.height)) - for y in range(0, self.height, self.grid_step): - cairo_paths(mcr, (0, y), (self.width, y)) - mcr.stroke() + cr.scale(self.zoom, self.zoom) - self.redraw = False + if not self.parent.running_mode: + cr.set_source(Preference.grid_color) + x1, y1, x2, y2 = cr.clip_extents() + if math.isfinite(x1) and math.isfinite(x2) and math.isfinite(y1) and math.isfinite(y2): + effective_step = self.grid_step + while (x2 - x1) / effective_step > 300 or (y2 - y1) / effective_step > 300: + effective_step *= 2 + + start_x = int(x1 - (x1 % effective_step)) + end_x = int(x2) + for x in range(start_x, end_x + effective_step, effective_step): + cairo_paths(cr, (x, y1), (x, y2)) + start_y = int(y1 - (y1 % effective_step)) + end_y = int(y2) + for y in range(start_y, end_y + effective_step, effective_step): + cairo_paths(cr, (x1, y), (x2, y)) + cr.stroke() - cr.set_source_surface(self.mpixbuf, 0, 0) - cr.paint() + self.redraw = False cr.translate(0.5, 0.5) cr.set_line_width(1.0) @@ -361,15 +519,15 @@ def on_draw(self, widget, cr: cairo.Context, width, height, *args): """ if self.rect_select_enabled: x, y = self.select_start_x - 0.5, self.select_start_y - 0.5 - width = self.cursor_smooth_x - self.select_start_x - height = self.cursor_smooth_y - self.select_start_y + sel_w = self.cursor_smooth_x - self.select_start_x + sel_h = self.cursor_smooth_y - self.select_start_y cr.set_source(Preference.selection_box) - draw_rounded_rectangle(cr, x, y, width, height) + draw_rounded_rectangle(cr, x, y, sel_w, sel_h) cr.fill() cr.set_source(Preference.selection_box_border) - draw_rounded_rectangle(cr, x, y, width, height) + draw_rounded_rectangle(cr, x, y, sel_w, sel_h) cr.stroke() cr.set_source(source) @@ -514,18 +672,19 @@ def on_key_press(self, *args): def on_motion(self, *args): """TODO: Optimize this""" - x_axis, y_axis = args[1], args[2] + x_axis, y_axis = args[1] / self.zoom, args[2] / self.zoom self.cursor_smooth_x = x_axis self.cursor_smooth_y = y_axis if self.middle_move_enabled: - delta_x = self.move_start_x - x_axis - delta_y = self.move_start_y - y_axis - if -1 < delta_x < 1 and -1 < delta_y < 1: - return - self.hadj.set_value(self.hadj.get_value() + delta_x) - self.vadj.set_value(self.vadj.get_value() + delta_y) + delta_x = self.move_start_x - args[1] + delta_y = self.move_start_y - args[2] + if not (-1 < delta_x < 1 and -1 < delta_y < 1): + self.hadj.set_value(self.hadj.get_value() + delta_x) + self.vadj.set_value(self.vadj.get_value() + delta_y) + self.move_start_x = args[1] + self.move_start_y = args[2] if self.parent.running_mode: return @@ -637,7 +796,8 @@ def on_motion(self, *args): self.redraw = True self.component_dragged = True - self.queue_draw() + + self.queue_draw() else: if self.rect_select_enabled: @@ -664,10 +824,10 @@ def on_motion(self, *args): def on_button_press_primary(self, *args): - self.cursor_smooth_x = args[2] - self.cursor_smooth_y = args[3] + self.cursor_smooth_x = args[2] / self.zoom + self.cursor_smooth_y = args[3] / self.zoom - if args[1] == Gdk.BUTTON_PRIMARY: + if args[1] >= 1: if not self.parent.running_mode: if self._pushed_component_name == const.component_none and not self._pasted_components: @@ -689,7 +849,7 @@ def on_button_press_primary(self, *args): else: im = inv_matrix(c[1].matrix) if c[1].isMouseOvered(im[0] * (self.cursor_smooth_x - c[1].pos_x) + im[1] * (self.cursor_smooth_y - c[1].pos_y) + c[1].pos_x, - im[2] * (self.cursor_smooth_x - c[1].pos_x) + im[3] * (self.cursor_smooth_y - c[1].pos_y) + c[1].pos_y): + im[2] * (self.cursor_smooth_x - c[1].pos_x) + im[3] * (self.cursor_smooth_y - c[1].pos_y) + c[1].pos_y): if c in self.circuit.selected_components: self.drag_delta_x = 0 self.drag_delta_y = 0 @@ -704,6 +864,13 @@ def on_button_press_primary(self, *args): if self.drag_enabled: self.comps_rect = get_components_rect(self.circuit.selected_components) + self.drag_initial_offsets = {} + for c in self.circuit.selected_components: + if c[0] != const.component_net: + comp_inst = c[1] + init_offset = self.animation_controller.get_visual_offset(comp_inst) + self.drag_initial_offsets[comp_inst] = init_offset + self.animation_controller.cancel_animation(comp_inst) else: self.preadd = True @@ -720,10 +887,9 @@ def on_button_press_primary(self, *args): self.mouse_down = True def on_button_press_middle(self, *args): - if args[2] == Gdk.BUTTON_MIDDLE: - self.middle_move_enabled = True - self.move_start_x = self.cursor_smooth_x - self.move_start_y = self.cursor_smooth_y + self.middle_move_enabled = True + self.move_start_x = args[2] + self.move_start_y = args[3] def refresh_nets(self): tmp_components = self.circuit.components[:] @@ -745,12 +911,21 @@ def refresh_nets(self): # Left Click def on_button_release_primary(self, *args): state = args[0].get_current_event_state() - if args[1] == Gdk.BUTTON_PRIMARY: # Left button released + if args[1] >= 1: # Left button released if not self.parent.running_mode: self.drag_enabled = False if self.component_dragged: self.component_dragged = False + for c in self.circuit.selected_components: + if c[0] != const.component_net: + comp_inst = c[1] + init_x, init_y = self.drag_initial_offsets.get(comp_inst, (0.0, 0.0)) + dx = init_x + (self.cursor_smooth_x - self.select_start_x - self.drag_delta_x) + dy = init_y + (self.cursor_smooth_y - self.select_start_y - self.drag_delta_y) + self.animation_controller.start_offset_animation(comp_inst, (dx, dy), self.glide_duration_ms) + self.drag_initial_offsets.clear() + tmp_components = self.circuit.components[:] for c in tmp_components: if c[0] == const.component_net: @@ -843,7 +1018,8 @@ def on_button_release_primary(self, *args): if not selected and not state & Gdk.ModifierType.CONTROL_MASK: self.circuit.selected_components = [] - self.set_selected_component_to_prop_window() + if args[1] == 2: + self.set_selected_component_to_prop_window() if len(self.circuit.selected_components) == 0: self.parent.disable_edit_actions() @@ -952,7 +1128,7 @@ def on_button_release_primary(self, *args): self.cursor_x - self._paste_center_x bottom = self._pasted_rect[3] + \ self.cursor_y - self._paste_center_y - if left >= 0 and top >= 0 and right <= 1980 and bottom <= 1080: + if left >= 0 and top >= 0 and right <= self.width and bottom <= self.height: for cadd in self._pasted_components: if cadd[0] == const.component_net: cadd[1] += self.cursor_x - \ @@ -1004,7 +1180,7 @@ def on_button_release_primary(self, *args): component_data[1].pos_x = self.cursor_x component_data[1].pos_y = self.cursor_y - if 0 <= component_data[1].pos_x + component_data[1].rot_comp_rect[0] + 3 and 0 <= component_data[1].pos_y + component_data[1].rot_comp_rect[1] + 3 and component_data[1].pos_x + component_data[1].rot_comp_rect[2] - 3 <= 1920 and component_data[1].pos_y + component_data[1].rot_comp_rect[3] - 3 <= 1080: + if 0 <= component_data[1].pos_x + component_data[1].rot_comp_rect[0] + 3 and 0 <= component_data[1].pos_y + component_data[1].rot_comp_rect[1] + 3 and component_data[1].pos_x + component_data[1].rot_comp_rect[2] - 3 <= self.width and component_data[1].pos_y + component_data[1].rot_comp_rect[3] - 3 <= self.height: for p in component_data[1].rot_input_pins + component_data[1].rot_output_pins: self.circuit.split_nets( p[0] + self.cursor_x, p[1] + self.cursor_y) @@ -1028,8 +1204,7 @@ def on_button_release_primary(self, *args): im[2] * (self.cursor_smooth_x - c[1].pos_x) + im[3] * ( self.cursor_smooth_y - c[1].pos_y) + c[1].pos_y, self.circuit.current_time): - if not self.parent.pause_running_mode and not self.circuit.analyze_logic(): - self.parent.diagram_window.diagram_area.createDiagram() + self.parent.recompute_simulation() self.queue_draw() break @@ -1063,12 +1238,12 @@ def on_button_release_secondary(self, *args): def set_selected_component_to_prop_window(self): if len(self.circuit.selected_components) == 1: if self.circuit.selected_components[0][0] != const.component_net: - self.parent.prop_window.set_component( - self.circuit.selected_components[0][1]) + self.parent.prop_window.show_properties( + self.circuit.selected_components[0][1], self.parent) else: - self.parent.prop_window.set_component(None) + self.parent.prop_window.show_properties(None) else: - self.parent.prop_window.set_component(None) + self.parent.prop_window.show_properties(None) def set_component(self, comp_name): self._pasted_components = None diff --git a/ggate/Exporter.py b/ggate/Exporter.py index 9aa1440..637d7e8 100644 --- a/ggate/Exporter.py +++ b/ggate/Exporter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- import cairo import os @@ -124,7 +123,7 @@ def _save_schematics_as_image_response(settingsdialog, response, content): settingsdialog.close() - settingsdialog.parent.statusbar.push(0, "Image schematics saved..") + settingsdialog.parent.statusbar.update(_("Image schematics saved..")) def save_timing_diagram_as_image(diagram_area, parent): @@ -178,7 +177,7 @@ def _save_timing_diagram_as_image_response(settingsdialog, response, content): surface.finish() settingsdialog.close() - settingsdialog.parent.statusbar.push(0, "Timing diagram saved..") + settingsdialog.parent.statusbar.update(_("Timing diagram saved..")) def draw_schematics_for_file(cr, circuit, withlevels): @@ -188,7 +187,7 @@ def draw_schematics_for_file(cr, circuit, withlevels): layout = PangoCairo.create_layout(cr) layout.set_font_description(Preference.drawing_font) - circuit.analyze_connections() + circuit.analyze_net_connections() if withlevels: circuit.set_netlevels() diff --git a/ggate/MainFrame.py b/ggate/MainFrame.py index 9dc618e..4c3257b 100644 --- a/ggate/MainFrame.py +++ b/ggate/MainFrame.py @@ -1,14 +1,14 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- import os import sys import webbrowser import gi + gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import Gtk, Gdk, GdkPixbuf, Gio, Adw +from gi.repository import Gtk, Gdk, GdkPixbuf, Gio, Adw, GLib from ggate import UserInterfaces, config from ggate.const import definitions as const from ggate.Exporter import save_schematics_as_image @@ -16,30 +16,53 @@ from ggate.DrawArea import DrawArea from ggate.ComponentView import ComponentView from ggate.CircuitManager import CircuitManager -from ggate.PropertyWindow import PropertyWindow -from ggate.PreferencesWindow import PreferencesWindow +from ggate.Components.Windows.Properties import PropertyWindow +from ggate.Components.Windows.Preferences import PreferencesWindow +from ggate.Components.Windows.About import AboutGGate from ggate import Preference +from ggate import Themes from ggate.Components.LogicGates import logic_gates - from ggate.Components.Managers.FileManager import FileIOManager from ggate.Components.Managers.FileManager import FileManager from ggate.Components.Managers.Alerts import AlertDialogs - +from ggate.Components.Windows.TimingGraph.Display import TimingGraphDisplayWindow from ggate.StatusDisplay import StatusDisplay -from ggate.TimingDiagramWindow import TimingDiagramWindow themed_icons = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) -themed_icons.add_search_path(config.DATADIR + "/images") +themed_icons.add_search_path(config.ICONDIR) TOOLTIPS = { "simulation": { "start": _("Run and simulate this circuit"), "stop": _("Stop simulation"), - "pause": _("Continue simulation"), } } +WINDOW_WIDTH_PERCENT = 0.85 +WINDOW_HEIGHT_PERCENT = 0.75 +DEFAULT_WINDOW_WIDTH = 640 +DEFAULT_WINDOW_HEIGHT = 400 + +def compute_default_window_size(): + display = Gdk.Display.get_default() + if not display: + return DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT + + monitors = display.get_monitors() + if not monitors or len(monitors) == 0: + return DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT + + monitor = monitors.get_item(0) + if not monitor: + return DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT + + geometry = monitor.get_geometry() + width = int(geometry.width * WINDOW_WIDTH_PERCENT) + height = int(geometry.height * WINDOW_HEIGHT_PERCENT) + return width, height + + class ShortCutWindow: def __init__(self, parent): shortcut_builder = Gtk.Builder.new_from_string(UserInterfaces.shortcut_ui, -1) @@ -54,7 +77,6 @@ def __init__(self, *args, **kwargs): ) self.application: Adw.Application = kwargs["application"] self.running_mode = False - self.pause_running_mode = False self.io_manager = FileIOManager self.file_manager = FileManager.for_glc(self) @@ -65,54 +87,37 @@ def __init__(self, *args, **kwargs): self.circuit.connect("message-changed", self.on_circuit_message_changed) self.circuit.connect("item-unselected", self.on_circuit_item_unselected) self.circuit.connect("alert", self.on_circuit_alert) + self.circuit.connect("currenttime-changed", self.on_sim_progress) Preference.load_settings() + Themes.init_registry(config.RUNNING_FROM_SOURCE) + Themes.apply_chrome(Preference.theme, Gdk.Display.get_default()) + # Component window self.comp_window = ComponentView() self.comp_window.connect("component-checked", self.on_comp_checked) + self.statusbar = StatusDisplay() + self.create_window() # Property window self.prop_window = PropertyWindow() - self.prop_window.set_transient_for(self) - self.prop_window.set_hide_on_close(True) - self.prop_window.set_modal(True) - self.prop_window.connect("window-hidden", self.on_propwindow_hidden) self.prop_window.connect("property-changed", self.on_property_changed) - # Timing diagram window - self.diagram_window = TimingDiagramWindow(self) - - # Preferences window - self.pref_window = PreferencesWindow(self) - - # About dialog - self.about_dialog = Gtk.AboutDialog() - picture = Gdk.Texture.new_for_pixbuf( - GdkPixbuf.Pixbuf.new_from_file(config.DATADIR + "/images/ggate.png") - ) - self.about_dialog.set_logo(picture) - self.about_dialog.set_program_name(const.app_name) - self.about_dialog.set_version(config.VERSION) - self.about_dialog.set_comments(const.description) - self.about_dialog.set_copyright(const.copyright) - self.about_dialog.set_website(const.website) - self.about_dialog.set_license(const.license) - self.about_dialog.set_authors(const.developer) - - tr_credits = _("translator-credits") - if tr_credits != "translator-credits": - self.about_dialog.set_translator_credits(tr_credits) + # Other Windows + self.timing_diagram = TimingGraphDisplayWindow(self) self.clipboard = self.get_clipboard() if len(sys.argv) >= 2: self.circuit.open_file(sys.argv[1]) + self.drawarea.zoom = 1.0 self.drawarea.redraw = True self.drawarea.queue_draw() + GLib.idle_add(self.drawarea.center_viewport) def set_up_shortcuts(self, *args): actions = { @@ -189,8 +194,6 @@ def set_up_action_bar(self, *args): _("Rotate component ") + "" + _("Left 90°") + "" ) - self.action_bar.pack_start(self.action_rotleft) - # Rotate Right Action self.action_rotright = Gtk.Button() image = Gtk.Image.new_from_icon_name("object-rotate-right-symbolic") @@ -200,7 +203,12 @@ def set_up_action_bar(self, *args): _("Rotate component ") + "" + _("Right 90°") + "" ) - self.action_bar.pack_start(self.action_rotright) + # Group Rotate Actions + _rot_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + _rot_box.add_css_class("linked") + _rot_box.append(self.action_rotleft) + _rot_box.append(self.action_rotright) + self.action_bar.pack_start(_rot_box) # Flip Horizontal self.action_fliphori = Gtk.Button() @@ -211,8 +219,6 @@ def set_up_action_bar(self, *args): _("Flip component " + "" + _("horizontally") + "") ) - self.action_bar.pack_start(self.action_fliphori) - # Flip Vertical self.action_flipvert = Gtk.Button() image = Gtk.Image.new_from_icon_name("object-flip-vertical-symbolic") @@ -222,13 +228,16 @@ def set_up_action_bar(self, *args): _("Flip component ") + "" + _("vertically") + "" ) - self.action_bar.pack_start(self.action_flipvert) + # Group Flip Actions + _flip_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + _flip_box.add_css_class("linked") + _flip_box.append(self.action_fliphori) + _flip_box.append(self.action_flipvert) + self.action_bar.pack_start(_flip_box) # Add Net self.action_net = Gtk.ToggleButton() - image = Gtk.Image.new_from_pixbuf( - GdkPixbuf.Pixbuf.new_from_file(config.DATADIR + "images/add-net.png") - ) + image = Gtk.Image.new_from_icon_name("list-add-symbolic") self.action_net.set_child(image) self.action_net.connect("toggled", self.on_action_net_toggled) self.action_net.set_tooltip_markup( @@ -238,10 +247,11 @@ def set_up_action_bar(self, *args): self.action_net.set_active(False) def create_window(self): - self.set_default_size(640, 400) + self.set_default_size(*compute_default_window_size()) paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.toast_overlay = Adw.ToastOverlay() # Menu Button @@ -257,68 +267,38 @@ def create_window(self): self.popover = Gtk.PopoverMenu.new_from_model(_menu) self.menu_button.set_popover(self.popover) - # play, pause, stop button + # play / stop button self.action_run = Gtk.ToggleButton() - self.action_pause = Gtk.Button() play_image = Gtk.Image.new_from_icon_name("media-playback-start-symbolic") self.action_run.set_tooltip_text(TOOLTIPS["simulation"]["start"]) self.action_run.connect("toggled", self.on_action_run_clicked) - - pause_image = Gtk.Image.new_from_icon_name("media-playback-pause-symbolic") - self.action_pause.set_tooltip_text(TOOLTIPS["simulation"]["pause"]) - self.action_pause.set_visible(False) - self.action_pause.connect("clicked", self.on_action_pause_clicked) - self.action_run.set_child(play_image) - self.action_pause.set_child(pause_image) - - _run_pause_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - _run_pause_box.add_css_class("linked") - _run_pause_box.append(self.action_run) - _run_pause_box.append(self.action_pause) # Header Bar - self.header_bar = Gtk.HeaderBar() - - if sys.platform.startswith("win32") or sys.platform.startswith("darwin"): - self.header_bar.set_use_native_controls(True) - - if self.header_bar.get_use_native_controls(): - self.header_bar.pack_end(self.menu_button) - self.header_bar.pack_end(_run_pause_box) - else: - self.header_bar.pack_start(self.menu_button) - self.header_bar.pack_end(_run_pause_box) - - self.set_titlebar(self.header_bar) + self.header_bar = Adw.HeaderBar() + self.header_bar.pack_end(self.menu_button) + self.header_bar.pack_end(self.action_run) # Draw area self.drawarea = DrawArea(self) self.drawarea.circuit = self.circuit - box.append(self.drawarea) - self.drawarea.set_vexpand(True) - self.drawarea.set_hexpand(True) - # Status bar - self.statusbar = StatusDisplay() + self.toast_overlay.set_child(self.drawarea) + self.toast_overlay.set_vexpand(True) + self.toast_overlay.set_hexpand(True) + + box.append(self.toast_overlay) + + # Status bar / Bottom Action Bar self.action_bar = Gtk.ActionBar() + self.action_bar.set_hexpand(True) self.set_up_action_bar() self.set_up_shortcuts() - action_bar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - - _box = Gtk.Box() - _box.set_hexpand(False) - _box.set_halign(Gtk.Align.START) - - _box.append(self.action_bar) - action_bar_box.append(_box) - action_bar_box.append(self.statusbar) - action_bar_box.set_hexpand(True) - - box.append(action_bar_box) + self.action_bar.pack_end(self.statusbar) + box.append(self.action_bar) # Component box paned.set_start_child(self.comp_window) @@ -331,7 +311,10 @@ def create_window(self): paned.set_resize_start_child(False) paned.set_shrink_start_child(False) - self.set_child(paned) + toolbar_view = Adw.ToolbarView() + toolbar_view.add_top_bar(self.header_bar) + toolbar_view.set_content(paned) + self.set_content(toolbar_view) # Connect events self.connect("close-request", self.on_window_delete) @@ -340,8 +323,11 @@ def create_window(self): def create_new_buffer(self, *args): self.set_title("%s - %s" % (const.text_notitle, const.app_name)) + self.drawarea.reset_initial_centering() self.reset_frame() self.circuit.reset_circuit() + self.drawarea.clear_animations() + self.drawarea.center_viewport() self.drawarea.nearest_component = None self.drawarea.redraw = True self.drawarea.queue_draw() @@ -352,13 +338,12 @@ def reset_frame(self): self.drawarea.set_component(const.component_none) self.disable_edit_actions() self.action_net.set_active(False) - self.diagram_window.destroy() - self.diagram_window = TimingDiagramWindow(self) # >> app action handlers >> def on_action_about_pressed(self, *widget): - self.about_dialog.present() + about_dialog = AboutGGate.create() + about_dialog.present(self) def on_action_new_pressed(self, *args): if self.circuit.need_save: @@ -373,7 +358,7 @@ def on_action_open_pressed(self, *args): def on_action_save_pressed(self, *args): fpath = self.circuit.filepath - if not os.path.exists(fpath) or not self.save_file_complete(): + if not os.path.exists(fpath) or not self.save_file__complete(): return self.on_action_saveas_pressed() def on_action_saveas_pressed(self, *args): @@ -392,12 +377,13 @@ def open_file_complete(self, path): if self.circuit.open_file(path): return + self.drawarea.reset_initial_centering() self.reset_frame() + self.drawarea.clear_animations() + self.drawarea.zoom = 1.0 self.drawarea.redraw = True self.drawarea.queue_draw() - - # todo: translations - self.statusbar.update(f"Opened {path.split('/')[-1]}") + GLib.idle_add(self.drawarea.center_viewport) def save_file__complete(self, path = None): "Saves current circuit to the specified path or opened file" @@ -443,50 +429,47 @@ def on_circuit_run(self, *args): self.comp_window.set_all_sensitive(False) self.action_net.set_sensitive(False) self.action_net.set_active(False) - self.prop_window.hide() + self.prop_window.dismiss() self.drawarea.set_component(const.component_none) self.drawarea.component_dragged = False self.drawarea.drag_enabled = False self.drawarea.rect_select_enabled = False + self.drawarea.clear_animations() self.circuit.analyze_net_connections() self.circuit.initialize_logic() - if not self.circuit.analyze_logic(): - self.diagram_window.diagram_area.createDiagram() + self.circuit.analyze_logic(callback=self.on_simulation_finished) + + def on_simulation_finished(self, is_error): + if not is_error: + if hasattr(self, "timing_diagram") and self.timing_diagram.get_visible(): + self.timing_diagram._draw_area.draw() + self.drawarea.redraw = True + self.drawarea.queue_draw() + + def on_sim_progress(self, circuit, current_time): + # progress signal -> status % + trailing timing-graph refresh + if self.circuit.playback.is_playing: + pct = min(100, int(current_time / Preference.max_calc_duration * 100)) + self.statusbar.update(_("Playing: %d%%") % pct) + if self.timing_diagram.get_visible(): + self.timing_diagram._draw_area.draw() self.drawarea.redraw = True self.drawarea.queue_draw() def on_circuit_stop(self, *args): + self.circuit.cancel_simulation() if self.running_mode: self.running_mode = False if self.circuit.action_count < len(self.circuit.components_history) - 1: self.action_redo.set_sensitive(True) self.comp_window.set_all_sensitive(True) self.action_net.set_sensitive(True) - self.diagram_window.close() - self.diagram_window = TimingDiagramWindow(self) - self.statusbar.update("") + + self.drawarea.clear_animations() self.drawarea.redraw = True self.drawarea.queue_draw() - def on_action_pause_clicked(self, widget, *args): - if self.drawarea.drag_enabled: - return - if self.pause_running_mode: # if already paused, play simulation - play_image = Gtk.Image.new_from_icon_name("media-playback-pause-symbolic") - widget.set_tooltip_markup(TOOLTIPS["simulation"]["pause"]) - widget.set_child(play_image) - - self.pause_running_mode = False - if not self.circuit.analyze_logic(): - self.diagram_window.diagram_area.createDiagram() - self.drawarea.queue_draw() - else: # if not paused, pause it - pause_image = Gtk.Image.new_from_icon_name("media-playback-start-symbolic") - widget.set_tooltip_markup(TOOLTIPS["simulation"]["start"]) - widget.set_child(pause_image) - self.pause_running_mode = True - def on_action_run_clicked(self, widget, *args): if self.drawarea.drag_enabled: return @@ -495,72 +478,60 @@ def on_action_run_clicked(self, widget, *args): widget.set_tooltip_markup(TOOLTIPS["simulation"]["stop"]) widget.set_child(stop_image) self.on_circuit_run() - self.action_pause.set_visible(True) else: start_image = Gtk.Image.new_from_icon_name("media-playback-start-symbolic") widget.set_tooltip_markup(TOOLTIPS["simulation"]["start"]) widget.set_child(start_image) self.on_circuit_stop() - self.action_pause.set_visible(False) def on_action_cut_pressed(self, *widget): self.on_action_copy_pressed() self.on_action_delete_pressed() def on_action_copy_pressed(self, *widget): - self.clipboard.set( - self.circuit.converter - .components_to_string(self.circuit.selected_components) - ) + serialized = self.circuit.converter.components_to_string(self.circuit.selected_components) + self.clipboard.set(serialized) + if not serialized: return + self.show_feedback(_("Copied selected components to clipboard"), is_toast=True) - def on_action_paste_pressed(self, *widget): - def _handler(clipboard, task): + def on_action_paste_pressed(self, *args): + def _handler(clipboard: Gdk.Clipboard, task, *args): str_data = clipboard.read_text_finish(task) - if str_data is not None: - tmp = string_to_components(str_data) - if isinstance(tmp, str): - dialog = Gtk.MessageDialog( - transient_for=self, - message_type=Gtk.MessageType.ERROR, - button_type=Gtk.ButtonsType.OK, - ) - dialog.set_markup(_("Error")) - dialog.get_message_area().append(Gtk.Label(label=tmp)) - dialog.present() - return - else: - pasted_components = tmp - - if not pasted_components: - return + if str_data is None: return # noqa: E701 + components = self.circuit.converter.string_to_components(str_data) + if isinstance(components, str) or len(components) == 0: + self.show_feedback(_("Unable to parse clipboard data"), is_toast=True) + return + else: self.drawarea.set_component(const.component_none) - self.drawarea.set_pasted_components(pasted_components) + self.drawarea.set_pasted_components(components) self.clipboard.read_text_async(None, _handler) def on_action_undo_pressed(self, *widget): - if self.circuit.action_count == 0: return self.circuit.undo() + self.drawarea.clear_animations() self.disable_edit_actions() self.drawarea.redraw = True self.drawarea.queue_draw() def on_action_redo_pressed(self, *widget): - if self.circuit.action_count == len(self.circuit.components_history) - 1: return self.circuit.redo() + self.drawarea.clear_animations() self.disable_edit_actions() self.drawarea.redraw = True self.drawarea.queue_draw() def on_action_delete_pressed(self, *args): self.circuit.remove_selected_component() + self.drawarea.clear_animations() self.drawarea.nearest_component = None self.drawarea.preselected_component = None self.circuit.push_history() @@ -569,6 +540,7 @@ def on_action_delete_pressed(self, *args): self.drawarea.queue_draw() def on_action_rotate_left_90(self, *widget): + self.drawarea.clear_animations() if logic_gates[self.drawarea.get_component()] is None: self.circuit.rotate_left_selected_components() self.circuit.push_history() @@ -578,6 +550,7 @@ def on_action_rotate_left_90(self, *widget): self.drawarea.queue_draw() def on_action_rotate_right_90(self, *widget): + self.drawarea.clear_animations() if logic_gates[self.drawarea.get_component()] is None: self.circuit.rotate_right_selected_components() self.circuit.push_history() @@ -587,6 +560,7 @@ def on_action_rotate_right_90(self, *widget): self.drawarea.queue_draw() def on_action_flip_horizontally(self, *widget): + self.drawarea.clear_animations() if logic_gates[self.drawarea.get_component()] is None: self.circuit.flip_hori_selected_components() self.circuit.push_history() @@ -596,6 +570,7 @@ def on_action_flip_horizontally(self, *widget): self.drawarea.queue_draw() def on_action_flip_vertically(self, *widget): + self.drawarea.clear_animations() if logic_gates[self.drawarea.get_component()] is None: self.circuit.flip_vert_selected_components() self.circuit.push_history() @@ -606,37 +581,23 @@ def on_action_flip_vertically(self, *widget): def on_action_property_pressed(self, *widget): self.drawarea.set_selected_component_to_prop_window() - self.prop_window.present() def on_action_show_help(self, *args): Gtk.show_uri(None, const.help, Gdk.CURRENT_TIME) - # def on_action_translate_pressed(self, *args): - # webbrowser.open(const.devel_translate) def on_action_bug_pressed(self, *args): webbrowser.open(const.devel_bug) def on_action_diagram_pressed(self, *widget): - self.diagram_window.present() + self.timing_diagram.display() def on_action_save_image(self, *args): save_schematics_as_image(self.circuit, self.running_mode, self) - def _prefs_changed(self, dialog, response, *args): - if response == Gtk.ResponseType.APPLY: - self.pref_window.apply_settings() - - Preference.save_settings() - self.drawarea.redraw = True - self.drawarea.queue_draw() - self.pref_window = PreferencesWindow(self) - dialog.close() - def on_action_prefs_pressed(self, *widget): - self.pref_window.connect("response", self._prefs_changed) - self.pref_window.update_dialog() - self.pref_window.present() + pref_dialog = PreferencesWindow(self) + pref_dialog.present(self) def on_comp_checked(self, widget, comp_name): if logic_gates[comp_name]: @@ -663,39 +624,45 @@ def on_compwindow_hidden(self, widget): self.action_components.set_active(False) def on_propwindow_hidden(self, widget): - - widget.destroy() - self.prop_window = PropertyWindow() - self.prop_window.set_transient_for(self) - self.prop_window.set_hide_on_close(True) - self.prop_window.set_modal(True) - self.prop_window.connect("window-hidden", self.on_propwindow_hidden) - self.prop_window.connect("property-changed", self.on_property_changed) self.drawarea.queue_draw() def on_property_changed(self, widget): self.circuit.push_history() + if self.running_mode: + self.recompute_simulation() self.drawarea.redraw = True self.drawarea.queue_draw() + def recompute_simulation(self): + # a live edit (property or switch) invalidates the recorded timeline: stop and recompute from t=0 + self.circuit.analyze_net_connections() + self.circuit.initialize_logic() + self.circuit.analyze_logic(callback=self.on_simulation_finished) + def on_circuit_title_changed(self, circuit, title): self.set_title(title) - def on_circuit_message_changed(self, circuit, message): - self.statusbar.update(message) + def show_feedback(self, message: str, is_toast: bool = False, timeout: int = 3): + if is_toast: + toast = Adw.Toast.new(message) + toast.set_timeout(timeout) + self.toast_overlay.add_toast(toast) + else: + self.statusbar.update(message) + + def on_circuit_message_changed(self, circuit, message, is_toast=False): + self.show_feedback(message, is_toast=is_toast) def on_circuit_item_unselected(self, circuit): - self.prop_window.set_component(None) + self.prop_window.show_properties(None) def on_circuit_alert(self, circuit, message): - dialog = Gtk.MessageDialog( - transient_for=self, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, + dialog = Adw.AlertDialog( + heading=_("Error"), + body=_(message), ) - dialog.set_markup(_("Error")) - dialog.get_message_area().append(Gtk.Label(label=_(message))) - dialog.present() + dialog.add_response("ok", _("OK")) + dialog.present(self) def disable_edit_actions(self): if logic_gates[self.drawarea.get_component()] is None: @@ -707,7 +674,7 @@ def disable_edit_actions(self): class GLogicApplication(Adw.Application): def __init__(self, *args, **kwargs): - super().__init__(*args, application_id="org.astralco.ggate", **kwargs) + super().__init__(*args, application_id=config.APP_PREFIX, **kwargs) self.window = None def action_handler(self, action): @@ -725,7 +692,13 @@ def on_show_shortcut(self, *args): ShortCutWindow(self.window) def do_startup(self, *args): - Gtk.Application.do_startup(self) + Adw.Application.do_startup(self) + + display = Gdk.Display.get_default() + if display: + theme = Gtk.IconTheme.get_for_display(display) + resource_prefix = "/org/astralco/ggate/Dev/hicolor" if config.RUNNING_FROM_SOURCE else "/org/astralco/ggate/hicolor" + theme.add_resource_path(resource_prefix) # New File Pressed action = Gio.SimpleAction.new("on_action_new_pressed", None) @@ -792,3 +765,4 @@ def do_activate(self, *args): self.add_window(self.window) self.window.present() self.window.show() + self.window.set_focus(None) diff --git a/ggate/MenuPopover.py b/ggate/MenuPopover.py index 23a2d06..dcd1b52 100644 --- a/ggate/MenuPopover.py +++ b/ggate/MenuPopover.py @@ -1,11 +1,7 @@ -import os -from gi.repository import Gtk, Gdk -from ggate.config import DATADIR +from gi.repository import Gtk, Gdk, GLib +from ggate import config -def _get_icon_path(icon: str): - return os.path.join(DATADIR, "images", "actions", f"{icon}.svg") - -menu_xml = f""" +menu_xml = """
@@ -13,22 +9,22 @@ def _get_icon_path(icon: str): Flip Horizontally menu.flip_hori - {_get_icon_path("flip-horizontal")} + flip-horizontal-symbolic Flip Vertically menu.flip_verti - {_get_icon_path("flip-vertical")} + flip-vertical-symbolic Rotate Left menu.rot_left - {_get_icon_path("rotate-left")} + rotate-left-symbolic Rotate right menu.rot_right - {_get_icon_path("rotate-right")} + rotate-right-symbolic
@@ -96,19 +92,25 @@ def __init__(self, parent): def _get_window(self) -> Gtk.ApplicationWindow: return self.get_parent().parent - def _handle_clipboard(self, clipboard, task, *args): + def _handle_clipboard(self, clipboard: Gdk.Clipboard, task, *args): window = self._get_window() - str_data = window.get_clipboard().read_text_finish(task) + try: + str_data = clipboard.read_text_finish(task) + except GLib.GError: + str_data = None if str_data is not None: components = self._parent.circuit.converter.string_to_components(str_data) has_clipboard = not isinstance(components, str) and len(components) > 0 window.action_set_enabled("app.on_action_paste_pressed", has_clipboard) + else: + window.action_set_enabled("app.on_action_paste_pressed", False) def calculate(self, x, y): parent = self.get_parent() - point_x = x - parent.get_hadjustment().get_value() - point_y = y - parent.get_vadjustment().get_value() - return point_x, point_y + coords = parent.drawingarea.translate_coordinates(parent, x, y) + if coords is not None: + return coords[0], coords[1] + return x, y def present(self, x, y, *args): rectangle = Gdk.Rectangle() @@ -142,9 +144,10 @@ def __init__(self, parent): def calculate(self, x, y): parent = self.get_parent() - point_x = x - parent.get_hadjustment().get_value() - point_y = y - parent.get_vadjustment().get_value() - return point_x, point_y + coords = parent.drawingarea.translate_coordinates(parent, x, y) + if coords is not None: + return coords[0], coords[1] + return x, y def present(self, x, y): rectangle = Gdk.Rectangle() diff --git a/ggate/PlaybackControls.py b/ggate/PlaybackControls.py new file mode 100644 index 0000000..a4e91c6 --- /dev/null +++ b/ggate/PlaybackControls.py @@ -0,0 +1,137 @@ +from gi.repository import Gtk, Gdk + +_CSS = """ +.playback-bar { padding: 5px; } +.playback-bar button { min-height: 0; padding: 3px; } +""" + +def _ensure_css(): + if getattr(_ensure_css, "_done", False): + return + provider = Gtk.CssProvider() + provider.load_from_string(_CSS) + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + _ensure_css._done = True + +def _format_progress(current, total): + # pick a session-stable unit from the total so width/unit never jump mid-playback + if total >= 1: + scale, unit = 1.0, "s" + elif total >= 1e-3: + scale, unit = 1e3, "ms" + elif total >= 1e-6: + scale, unit = 1e6, "µs" + elif total > 0: + scale, unit = 1e9, "ns" + else: + scale, unit = 1.0, "s" + return f"{current * scale:6.2f} / {total * scale:6.2f} {unit}" + +class PlaybackControls(Gtk.Box): + def __init__(self, mainframe): + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self.mainframe = mainframe + self.circuit = mainframe.circuit + self.playback = self.circuit.playback + self._suppress_seek = False + _ensure_css() + + self.add_css_class("playback-bar") + self.add_css_class("osd") + self.add_css_class("toolbar") + self.set_halign(Gtk.Align.CENTER) + self.set_valign(Gtk.Align.END) + self.set_margin_bottom(6) + self.set_spacing(2) + self.set_visible(False) + + # Stop button — fully ends the run via the header toggle + self.stop_btn = Gtk.Button() + self.stop_btn.add_css_class("flat") + self.stop_img = Gtk.Image.new_from_icon_name("media-playback-stop-symbolic") + self.stop_img.set_pixel_size(14) + self.stop_btn.set_child(self.stop_img) + self.stop_btn.connect("clicked", lambda btn: self.mainframe.action_run.set_active(False)) + self.append(self.stop_btn) + + # Restart button + self.restart_btn = Gtk.Button() + self.restart_btn.add_css_class("flat") + self.restart_img = Gtk.Image.new_from_icon_name("view-refresh-symbolic") + self.restart_img.set_pixel_size(14) + self.restart_btn.set_child(self.restart_img) + self.restart_btn.connect("clicked", lambda btn: self.playback.restart()) + self.append(self.restart_btn) + + # Play/Pause toggle + self.play_btn = Gtk.Button() + self.play_btn.add_css_class("flat") + self.play_img = Gtk.Image.new_from_icon_name("media-playback-start-symbolic") + self.play_img.set_pixel_size(14) + self.play_btn.set_child(self.play_img) + self.play_btn.connect("clicked", lambda btn: self.playback.toggle_play()) + self.append(self.play_btn) + + # Vertical divider separator + self.divider = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) + self.divider.set_margin_top(4) + self.divider.set_margin_bottom(4) + self.append(self.divider) + + # Seek scale + self.scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + self.scale.set_hexpand(True) + self.scale.set_size_request(140, -1) + self.scale.set_draw_value(False) + self.scale.connect("value-changed", self._on_scale_value_changed) + self.append(self.scale) + + # Time label — fixed width + tabular figures so updates don't jiggle the bar + self.time_label = Gtk.Label() + self.time_label.add_css_class("numeric") + self.time_label.set_width_chars(18) + self.time_label.set_xalign(1.0) + self.time_label.set_text("0.00 / 0.00 s") + self.append(self.time_label) + + # Connect signals + self.playback.connect("playback-state-changed", self._on_playback_state_changed) + self.circuit.connect("currenttime-changed", self._on_current_time_changed) + + def _on_playback_state_changed(self, playback, state): + if state in ("playing", "paused", "ended"): + self.set_visible(True) + else: + self.set_visible(False) + + if state == "playing": + self.play_img.set_from_icon_name("media-playback-pause-symbolic") + else: + self.play_img.set_from_icon_name("media-playback-start-symbolic") + + is_ready = state in ("playing", "paused", "ended") + self.stop_btn.set_sensitive(True) + self.play_btn.set_sensitive(is_ready) + self.restart_btn.set_sensitive(is_ready) + self.scale.set_sensitive(is_ready) + + if is_ready: + total_frames = self.playback.frame_count() + self.scale.set_range(0, max(0, total_frames - 1)) + self._update_time_label() + + def _update_time_label(self): + self.time_label.set_text(_format_progress(self.circuit.current_time, self.circuit.total_time())) + + def _on_current_time_changed(self, circuit, t): + self._suppress_seek = True + self.scale.set_value(self.playback.playback_index) + self._suppress_seek = False + self._update_time_label() + + def _on_scale_value_changed(self, scale): + if self._suppress_seek: + return + self.playback.seek(int(scale.get_value())) diff --git a/ggate/Preference.py b/ggate/Preference.py index ff4977c..18010c4 100644 --- a/ggate/Preference.py +++ b/ggate/Preference.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- - import cairo import sys import os @@ -13,14 +11,19 @@ def __setattr__(self, name, value): self.pref_dict[name] = Pango.FontDescription(value) elif isinstance(self.pref_dict[name], cairo.Pattern): try: - rgb = value.split(",") - self.pref_dict[name] = cairo.SolidPattern(float(rgb[0]), float(rgb[1]), float(rgb[2])) - except ValueError: + parts = [float(x) for x in value.split(",")] + if len(parts) == 4: + self.pref_dict[name] = cairo.SolidPattern(*parts) + else: + self.pref_dict[name] = cairo.SolidPattern(parts[0], parts[1], parts[2], 1.0) + except (ValueError, IndexError): self.pref_dict[name] = cairo.SolidPattern(0.0, 0.0, 0.0) elif isinstance(self.pref_dict[name], int): self.pref_dict[name] = int(value) elif isinstance(self.pref_dict[name], float): self.pref_dict[name] = float(value) + elif isinstance(self.pref_dict[name], str): + self.pref_dict[name] = str(value) def __getattr__(self, name): return self.pref_dict[name] @@ -29,10 +32,10 @@ def __getitem__(self, __k): return self.pref_dict.get(__k, None) pref_dict = { - '_red': cairo.SolidPattern(1.0, 0.0, 0.0), - '_green': cairo.SolidPattern(0.0, 1.0, 0.0), - '_blue': cairo.SolidPattern(0.0, 0.0, 1.0), - '_yellow': cairo.SolidPattern(1.0, 1.0, 0.0), + "_red": cairo.SolidPattern(1.0, 0.0, 0.0), + "_green": cairo.SolidPattern(0.0, 1.0, 0.0), + "_blue": cairo.SolidPattern(0.0, 0.0, 1.0), + "_yellow": cairo.SolidPattern(1.0, 1.0, 0.0), "grid_step": 10, @@ -56,9 +59,12 @@ def __getitem__(self, __k): "grid_color": cairo.SolidPattern(0.15, 0.12, 0.15), "selection_box": cairo.SolidPattern(1, 0.75, 0, 0.25), "selection_box_border": cairo.SolidPattern(1, 0.75, 0), - "symbol_type": 0, # 0: MIL/ANSI 1: IEC + "symbol_type": 0, "max_calc_iters": 10000, - "max_calc_duration": 0.0002 + "max_calc_duration": 0.0002, + "playback_duration": 6.0, + "autocenter_resize": 1, + "theme": "Classic", } def load_settings(self): @@ -67,8 +73,8 @@ def load_settings(self): if not success or not data: return - for line in data: - pref = line.split("=") + for line in data.splitlines(): + pref = line.split("=", 1) if len(pref) == 2 and pref[0] in self.pref_dict: self.__setattr__(pref[0], pref[1]) @@ -86,11 +92,16 @@ def save_settings(self): fp.write("%s=%s\n" % (key, self.pref_dict[key].to_string())) elif isinstance(self.pref_dict[key], cairo.Pattern): rgba = self.pref_dict[key].get_rgba() - fp.write("%s=%f,%f,%f\n" % (key, rgba[0], rgba[1], rgba[2])) + if rgba[3] < 1.0: + fp.write("%s=%f,%f,%f,%f\n" % (key, rgba[0], rgba[1], rgba[2], rgba[3])) + else: + fp.write("%s=%f,%f,%f\n" % (key, rgba[0], rgba[1], rgba[2])) elif isinstance(self.pref_dict[key], int): fp.write("%s=%d\n" % (key, self.pref_dict[key])) elif isinstance(self.pref_dict[key], float): fp.write("%s=%.9f\n" % (key, self.pref_dict[key])) + elif isinstance(self.pref_dict[key], str): + fp.write("%s=%s\n" % (key, self.pref_dict[key])) fp.close() sys.modules[__name__] = _Preference() diff --git a/ggate/PreferencesWindow.py b/ggate/PreferencesWindow.py deleted file mode 100644 index 12ab9b8..0000000 --- a/ggate/PreferencesWindow.py +++ /dev/null @@ -1,195 +0,0 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- - -from ggate import Preference -from gi.repository import Gtk, Gdk -from gettext import gettext as _ - - -class PreferencesWindow(Gtk.Dialog): - def __init__(self, parent): - Gtk.Dialog.__init__( - self, title=_("Preferences"), use_header_bar=True, transient_for=parent - ) - self.set_resizable(False) - self.set_modal(True) - self.set_destroy_with_parent(True) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - - pref = Gtk.Box(spacing=5) - - font_label = Gtk.Label(label=_("Font:")) - font_label.set_margin_start(5) - pref.append(font_label) - - # todo: fix font picker - self.drawing_font_btn = Gtk.FontDialogButton() - self.drawing_font_btn.set_halign(Gtk.Align.END) - pref.append(self.drawing_font_btn) - vbox.append(pref) - - table = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - table.set_margin_top(5) - table.set_margin_bottom(5) - - prefset = { - "net colors: ": [ - ("net_color", _("Net:")), - ("net_high_color", _("Net (highlighted):")), - ("net_color_running", _("Net (running):")), - ("highlevel_color", _("Net (high level):")), - ("lowlevel_color", _("Net (low level):")), - ], - "component colors": [ - ("component_color", _("Component:")), - ("component_high_color", _("Component (highlighted):")), - ("component_color_running", _("Component (running):")), - ("picked_color", _("Component (picked):")), - ("preadd_color", _("Component (pre added):")), - ("selected_color", _("Component (selected):")), - ], - "terminals color": [ - ("terminal_color", _("Terminal:")), - ("terminal_color_running", _("Terminal (running):")), - ("cursor_color", _("Cursor:")), - ("bg_color", _("Background:")), - ("bg_color_running", _("Background (running):")), - ("grid_color", _("Grid:")), - ], - } - - self.color_buttons = {} - - for key in prefset.keys(): - frame = Gtk.Frame() - - listBox = Gtk.ListBox() - listBox.set_selection_mode(Gtk.SelectionMode.NONE) - listBox.add_css_class("rich-list") - listBox.add_css_class("view") - - section_row = Gtk.ListBoxRow() - section_row.set_activatable(False) - - _label = Gtk.Label() - _label.set_markup(f"{key.capitalize()}") - - _box = Gtk.Box() - _box.append(_label) - _box.set_size_request(-1, 35) - - section_row.set_selectable(False) - - section_row.set_child(_box) - listBox.append(section_row) - - for prefpair in prefset[key]: - caption_label = Gtk.Label() - caption = Gtk.Box() - - caption_label.set_text(prefpair[1]) - caption.set_size_request(180, -1) - caption.set_halign(Gtk.Align.START) - caption.set_hexpand(True) - - caption.append(caption_label) - caption.set_margin_start(7) - - color_button = Gtk.ColorButton() - self.color_buttons[prefpair[0]] = color_button - - box = Gtk.Box() - box.append(caption) - box.append(color_button) - - row = Gtk.ListBoxRow() - row.set_activatable(False) - row.set_child(box) - - listBox.append(row) - - table.append(frame) - frame.set_child(listBox) - frame.set_margin_start(2) - frame.set_margin_end(2) - - vbox.append(table) - table.set_vexpand(True) - table.set_hexpand(True) - - pref = Gtk.Box(spacing=5) - pref.append(Gtk.Label(label=_("Symbol type:"))) - self.symbol_type_combo = Gtk.ComboBoxText() - self.symbol_type_combo.set_entry_text_column(0) - self.symbol_type_combo.append_text(_("MIL/ANSI")) - self.symbol_type_combo.append_text(_("IEC")) - pref.append(self.symbol_type_combo) - - # vbox.append(pref) - # pref.set_vexpand(True) - # pref.set_hexpand(True) - - # pref = Gtk.Box(spacing=5) - - pref.append(Gtk.Label(label=_("Max calc iters:"))) - self.calc_iter_spin = Gtk.SpinButton() - self.calc_iter_spin.set_hexpand(True) - self.calc_iter_spin.set_increments(1, 10) - self.calc_iter_spin.set_range(10, 1000000) - pref.append(self.calc_iter_spin) - pref.append(Gtk.Label(label=_("Max calc duration [µs]:"))) - self.calc_duration_spin = Gtk.SpinButton() - self.calc_duration_spin.set_increments(1, 10) - self.calc_duration_spin.set_range(0, 100000) - self.calc_duration_spin.set_digits(3) - pref.append(self.calc_duration_spin) - - vbox.append(pref) - - vbox.set_margin_start(10) - vbox.set_margin_top(10) - vbox.set_margin_bottom(10) - vbox.set_margin_end(10) - - pref.set_vexpand(True) - pref.set_hexpand(True) - - box = self.get_content_area() - box.append(vbox) - - box.show() - - self.add_button("Cancel", Gtk.ResponseType.CANCEL) - apply_button = self.add_button("Apply", Gtk.ResponseType.APPLY) - apply_button.add_css_class("suggested-action") - - def update_dialog(self): - self.drawing_font_btn.set_use_font(True) - self.drawing_font_btn.set_font_desc(Preference.drawing_font) - - for key in self.color_buttons: - rgba = Preference.__getattr__(key).get_rgba() - color = Gdk.RGBA() - color.red = round(rgba[0], 2) - color.green = round(rgba[1], 2) - color.blue = round(rgba[2], 2) - color.alpha = 1.0 - - self.color_buttons[key].set_rgba(color) - - self.symbol_type_combo.set_active(Preference.symbol_type) - self.calc_iter_spin.set_value(Preference.max_calc_iters) - self.calc_duration_spin.set_value(Preference.max_calc_duration * 1000000) - - def apply_settings(self): - Preference.drawing_font = self.drawing_font_btn.get_font_desc() - - for key in self.color_buttons: - color = self.color_buttons[key].get_rgba() - Preference.__setattr__( - key, "%f,%f,%f" % (color.red, color.green, color.blue) - ) - - Preference.symbol_type = self.symbol_type_combo.get_active() - Preference.max_calc_iters = self.calc_iter_spin.get_value() - Preference.max_calc_duration = self.calc_duration_spin.get_value() * 0.000001 diff --git a/ggate/PropertyWindow.py b/ggate/PropertyWindow.py deleted file mode 100644 index 01ddd4d..0000000 --- a/ggate/PropertyWindow.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- - -from typing import Optional -from ggate.Components.LogicGates.SystemComponents import BaseComponent -from ggate.const import definitions as const -from gi.repository import Gtk, GObject -from gettext import gettext as _ - - -class PropertyWindow(Gtk.Dialog): - __gsignals__ = { - "property-changed": (GObject.SIGNAL_RUN_FIRST, None, ()), - "window-hidden": (GObject.SIGNAL_RUN_LAST, None, ()), - } - - def __init__(self): - self.title = _("Properties") - - Gtk.Window.__init__(self, title=self.title) - self.set_resizable(True) - self.set_size_request(310, -1) - self.prop_controls = [] - self.old_values = [] - self.vbox: Gtk.Box = None - - self.connect("close-request", self.on_window_delete) - - def on_apply_btn_clicked(self, setter=False, *widget): - values = [] - i = 0 - - if self.component is None: - return - - for p in self.component.properties: - if isinstance(p[1], tuple): - if p[1][0] == const.property_select: - values.append(self.prop_controls[i].get_active()) - - elif p[1][0] == const.property_int: - values.append(int(self.prop_controls[i].get_value())) - self.prop_controls[i].update() - - elif p[1][0] == const.property_float: - values.append(self.prop_controls[i].get_value()) - self.prop_controls[i].update() - - else: - values.append(self.prop_controls[i].get_text()) - elif p[1] == const.property_bool: - values.append(self.prop_controls[i].get_active()) - else: - i -= 1 - i += 1 - - if self.component.propertyChanged(values): - - def _response(widget, *args): - widget.close() - - dialog = Gtk.MessageDialog() - dialog.set_transient_for(self) - dialog.set_modal(True) - dialog.add_button("Ok", Gtk.ButtonsType.OK) - dialog.set_markup(_("Set values are invalid.")) - - dialog.present() - dialog.connect("response", _response) - - else: - self.component.values = values - self.component.set_rot_props() - - self.emit("property-changed") - - def set_component(self, component: Optional[BaseComponent]): - if component is None: - return - - self.component = component - if self.vbox is not None: - self.set_child(None) - - self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - if component is None: - label = Gtk.Label(label=_("Please select a component to edit properties.")) - label.set_vexpand(True) - label.set_hexpand(True) - label.set_margin_bottom(15) - label.set_margin_top(15) - label.set_margin_end(15) - label.set_margin_start(15) - - self.vbox.append(label) - self.set_title(self.title) - - else: - self.set_title("%s - %s" % (self.title, component.description)) - - if len(component.properties) != 0: - layout = Gtk.ListBox() - i = 0 - - # Create property editor - self.prop_controls = [] - for p in component.properties: - has_property = True - - caption = Gtk.Label(label=p[0]) - caption.set_margin_start(2) - caption.set_margin_top(1) - - if isinstance(p[1], tuple): - if p[1][0] == const.property_select: - ctrl = Gtk.ComboBoxText() - choices = p[1][1:] - for choice in choices: - ctrl.append_text(choice) - ctrl.set_active(self.component.values[i]) - ctrl.set_halign(Gtk.Align.START) - ctrl.set_hexpand(True) - ctrl.connect("changed", self.on_apply_btn_clicked) - - elif p[1][0] == const.property_int: - ctrl = Gtk.SpinButton() - ctrl.set_increments(1, 10) - ctrl.set_halign(Gtk.Align.START) - ctrl.set_hexpand(True) - ctrl.set_range(p[1][1], p[1][2]) - ctrl.set_value(component.values[i]) - ctrl.connect("value-changed", self.on_apply_btn_clicked) - # ctrl.set_size_request(p[1][3], -1) - - elif p[1][0] == const.property_float: - ctrl = Gtk.SpinButton() - ctrl.set_increments(1, 10) - ctrl.set_halign(Gtk.Align.START) - ctrl.set_hexpand(True) - ctrl.set_range(p[1][1], p[1][2]) - ctrl.set_digits(p[1][3] if p[1][3] < 3 else 2) - ctrl.set_value(component.values[i]) - ctrl.connect("value-changed", self.on_apply_btn_clicked) - # ctrl.set_size_request(p[1][4], -1) - - else: - ctrl = Gtk.Entry() - ctrl.set_text(component.values[i]) - ctrl.set_halign(Gtk.Align.START) - ctrl.set_hexpand(True) - ctrl.connect("activate", self.on_apply_btn_clicked) - ctrl.set_width_chars(p[1][1]) - - # Setup Boolean properties - elif p[1] == const.property_bool: - ctrl = Gtk.CheckButton("") - ctrl.set_halign(Gtk.Align.START) - ctrl.set_hexpand(True) - ctrl.set_active(component.values[i]) - ctrl.connect("toggled", self.on_apply_btn_clicked) - - else: - i -= 1 - ctrl = Gtk.Label(label="") - has_property = False - i += 1 - - propbox = Gtk.ListBoxRow() - propbox.set_activatable(False) - - ctrl.set_margin_top(3) - ctrl.set_margin_bottom(3) - ctrl.set_margin_end(3) - ctrl.set_halign(Gtk.Align.START) - ctrl.set_size_request(60, -1) - - if has_property: - self.prop_controls.append(ctrl) - - _label = Gtk.Label(label=p[2]) - _label.set_margin_top(3) - _label.set_margin_bottom(3) - _label.set_margin_start(3) - _label.set_margin_end(3) - - if isinstance(ctrl, Gtk.Label): - if ctrl.get_text() == "": - caption = Gtk.Label() - caption.set_markup(f"{p[0]}") - caption.set_margin_top(5) - caption.set_margin_bottom(5) - caption.set_margin_start(2) - caption.set_margin_end(2) - - _list_prop_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - _list_prop_row.append(ctrl) - - _list_prop_row.append(_label) - # _list_prop_row.set_halign(Gtk.Align.START) - _list_prop_row.set_hexpand(True) - caption.set_hexpand(True) - - _list_box_row = Gtk.Box() - _list_box_row.append(caption) - _list_box_row.append(_list_prop_row) - - caption.set_size_request(100, -1) - - _list_prop_row.set_size_request(90, -1) - - propbox.set_child(_list_box_row) - layout.append(propbox) - - frame = Gtk.Frame() - frame.set_child(layout) - layout.add_css_class("view") - layout.add_css_class("rich-list") - layout.set_selection_mode(Gtk.SelectionMode.NONE) - self.vbox.append(frame) - - frame.set_margin_top(10) - frame.set_margin_bottom(10) - frame.set_margin_start(10) - frame.set_margin_end(10) - - else: - _label = Gtk.Label(label=_("This component has no property.")) - self.vbox.append(_label) - _label.set_margin_top(15) - _label.set_margin_end(15) - _label.set_margin_start(15) - _label.set_margin_bottom(15) - - # self.on_apply_btn_clicked(True) - - self.set_child(self.vbox) - self.vbox.show() - self.queue_resize() - - def on_window_delete(self, *args): - self.emit("window-hidden") diff --git a/ggate/Themes.py b/ggate/Themes.py new file mode 100644 index 0000000..0786da0 --- /dev/null +++ b/ggate/Themes.py @@ -0,0 +1,114 @@ +import cairo +import json +import os +from gi.repository import Gio, Adw, Gtk, Gdk, GLib + + +def hex_to_pattern(hex_color: str) -> cairo.SolidPattern: + h = hex_color.lstrip("#") + if len(h) == 8: + r, g, b, a = (int(h[i:i+2], 16) / 255.0 for i in (0, 2, 4, 6)) + elif len(h) == 6: + r, g, b = (int(h[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + a = 1.0 + else: + return cairo.SolidPattern(0.0, 0.0, 0.0) + return cairo.SolidPattern(r, g, b, a) + + +class Theme: + def __init__(self, name: str, accent_hex: str, colors: dict): + self.name = name + self.accent_hex = accent_hex + self.colors = colors + + def get_pattern(self, key: str) -> cairo.SolidPattern: + hex_val = self.colors.get(key) + if not hex_val: + return cairo.SolidPattern(0.0, 0.0, 0.0) + return hex_to_pattern(hex_val) + + +THEME_REGISTRY: dict[str, Theme] = {} + + +def init_registry(is_dev_mode: bool = False) -> None: + THEME_REGISTRY.clear() + + prefix = "/org/astralco/ggate/Dev/themes" if is_dev_mode else "/org/astralco/ggate/themes" + + try: + children = Gio.resources_enumerate_children(prefix, Gio.ResourceLookupFlags.NONE) + for child in children: + path = f"{prefix}/{child}" + try: + data_bytes = Gio.resources_lookup_data(path, Gio.ResourceLookupFlags.NONE) + data_str = data_bytes.get_data().decode("utf-8") + data = json.loads(data_str) + theme = Theme( + name=data["name"], + accent_hex=data.get("accent_hex", "#ffffff"), + colors=data["colors"], + ) + THEME_REGISTRY[theme.name] = theme + except (KeyError, ValueError, json.JSONDecodeError): + pass + except GLib.Error: + pass + + _load_user_themes() + + +def _load_user_themes() -> None: + from ggate.const import definitions + themes_dir = os.path.join(definitions.config_path, "themes") + if not os.path.isdir(themes_dir): + return + for fname in os.listdir(themes_dir): + if not fname.endswith(".json"): + continue + path = os.path.join(themes_dir, fname) + try: + with open(path, encoding="utf-8") as fh: + data = json.load(fh) + theme = Theme( + name=data["name"], + accent_hex=data.get("accent_hex", "#ffffff"), + colors=data["colors"], + ) + THEME_REGISTRY[theme.name] = theme + except (KeyError, ValueError, OSError): + pass + + +def apply_theme(name: str, preference_module) -> None: + theme = THEME_REGISTRY.get(name) + if theme is None: + return + for key, hex_val in theme.colors.items(): + pat = hex_to_pattern(hex_val) + rgba = pat.get_rgba() + if rgba[3] < 1.0: + preference_module.__setattr__(key, f"{rgba[0]},{rgba[1]},{rgba[2]},{rgba[3]}") + else: + preference_module.__setattr__(key, f"{rgba[0]},{rgba[1]},{rgba[2]}") + preference_module.__setattr__("theme", name) + preference_module.save_settings() + + +def apply_chrome(name: str, display: Gdk.Display) -> None: + theme = THEME_REGISTRY.get(name) + if theme is None: + return + + css = ( + f"@define-color accent_color {theme.accent_hex};" + f"@define-color accent_bg_color {theme.accent_hex};" + ) + provider = Gtk.CssProvider() + provider.load_from_string(css) + Gtk.StyleContext.add_provider_for_display( + display, + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) diff --git a/ggate/TimingDiagramWindow.py b/ggate/TimingDiagramWindow.py deleted file mode 100644 index 202f85c..0000000 --- a/ggate/TimingDiagramWindow.py +++ /dev/null @@ -1,312 +0,0 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- - -from ggate.DiagramArea import DiagramArea -from ggate.Exporter import save_timing_diagram_as_image -from gi.repository import Gtk -from gettext import gettext as _ - -class TimingDiagramWindow(Gtk.Window): - def __init__(self, parent): - Gtk.Window.__init__(self, title=_("Timing diagram")) - self.set_default_size(480, 480) - self.parent = parent - self.set_hide_on_close(True) - # self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - self.set_transient_for(parent) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - - self.scale_list = Gtk.ListStore(str) - scales = ["500.0", "200.0", "100.0", "50.0", "20.0", "10.0", "5.0", "2.0", "1.0"] - for scale in scales: - self.scale_list.insert_with_values(0, range(len(scales)), [scale]) - - self.units = ["ns", "µs", "ms"] - - # Scale - vbox.append(self._get_scale()) - - # Cursor Position - vbox.append(self._get_cursor_position()) - - # Range - vbox.append(self._get_range()) - - # Save button - vbox.append(self._get_save()) - - self.diagram_area = DiagramArea(self.parent.circuit, self.parent.drawarea) - self.parent.circuit.connect("currenttime-changed", self.on_diagram_area_current_time_changed) - vbox.append(self.diagram_area) - - vbox.set_margin_top(3) - vbox.set_margin_bottom(6) - vbox.set_margin_start(6) - vbox.set_margin_end(6) - - self.set_child(vbox) - - def _get_save(self): # Save image - save_image_btn = Gtk.Button(label=_("Save _diagram...")) - save_image_btn.set_use_underline(True) - save_image_btn.connect("clicked", self.on_save_image_clicked) - save_image_btn.set_margin_bottom(6) - - return save_image_btn - - def _get_unit_list(self): - unit_list = Gtk.ListStore(str) - for unit in self.units: - unit_list.insert_with_values(0, range(len(self.units)), [unit]) - - return unit_list - - - def _get_cursor_position(self): - - self.cursor_spin = Gtk.SpinButton(hexpand=True) - self.cursor_spin.set_increments(1, 10) - self.cursor_spin.set_range(0, 10000) - self.cursor_spin.set_digits(2) - self.cursor_spin.set_value(200.0) - self.cursor_spin_changed_id = self.cursor_spin.connect("changed", self.on_cursor_spin_changed) - - self.cursor_combo = Gtk.ComboBox.new_with_model(self._get_unit_list()) - renderer_text = Gtk.CellRendererText() - self.cursor_combo.pack_start(renderer_text, True) - self.cursor_combo.add_attribute(renderer_text, "text", 0) - self.cursor_combo.set_active(1) - self.cursor_combo_changed_id = self.cursor_combo.connect("changed", self.on_combo_changed) - - cursor_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True) - cursor_box.append(self.cursor_spin) - cursor_box.append(self.cursor_combo) - cursor_box.add_css_class("linked") - - frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - frame_label = Gtk.Box() - frame_label.append(Gtk.Label(label=_("Cursor position:"))) - frame.append(frame_label) - frame.append(cursor_box) - - frame.set_margin_top(3) - frame.set_margin_bottom(3) - - return frame - - def _get_scale(self): - self.scale_combo = Gtk.ComboBox.new_with_model_and_entry(self.scale_list) - # self.scale_combo.set_events(Gdk.EventMask.FOCUS_CHANGE_MASK) - self.scale_combo.get_child().connect("activate", self.on_scale_combo_activate) - - focus_event_controller = Gtk.EventControllerFocus() - focus_event_controller.connect('leave', self.on_scale_combo_focusout) - - self.scale_combo.get_child().add_controller(focus_event_controller) - self.scale_combo.get_child().set_width_chars(6) - self.scale_combo.set_entry_text_column(0) - self.scale_combo.get_child().set_text("5.0") - - self.scale_unit_combo = Gtk.ComboBox.new_with_model(self._get_unit_list()) - renderer_text = Gtk.CellRendererText() - self.scale_unit_combo.pack_start(renderer_text, True) - self.scale_unit_combo.add_attribute(renderer_text, "text", 0) - self.scale_unit_combo.set_active(1) - self.scale_unit_combo_changed_id = self.scale_unit_combo.connect("changed", self.on_combo_changed) - - scale_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - scale_box.append(self.scale_combo) - scale_box.append(self.scale_unit_combo) - scale_box.add_css_class("linked") - - frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - frame_label = Gtk.Box() - frame_label.append(Gtk.Label(label=_("Scale (px):"))) - frame.append(frame_label) - frame.append(scale_box) - - frame.set_margin_top(3) - frame.set_margin_bottom(3) - - return frame - - def _get_range(self): - # From - from_label = Gtk.Box() - from_label.append(Gtk.Label(label=_("From"))) - - from_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, hexpand=True) - inner_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True) - - self.from_spin = Gtk.SpinButton(hexpand=True) - self.from_spin.set_increments(1, 10) - self.from_spin.set_range(0, 10000) - self.from_spin.set_digits(2) - self.from_spin.set_value(0) - self.from_spin_changed_id = self.from_spin.connect("changed", self.on_from_time_changed) - - self.from_combo = Gtk.ComboBox.new_with_model(self._get_unit_list()) - renderer_text = Gtk.CellRendererText() - self.from_combo.pack_start(renderer_text, True) - self.from_combo.add_attribute(renderer_text, "text", 0) - self.from_combo.set_active(1) - self.from_combo_changed_id = self.from_combo.connect("changed", self.on_combo_changed) - - inner_box.append(self.from_spin) - inner_box.append(self.from_combo) - inner_box.add_css_class("linked") - inner_box.set_margin_end(3) - - from_box.append(from_label) - from_box.append(inner_box) - - # To - to_label = Gtk.Box() - to_label.append(Gtk.Label(label=_("To"))) - to_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, hexpand=True) - inner_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True) - - self.to_spin = Gtk.SpinButton(hexpand=True) - self.to_spin.set_increments(1, 10) - self.to_spin.set_range(0, 10000) - self.to_spin.set_digits(2) - self.to_spin.set_value(200.0) - self.to_spin_changed_id = self.to_spin.connect("changed", self.on_to_time_changed) - - self.to_combo = Gtk.ComboBox.new_with_model(self._get_unit_list()) - renderer_text = Gtk.CellRendererText() - self.to_combo.pack_start(renderer_text, True) - self.to_combo.add_attribute(renderer_text, "text", 0) - self.to_combo.set_active(1) - self.to_combo_changed_id = self.to_combo.connect("changed", self.on_combo_changed) - - inner_box.append(self.to_spin) - inner_box.append(self.to_combo) - inner_box.add_css_class("linked") - inner_box.set_margin_start(3) - - to_box.append(to_label) - to_box.append(inner_box) - - # Container - frame = Gtk.Frame(label=_("Range:")) - - range_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - range_box.append(from_box) - range_box.append(to_box) - range_box.set_margin_end(3) - range_box.set_margin_start(3) - range_box.set_margin_bottom(3) - - frame.set_child(range_box) - - frame.set_margin_top(3) - frame.set_margin_bottom(3) - - return frame - - - def check_scale_format(self): - try: - scale = float(self.scale_combo.get_child().get_text()) - if scale == int(scale): - self.scale_combo.get_child().set_text(str(int(scale)) + ".0") - except ValueError: - scale = 5.0 - self.scale_combo.get_child().set_text("5.0") - - self.on_redraw() - - def on_scale_combo_activate(self, *widget): - self.check_scale_format() - - def on_scale_combo_focusout(self, *args): - self.check_scale_format() - - def expand_time(self, time, unit): - if unit == 0: - return time / 1000000000 - elif unit == 1: - return time / 1000000 - else: - return time / 1000 - - def on_diagram_area_current_time_changed(self, diagram_area, time): - unit = self.cursor_combo.get_active() - if unit == 0: - self.cursor_spin.set_value(time * 1000000000) - elif unit == 1: - self.cursor_spin.set_value(time * 1000000) - else: - self.cursor_spin.set_value(time * 1000) - - def on_from_time_changed(self, widget): - from_time = self.from_spin.get_value() - to_time = self.to_spin.get_value() - if from_time > to_time: - self.to_spin.disconnect(self.to_spin_changed_id) - self.to_spin.set_value(from_time) - self.to_spin_changed_id = self.to_spin.connect("changed", self.on_to_time_changed) - self.on_redraw() - - - def on_to_time_changed(self, widget): - from_time = self.from_spin.get_value() - to_time = self.to_spin.get_value() - if from_time > to_time: - self.from_spin.disconnect(self.from_spin_changed_id) - self.from_spin.set_value(to_time) - self.from_spin_changed_id = self.from_spin.connect("changed", self.on_from_time_changed) - self.on_redraw() - - - def on_cursor_spin_changed(self, widget): - self.diagram_area.set_time(self.expand_time(self.cursor_spin.get_value(), self.cursor_combo.get_active())) - self.on_redraw() - - def on_combo_changed(self, widget): - self.scale_unit_combo.disconnect(self.scale_unit_combo_changed_id) - self.from_combo.disconnect(self.from_combo_changed_id) - self.to_combo.disconnect(self.to_combo_changed_id) - self.cursor_combo.disconnect(self.cursor_combo_changed_id) - self.scale_unit_combo.set_active(widget.get_active()) - self.from_combo.set_active(widget.get_active()) - self.to_combo.set_active(widget.get_active()) - self.cursor_combo.set_active(widget.get_active()) - self.scale_unit_combo_changed_id = self.scale_unit_combo.connect("changed", self.on_combo_changed) - self.from_combo_changed_id = self.from_combo.connect("changed", self.on_combo_changed) - self.to_combo_changed_id = self.to_combo.connect("changed", self.on_combo_changed) - self.cursor_combo_changed_id = self.cursor_combo.connect("changed", self.on_combo_changed) - self.diagram_area.set_time(self.expand_time(self.cursor_spin.get_value(), self.cursor_combo.get_active())) - self.on_redraw() - - def on_redraw(self, *args): - if self.scale_unit_combo.get_active() == 0: - new_scale = float(self.scale_combo.get_child().get_text()) * 1000000000 - elif self.scale_unit_combo.get_active() == 1: - new_scale = float(self.scale_combo.get_child().get_text()) * 1000000 - else: - new_scale = float(self.scale_combo.get_child().get_text()) * 1000 - - new_diagram_unit = self.scale_unit_combo.get_active() - new_start_time = self.expand_time(self.from_spin.get_value(), self.from_combo.get_active()) - new_end_time = self.expand_time(self.to_spin.get_value(), self.to_combo.get_active()) - - diagram_width = int((new_end_time - new_start_time) * new_scale) - - if diagram_width > 5000: - dialog = Gtk.MessageDialog(transient_for=self, message_type=Gtk.MessageType.ERROR, button_type=Gtk.ButtonsType.OK) - dialog.set_modal(True) - dialog.set_markup(_("Can't create timing diagram!")) - dialog.get_message_area().append(Gtk.Label(label=_("The width of the timing diagram will be too wide. (> 5000)"))) - dialog.present() - - else: - self.diagram_area.scale = new_scale - self.diagram_area.diagram_unit = new_diagram_unit - self.diagram_area.start_time = new_start_time - self.diagram_area.end_time = new_end_time - self.diagram_area.createDiagram() - - def on_save_image_clicked(self, widget): - save_timing_diagram_as_image(self.diagram_area, self) diff --git a/ggate/Utils.py b/ggate/Utils.py index 4efb6d5..5fcbe81 100644 --- a/ggate/Utils.py +++ b/ggate/Utils.py @@ -1,10 +1,13 @@ # -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- -from collections import Counter +import logging from gi.repository import Pango, PangoCairo from ggate.const import definitions as const import cairo +# shared application logger +logger = logging.getLogger("ggate") + def number_in_range(value, min_value, max_value): return min_value <= value <= max_value @@ -74,9 +77,6 @@ def decode_text(text): t_text += c return t_text -def get_duplicate_points(points, instance = 2): - return [point for point, count in Counter(points).items() if count == instance] - def stack_with_tphl_lh(time, current, stacks, newdatas, tp_hl, tp_lh): for i, dat in enumerate(current): if stacks[i] and newdatas[i] == stacks[i][-1][1]: diff --git a/ggate/__init__.py b/ggate/__init__.py index dc5e3f7..d89f6e7 100644 --- a/ggate/__init__.py +++ b/ggate/__init__.py @@ -1 +1 @@ -__version__ = '4.0.0' \ No newline at end of file +__version__ = "5.0.0" \ No newline at end of file diff --git a/ggate/config.py b/ggate/config.py index 392aa84..bfd4823 100644 --- a/ggate/config.py +++ b/ggate/config.py @@ -1,8 +1,20 @@ -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- +from os import path from packaging.version import Version -DATADIR = '/'.join(__file__.split("/")[:-1]) + '/' -VERSION = "4.0.0" +# Determine if running from source checkout (run.py is adjacent to the parent directory of this file) +_parent_dir = path.dirname(path.dirname(path.abspath(__file__))) +RUNNING_FROM_SOURCE = path.isfile(path.join(_parent_dir, "run.py")) + +if RUNNING_FROM_SOURCE: + DATADIR = path.join(_parent_dir, "data") + ICONDIR = path.join(DATADIR, "images") + APP_PREFIX = "org.astralco.ggate.Dev" + VERSION = "5.0.0" +else: + DATADIR = "@pkgdatadir@" + ICONDIR = path.join(DATADIR, "icons") + APP_PREFIX = "@APP_PREFIX@" + VERSION = "@VERSION@" compatibility = { "version": VERSION, @@ -10,9 +22,9 @@ } def is_compatible(target: str) -> int: - print(target, compatibility["required"]) if Version(target) < Version(compatibility["required"]): return -1 # Lower version needed elif Version(target) > Version(compatibility["version"]): return 1 # Higher version needed return 0 + diff --git a/ggate/const.py b/ggate/const.py index 7c595f0..acab9ef 100644 --- a/ggate/const.py +++ b/ggate/const.py @@ -3,12 +3,27 @@ import os import gettext from gettext import gettext as _ +from gi.repository import Gtk gettext.textdomain("ggate") class DefinitionError(TypeError): pass +_licence_string = """GGate is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GGate is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see .""" + + class Definitions(object): def __setattr__(self, name, value): if name in self.__dict__: @@ -67,29 +82,24 @@ def __setattr__(self, name, value): psfile_text = _("Post Script (*.ps)") anyfile_text = _("All files") + developer_name = "Ekure Edem" + developer_email = "ekureedem480@gmail.com" + config_path = os.path.join(os.path.expanduser("~"), ".config/ggate") app_name = _("GGate") - description = _( - "GGate is a logic circuit simulator developed with GTK+ and Python.") - copyright = "Copyright © 2012 - 2022 Koichi Akabe" - developer = ["Koichi Akabe ", - 'Ekure Edem '] - website = "https://launchpad.net/ggate" + description = _("GGate is a logic circuit simulator developed with GTK+ and Python.") + copyright = "Copyright © 2022 - 2025 Ekure Edem" + developer = [ + "Koichi Akabe ", + f"{developer_name} <{developer_email}>" + ] + + website = "https://github.com/astraldev/GGate" help = "help:ggate" - license = """GGate is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -GGate is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program. If not, see .""" + license = Gtk.License.GPL_3_0 devel_bug = "https://github.com/astraldev/GGate/issues/new" + licence_string = _licence_string definitions = Definitions \ No newline at end of file diff --git a/gnd.svg b/gnd.svg deleted file mode 100644 index 7d5a1e5..0000000 --- a/gnd.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..cddf286 --- /dev/null +++ b/meson.build @@ -0,0 +1,140 @@ +project( + 'ggate', + version: '5.0.0', + meson_version: '>= 1.4.0', + default_options: [ + 'warning_level=2', + 'werror=false', + ], +) + +dependency('pygobject-3.0') +dependency('gtk4', version: '>= 4.16.0') +dependency('libadwaita-1', version: '>= 1.6.8') + +gnome = import('gnome') +python = import('python') + +py_installation = python.find_installation('python3', + modules: ['gi', 'cairo', 'packaging', 'igraph', 'shapely'] +) + +py_installation.dependency() + +pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() + +profile = get_option('profile') +if profile == 'development' + app_id = 'org.astralco.ggate.Dev' + gresource_prefix = '/org/astralco/ggate/Dev' +elif profile == 'release' + app_id = 'org.astralco.ggate' + gresource_prefix = '/org/astralco/ggate' +endif + +conf = configuration_data() +conf.set('PYTHON', python.find_installation('python3').full_path()) +conf.set('VERSION', meson.project_version()) +conf.set('bindir', get_option('bindir')) +conf.set('pkgdatadir', pkgdatadir) +conf.set('APP_PREFIX', app_id) + +# Configure and install config.py to python installation directory +configure_file( + input: 'ggate/config.py', + output: 'config.py', + configuration: conf, + install: true, + install_dir: py_installation.get_install_dir() / 'ggate' +) + +# Configure and install desktop file +desktop_conf = configuration_data() +desktop_conf.set('APP_ID', app_id) +configure_file( + input: 'data/org.astralco.ggate.desktop.in', + output: app_id + '.desktop', + configuration: desktop_conf, + install: true, + install_dir: get_option('datadir') / 'applications' +) + +# Configure and install AppStream metadata +metainfo_conf = configuration_data() +metainfo_conf.set('APP_ID', app_id) +configure_file( + input: 'data/org.astralco.ggate.metainfo.xml.in', + output: app_id + '.metainfo.xml', + configuration: metainfo_conf, + install: true, + install_dir: get_option('datadir') / 'metainfo' +) + +# Install MIME package description +install_data('data/ggate.xml', + install_dir: get_option('datadir') / 'mime' / 'packages' +) + +# Install scalable application icon (renamed to app_id) +install_data('data/images/scalable/ggate.svg', + rename: app_id + '.svg', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps' +) + +# Install sized application icons (renamed to app_id) +icon_sizes = ['16', '24', '32', '48', '64', '96', '128', '256', '512'] +foreach size : icon_sizes + install_data('data/images/apps' / (size + 'x' + size) / 'ggate.png', + rename: app_id + '.png', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / (size + 'x' + size) / 'apps' + ) +endforeach + +# Install MIME type icons (using standard name text-glc.png) +foreach size : icon_sizes + install_data('data/images/mime' / (size + 'x' + size) / 'text-glc.png', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / (size + 'x' + size) / 'mimetypes' + ) + if size in ['16', '24', '32', '48', '256'] + install_data('data/images/mime' / (size + 'x' + size) / 'text-glc.png', + install_dir: get_option('datadir') / 'icons' / 'Yaru' / (size + 'x' + size) / 'mimetypes' + ) + endif +endforeach + +# Install help manual +install_subdir('data/help/C', + install_dir: get_option('datadir') / 'help' / 'ggate', + strip_directory: false +) + +subdir('bin') +subdir('data/images') + +exclude_pycache_dirs = [] +foreach d : run_command( + find_program('sh'), + files('build-aux/meson-exclude-pycache.sh'), + 'ggate', + check: true +).stdout().strip().split('\n') + if d != '' + exclude_pycache_dirs += d + endif +endforeach + +# Install the entire ggate package recursively, excluding .pyc and unconfigured config.py +install_subdir('ggate', + install_dir: py_installation.get_install_dir(), + exclude_directories: exclude_pycache_dirs, + exclude_files: ['*.pyc', 'config.py'] +) + +# Update desktop/icon caches only when not building inside a sandbox (Flatpak/Snap) +if not get_option('prefix').startswith('/app') and not get_option('prefix').startswith('/snap') + gnome.post_install( + glib_compile_schemas: true, + gtk_update_icon_cache: true, + update_desktop_database: find_program('update-desktop-database', required: false).found(), + ) +endif \ No newline at end of file diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..009b4a6 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,10 @@ +option( + 'profile', + type: 'combo', + choices: [ + 'release', + 'development', + ], + value: 'development', + description: 'Build profile: development uses the .Dev app id and dev resources' +) diff --git a/poetry.lock b/poetry.lock index baf0324..9f054d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,52 +1,75 @@ # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "igraph" -version = "0.11.8" +version = "0.11.9" description = "High performance graph data structures and algorithms" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "igraph-0.11.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a7aa8e65e7b9ddfe66f9473ce93863f40fccac26b24dc3f56e63159641c9946"}, - {file = "igraph-0.11.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9e953e1c5e9c5712a48df5cea93963be84aa26618cdae341b4a6b07761f56a45"}, - {file = "igraph-0.11.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9baf699fdd10491e9a0842e546e630165c49c78d21ac4aaa9fb434ab4a817458"}, - {file = "igraph-0.11.8-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:434e35d935675caddac3221863b43a02085c7f66030eda542f0dd9fc36e1f8ff"}, - {file = "igraph-0.11.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:745e5d7aebca7e9c16f882041718c8ceeb404a5c7201cb8685f57b0e19ebe24d"}, - {file = "igraph-0.11.8-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24c97ce9f40a358a8d6ff9c27fab0e4617068aaeeb5448247308c67519b91fa2"}, - {file = "igraph-0.11.8-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c89ab68f076528736d4ed56a01983d7fd433f50b08c58bee7ded2326a4eacda1"}, - {file = "igraph-0.11.8-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d964fc35d65ce67b121e4dcfd7d3479fb3eeb232b6a346a217e397c7d5c5f124"}, - {file = "igraph-0.11.8-cp38-cp38-win32.whl", hash = "sha256:511b036c876fdbfc919a6a4c72b0335fd2a6a3249e5e4312b660390213875afb"}, - {file = "igraph-0.11.8-cp38-cp38-win_amd64.whl", hash = "sha256:417b8375c1c9adbb431f7481a6cae6f9e440db81d8d4ee6fa5d2c2983148930a"}, - {file = "igraph-0.11.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:92c47ceb9f4c95ff7461cd94eaec55e901dbc59f6e51f4244d2dd064f31c5491"}, - {file = "igraph-0.11.8-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3a2afdb046b602fea71ca18aff6c72165de5002ec38d0dcf1275e34ecd0d9d02"}, - {file = "igraph-0.11.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7870fb72fd9e9218940262671fb79baf281ef066aa1cd35adc092ce9ad39a038"}, - {file = "igraph-0.11.8-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8108138ad605714761bc6d526feab54074904a99c47dcaabc269ed7b45e7650"}, - {file = "igraph-0.11.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2e3abe641622cd9c95eb152c97caa22e68257a524cc1851a099b066f8f5c5f3"}, - {file = "igraph-0.11.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b546eaa9461473a65bb56a51672c6aeb898b737d5e86c3efa1b1bf520ee4b031"}, - {file = "igraph-0.11.8-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:178ef859135ac5075a7159e6826a546b7340cf45a01a928c2a0c24c32e3dfa63"}, - {file = "igraph-0.11.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a68ae7b6324e9c4cb1d04ce75b6e0f67974433fc7667895f1e25bf4f4c6fd530"}, - {file = "igraph-0.11.8-cp39-abi3-win32.whl", hash = "sha256:cc93d2f97f93bf30c2027c31e9e1aa088a3c60cdfeb6b33e0259e4b40b4c5597"}, - {file = "igraph-0.11.8-cp39-abi3-win_amd64.whl", hash = "sha256:248831a6162130f16909c1f776cc246b48f692339ea4baca489cad4ed8dc0e13"}, - {file = "igraph-0.11.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0a8cad10406fba28c4b0199dfb491bcfdf1cdd3a56feeb52bb3b1cd724d04b4"}, - {file = "igraph-0.11.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1c11127a58ac2fc8062dac9f20ef612aff1b09f5f9d3e90800c4817229d0a0bc"}, - {file = "igraph-0.11.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17d79bfb170b8c195fe6bacfa1c906817e8e812417c7e6a8e15f0bcf9b0775a8"}, - {file = "igraph-0.11.8-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9de09a26d7aae4d83338497cfd2d107d3ee3a2e9335b9db4b6c73a089e8cf47"}, - {file = "igraph-0.11.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6559e2c925ed2ac608103adfd1dec9ccb9a04ddc7ad1d9d2a7db46dda6b1955"}, - {file = "igraph-0.11.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6c17658b367be4f725a253678bfe799d9fe4d4e5c01ad82449cf8f2e9917937c"}, - {file = "igraph-0.11.8-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d4971b4fcb005ed72f630a5f4c9bb80f10153471fe30846810f63beb3e282a19"}, - {file = "igraph-0.11.8-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d7b1eaa3563c1e2dda940c1f154fefe9b3b257da8e8251af443cdc69a039480"}, - {file = "igraph-0.11.8-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35438d6d69a73288949a80f1eb84597e783486cd71a5cdf5862c0db7a7cbd5c5"}, - {file = "igraph-0.11.8-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09c609c5d6a844582a10085c18c1c15d14b2f9fd3be59fed3feaa4be091d671f"}, - {file = "igraph-0.11.8-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b836baa221027f1781ebcff05f1b23339a51a63eb70948ebaba5641efc060a"}, - {file = "igraph-0.11.8-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:caf4a178f7fb7890195c9fb358dbef0ed4a4f5323f529ea14a0f64da4c05f564"}, - {file = "igraph-0.11.8-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e702a436935d3e127f6affff397ebbab48b522434bd8d6f15cfb1ab5d940e7d5"}, - {file = "igraph-0.11.8-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bcad4d052785fe9b076f5aca6e870e2fae35373b09867477adc7307f2692a36"}, - {file = "igraph-0.11.8-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d420cd48353e7c138bc39a118c3a01dc41aeba38486cca1524a960a755016171"}, - {file = "igraph-0.11.8-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ae9486a52da72d2ab634b92e17a969dc4e8e83303384329b903830ad67315e5"}, - {file = "igraph-0.11.8-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e36a4f8a40bb4ffc8aa08c1cfe6fa3dfa78393cf65165bd9d59e6ac24a2468"}, - {file = "igraph-0.11.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f4048b843be54a77bc7206ce8c58825a9b1b42748c1713699034dc4f7df36f73"}, - {file = "igraph-0.11.8.tar.gz", hash = "sha256:d7dc1404567ba3b0ea1bf8b5fa6e101617915c8ad11ea5a9f925a40bf4adad7d"}, + {file = "igraph-0.11.9-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ef30a8eb6329a71211652223cad900dc42bc7fdb44d9e942e991181232906ac2"}, + {file = "igraph-0.11.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:4b3224b2b74e9dfac1271dc6f2e1061d13492f91198d05e1b8b696b994e5e269"}, + {file = "igraph-0.11.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adf7d7200c2e11a3b1122786f77cee96072c593fd62794aadb5ce546a24fa791"}, + {file = "igraph-0.11.9-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78a7f3a490f3f6a8aab99948e3e62ae81fc1e8a8aa07e326b09f4e570c042e79"}, + {file = "igraph-0.11.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:773201c9eafef668be11d8966cf2d7114d34757cd9cfdbd8c190fefcd341220b"}, + {file = "igraph-0.11.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9bc6fb4316bc79bd0d800dd0186921ef62da971be147861872be90242acbae7d"}, + {file = "igraph-0.11.9-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:e45d03bfb931b73f323b531fc0d87235ac96c41a64363b243677034576cf411b"}, + {file = "igraph-0.11.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:157b4a836628ca55c6422098bf34336006c1d517fc86fa0e89af3a233e3baa30"}, + {file = "igraph-0.11.9-cp39-abi3-win32.whl", hash = "sha256:1fd67a0771b8ce70bef361557bdeb6ca7a1012f9fb8368eba86967deeb99a110"}, + {file = "igraph-0.11.9-cp39-abi3-win_amd64.whl", hash = "sha256:09c7d49c7759e058bf2526bbac54dd1f9e0725ff64352f01545db59c09de88cf"}, + {file = "igraph-0.11.9-cp39-abi3-win_arm64.whl", hash = "sha256:8acca4f2463f4de572471cca2d46bb3ef5b3082bc125b9ec30e8032b177951df"}, + {file = "igraph-0.11.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:63f5953619b308b0afbb3ceb5c7b7ab3ee847eca348dfca7d7eb93290568ce02"}, + {file = "igraph-0.11.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2c4384d1ea1fb071c1b367069783dc195919596d9bb73fef1eddf97cfb5613b"}, + {file = "igraph-0.11.9-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02e2e6747d3c70fcb539bc29b80377d63859f30db8a9b4bc6f440d317c07a47b"}, + {file = "igraph-0.11.9-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f74dd13b36fd5a831632be0e8f0b3b1519067c479a820f54168e70ac0b71b89d"}, + {file = "igraph-0.11.9-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb5097c402e82a8bb074ab9df2e45e0c9bcd76bb36a3a839e7cd4d71143bbba"}, + {file = "igraph-0.11.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b0561914fc415dc2fa4194c39585336dde42c3cf5fafd1b404f5e847d055fa17"}, + {file = "igraph-0.11.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a38e20a45499ae258c36ff27a32e9afeac777bac0c94c3511af75503f37523f"}, + {file = "igraph-0.11.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1bbf2b7a928441184ec9fc1f771ddf61bcd6a3f812a8861cab465c5c985ccc6c"}, + {file = "igraph-0.11.9-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb6db6056f90364436f32439b3fc23947d469de0894240ed94dfdecc2eb3c89"}, + {file = "igraph-0.11.9-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4c6ff8fea88f4b7f6202f6ff939853e09c383b2a35c58aa05f374b66fe46c7c"}, + {file = "igraph-0.11.9-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9911a7c5b256c0e7d50f958bbabba47a5eeddde67b47271a05e0850de129e2fc"}, + {file = "igraph-0.11.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f35694100691bf8ef0c370615d87bcf1d6c0f15e356269c6357f8f78a9f1acea"}, + {file = "igraph-0.11.9-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:03114d751b2bc6171259e52d773682b1d771a4b31acc345574f187c6c12ea943"}, + {file = "igraph-0.11.9-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dc836e4fddb040eae297bdf56cf0158279f534519a27b35a3c4112bfd2b5244"}, + {file = "igraph-0.11.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fe5f5a55ea211219278da3b74ecbd86c57fedadead51481141853b7594b33f0"}, + {file = "igraph-0.11.9-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06434f9d31300a7b7174c922e6940ffa4ca5db46288ca654a3c923729f5c4ab0"}, + {file = "igraph-0.11.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:461bcc31f1ffec4f60881c4cc79ed3f732c7c98b405f97ff3f84d748452a279d"}, + {file = "igraph-0.11.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b948b2e302585c5ed23ef4a7e8caa8ca4cc946106ff8d9b197d16c49d929b777"}, + {file = "igraph-0.11.9.tar.gz", hash = "sha256:c57ce44873abcfcfd1d61d7d261e416d352186958e7b5d299cf244efa6757816"}, ] [package.dependencies] @@ -60,51 +83,65 @@ plotly = ["plotly (>=5.3.0)"] plotting = ["cairocffi (>=1.2.0)"] test = ["Pillow (>=9) ; platform_python_implementation != \"PyPy\"", "cairocffi (>=1.2.0)", "matplotlib (>=3.6.0) ; platform_python_implementation != \"PyPy\"", "networkx (>=2.5)", "numpy (>=1.19.0) ; platform_python_implementation != \"PyPy\"", "pandas (>=1.1.0) ; platform_python_implementation != \"PyPy\"", "plotly (>=5.3.0)", "pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)", "scipy (>=1.5.0) ; platform_python_implementation != \"PyPy\""] test-musl = ["cairocffi (>=1.2.0)", "networkx (>=2.5)", "pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)"] +test-win-arm64 = ["cairocffi (>=1.2.0)", "networkx (>=2.5)", "pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, - {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, - {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, - {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, - {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, - {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, - {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, - {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, - {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, - {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, - {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, - {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, - {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, - {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, + {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, + {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, + {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, + {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, + {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, + {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, + {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, + {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, + {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, + {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, + {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, + {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, + {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, + {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, + {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, + {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, + {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, + {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, + {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, + {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, + {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, + {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, + {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, + {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, + {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, + {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, + {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, + {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, + {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, + {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, + {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, + {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, ] [package.dependencies] mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" @@ -117,104 +154,164 @@ reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] name = "numpy" -version = "2.0.2" +version = "2.2.6" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, + {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, + {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, + {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, + {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, + {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, + {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, + {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, + {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, + {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, + {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, + {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "pycairo" -version = "1.27.0" +version = "1.28.0" description = "Python interface for cairo" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pycairo-1.27.0-cp310-cp310-win32.whl", hash = "sha256:e20f431244634cf244ab6b4c3a2e540e65746eed1324573cf291981c3e65fc05"}, - {file = "pycairo-1.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:03bf570e3919901572987bc69237b648fe0de242439980be3e606b396e3318c9"}, - {file = "pycairo-1.27.0-cp311-cp311-win32.whl", hash = "sha256:9a9b79f92a434dae65c34c830bb9abdbd92654195e73d52663cbe45af1ad14b2"}, - {file = "pycairo-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:d40a6d80b15dacb3672dc454df4bc4ab3988c6b3f36353b24a255dc59a1c8aea"}, - {file = "pycairo-1.27.0-cp312-cp312-win32.whl", hash = "sha256:e2239b9bb6c05edae5f3be97128e85147a155465e644f4d98ea0ceac7afc04ee"}, - {file = "pycairo-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:27cb4d3a80e3b9990af552818515a8e466e0317063a6e61585533f1a86f1b7d5"}, - {file = "pycairo-1.27.0-cp313-cp313-win32.whl", hash = "sha256:01505c138a313df2469f812405963532fc2511fb9bca9bdc8e0ab94c55d1ced8"}, - {file = "pycairo-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:b0349d744c068b6644ae23da6ada111c8a8a7e323b56cbce3707cba5bdb474cc"}, - {file = "pycairo-1.27.0-cp39-cp39-win32.whl", hash = "sha256:f9ca8430751f1fdcd3f072377560c9e15608b9a42d61375469db853566993c9b"}, - {file = "pycairo-1.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b1321652a6e27c4de3069709b1cae22aed2707fd8c5e889c04a95669228af2a"}, - {file = "pycairo-1.27.0.tar.gz", hash = "sha256:5cb21e7a00a2afcafea7f14390235be33497a2cce53a98a19389492a60628430"}, + {file = "pycairo-1.28.0-cp310-cp310-win32.whl", hash = "sha256:53e6dbc98456f789965dad49ef89ce2c62f9a10fc96c8d084e14da0ffb73d8a6"}, + {file = "pycairo-1.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:c8ab91a75025f984bc327ada335c787efb61c929ea0512063793cb36cee503d4"}, + {file = "pycairo-1.28.0-cp310-cp310-win_arm64.whl", hash = "sha256:e955328c1a5147bf71ee94e206413ce15e12630296a79788fcd246c80e5337b8"}, + {file = "pycairo-1.28.0-cp311-cp311-win32.whl", hash = "sha256:0fee15f5d72b13ba5fd065860312493dc1bca6ff2dce200ee9d704e11c94e60a"}, + {file = "pycairo-1.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:6339979bfec8b58a06476094a9a5c104bd5a99932ddaff16ca0d9203d2f4482c"}, + {file = "pycairo-1.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6ae15392e28ebfc0b35d8dc05d395d3b6be4bad9ad4caecf0fa12c8e7150225"}, + {file = "pycairo-1.28.0-cp312-cp312-win32.whl", hash = "sha256:c00cfbb7f30eb7ca1d48886712932e2d91e8835a8496f4e423878296ceba573e"}, + {file = "pycairo-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:d50d190f5033992b55050b9f337ee42a45c3568445d5e5d7987bab96c278d8a6"}, + {file = "pycairo-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:957e0340ee1c279d197d4f7cfa96f6d8b48e453eec711fca999748d752468ff4"}, + {file = "pycairo-1.28.0-cp313-cp313-win32.whl", hash = "sha256:d13352429d8a08a1cb3607767d23d2fb32e4c4f9faa642155383980ec1478c24"}, + {file = "pycairo-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:082aef6b3a9dcc328fa648d38ed6b0a31c863e903ead57dd184b2e5f86790140"}, + {file = "pycairo-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:026afd53b75291917a7412d9fe46dcfbaa0c028febd46ff1132d44a53ac2c8b6"}, + {file = "pycairo-1.28.0-cp314-cp314-win32.whl", hash = "sha256:d0ab30585f536101ad6f09052fc3895e2a437ba57531ea07223d0e076248025d"}, + {file = "pycairo-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:94f2ed204999ab95a0671a0fa948ffbb9f3d6fb8731fe787917f6d022d9c1c0f"}, + {file = "pycairo-1.28.0-cp39-cp39-win32.whl", hash = "sha256:3ed16d48b8a79cc584cb1cb0ad62dfb265f2dda6d6a19ef5aab181693e19c83c"}, + {file = "pycairo-1.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:da0d1e6d4842eed4d52779222c6e43d254244a486ca9fdab14e30042fd5bdf28"}, + {file = "pycairo-1.28.0-cp39-cp39-win_arm64.whl", hash = "sha256:458877513eb2125513122e8aa9c938630e94bb0574f94f4fb5ab55eb23d6e9ac"}, + {file = "pycairo-1.28.0.tar.gz", hash = "sha256:26ec5c6126781eb167089a123919f87baa2740da2cca9098be8b3a6b91cc5fbc"}, ] +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pygobject" version = "3.52.3" @@ -243,20 +340,44 @@ files = [ [package.extras] dev = ["PyGObject", "black (>=25.1.0)", "codespell (>=2.4.1)", "isort (>=6.0.1)", "pre-commit", "ruff (>=0.9.10)"] +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-igraph" -version = "0.11.8" +version = "0.11.9" description = "High performance graph data structures and algorithms (legacy package)" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "python_igraph-0.11.8-py3-none-any.whl", hash = "sha256:f917c7ed8fbaa08d31bd47e9ca8f68305fc53b294c304190f285392eb3f2dc13"}, - {file = "python_igraph-0.11.8.tar.gz", hash = "sha256:235550fedd3f649e8350bc97cd9220c9751a9fe4b912360d234fe0ef84e82d0b"}, + {file = "python_igraph-0.11.9-py3-none-any.whl", hash = "sha256:9154606132dac48071edf5bc27f5b54cb316db09686ad8cffce078943733de29"}, + {file = "python_igraph-0.11.9.tar.gz", hash = "sha256:51ad8bfba7777ff110cd4f47eb9efeaf092e4edf3167153b05156dbe55dbf90c"}, ] [package.dependencies] -igraph = "0.11.8" +igraph = "0.11.9" [package.extras] cairo = ["cairocffi (>=1.2.0)"] @@ -266,90 +387,89 @@ plotting = ["cairocffi (>=1.2.0)"] [[package]] name = "ruff" -version = "0.11.2" +version = "0.11.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477"}, - {file = "ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272"}, - {file = "ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147"}, - {file = "ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630"}, - {file = "ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f"}, - {file = "ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc"}, - {file = "ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080"}, - {file = "ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4"}, - {file = "ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94"}, + {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, + {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, + {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, + {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, + {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, + {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, + {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, ] [[package]] name = "shapely" -version = "2.0.7" +version = "2.1.1" description = "Manipulation and analysis of geometric objects" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "shapely-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:33fb10e50b16113714ae40adccf7670379e9ccf5b7a41d0002046ba2b8f0f691"}, - {file = "shapely-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f44eda8bd7a4bccb0f281264b34bf3518d8c4c9a8ffe69a1a05dabf6e8461147"}, - {file = "shapely-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6c50cd879831955ac47af9c907ce0310245f9d162e298703f82e1785e38c98"}, - {file = "shapely-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04a65d882456e13c8b417562c36324c0cd1e5915f3c18ad516bb32ee3f5fc895"}, - {file = "shapely-2.0.7-cp310-cp310-win32.whl", hash = "sha256:7e97104d28e60b69f9b6a957c4d3a2a893b27525bc1fc96b47b3ccef46726bf2"}, - {file = "shapely-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:35524cc8d40ee4752520819f9894b9f28ba339a42d4922e92c99b148bed3be39"}, - {file = "shapely-2.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5cf23400cb25deccf48c56a7cdda8197ae66c0e9097fcdd122ac2007e320bc34"}, - {file = "shapely-2.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f1da01c04527f7da59ee3755d8ee112cd8967c15fab9e43bba936b81e2a013"}, - {file = "shapely-2.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f623b64bb219d62014781120f47499a7adc30cf7787e24b659e56651ceebcb0"}, - {file = "shapely-2.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6d95703efaa64aaabf278ced641b888fc23d9c6dd71f8215091afd8a26a66e3"}, - {file = "shapely-2.0.7-cp311-cp311-win32.whl", hash = "sha256:2f6e4759cf680a0f00a54234902415f2fa5fe02f6b05546c662654001f0793a2"}, - {file = "shapely-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:b52f3ab845d32dfd20afba86675c91919a622f4627182daec64974db9b0b4608"}, - {file = "shapely-2.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4c2b9859424facbafa54f4a19b625a752ff958ab49e01bc695f254f7db1835fa"}, - {file = "shapely-2.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5aed1c6764f51011d69a679fdf6b57e691371ae49ebe28c3edb5486537ffbd51"}, - {file = "shapely-2.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c9ae8cf443187d784d57202199bf9fd2d4bb7d5521fe8926ba40db1bc33e8e"}, - {file = "shapely-2.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9469f49ff873ef566864cb3516091881f217b5d231c8164f7883990eec88b73"}, - {file = "shapely-2.0.7-cp312-cp312-win32.whl", hash = "sha256:6bca5095e86be9d4ef3cb52d56bdd66df63ff111d580855cb8546f06c3c907cd"}, - {file = "shapely-2.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f86e2c0259fe598c4532acfcf638c1f520fa77c1275912bbc958faecbf00b108"}, - {file = "shapely-2.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a0c09e3e02f948631c7763b4fd3dd175bc45303a0ae04b000856dedebefe13cb"}, - {file = "shapely-2.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06ff6020949b44baa8fc2e5e57e0f3d09486cd5c33b47d669f847c54136e7027"}, - {file = "shapely-2.0.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6dbf096f961ca6bec5640e22e65ccdec11e676344e8157fe7d636e7904fd36"}, - {file = "shapely-2.0.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adeddfb1e22c20548e840403e5e0b3d9dc3daf66f05fa59f1fcf5b5f664f0e98"}, - {file = "shapely-2.0.7-cp313-cp313-win32.whl", hash = "sha256:a7f04691ce1c7ed974c2f8b34a1fe4c3c5dfe33128eae886aa32d730f1ec1913"}, - {file = "shapely-2.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:aaaf5f7e6cc234c1793f2a2760da464b604584fb58c6b6d7d94144fd2692d67e"}, - {file = "shapely-2.0.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19cbc8808efe87a71150e785b71d8a0e614751464e21fb679d97e274eca7bd43"}, - {file = "shapely-2.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc19b78cc966db195024d8011649b4e22812f805dd49264323980715ab80accc"}, - {file = "shapely-2.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd37d65519b3f8ed8976fa4302a2827cbb96e0a461a2e504db583b08a22f0b98"}, - {file = "shapely-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:25085a30a2462cee4e850a6e3fb37431cbbe4ad51cbcc163af0cea1eaa9eb96d"}, - {file = "shapely-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:1a2e03277128e62f9a49a58eb7eb813fa9b343925fca5e7d631d50f4c0e8e0b8"}, - {file = "shapely-2.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e1c4f1071fe9c09af077a69b6c75f17feb473caeea0c3579b3e94834efcbdc36"}, - {file = "shapely-2.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3697bd078b4459f5a1781015854ef5ea5d824dbf95282d0b60bfad6ff83ec8dc"}, - {file = "shapely-2.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e9fed9a7d6451979d914cb6ebbb218b4b4e77c0d50da23e23d8327948662611"}, - {file = "shapely-2.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2934834c7f417aeb7cba3b0d9b4441a76ebcecf9ea6e80b455c33c7c62d96a24"}, - {file = "shapely-2.0.7-cp38-cp38-win32.whl", hash = "sha256:2e4a1749ad64bc6e7668c8f2f9479029f079991f4ae3cb9e6b25440e35a4b532"}, - {file = "shapely-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8ae5cb6b645ac3fba34ad84b32fbdccb2ab321facb461954925bde807a0d3b74"}, - {file = "shapely-2.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4abeb44b3b946236e4e1a1b3d2a0987fb4d8a63bfb3fdefb8a19d142b72001e5"}, - {file = "shapely-2.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0e75d9124b73e06a42bf1615ad3d7d805f66871aa94538c3a9b7871d620013"}, - {file = "shapely-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7977d8a39c4cf0e06247cd2dca695ad4e020b81981d4c82152c996346cf1094b"}, - {file = "shapely-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0145387565fcf8f7c028b073c802956431308da933ef41d08b1693de49990d27"}, - {file = "shapely-2.0.7-cp39-cp39-win32.whl", hash = "sha256:98697c842d5c221408ba8aa573d4f49caef4831e9bc6b6e785ce38aca42d1999"}, - {file = "shapely-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:a3fb7fbae257e1b042f440289ee7235d03f433ea880e73e687f108d044b24db5"}, - {file = "shapely-2.0.7.tar.gz", hash = "sha256:28fe2997aab9a9dc026dc6a355d04e85841546b2a5d232ed953e3321ab958ee5"}, + {file = "shapely-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8ccc872a632acb7bdcb69e5e78df27213f7efd195882668ffba5405497337c6"}, + {file = "shapely-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f24f2ecda1e6c091da64bcbef8dd121380948074875bd1b247b3d17e99407099"}, + {file = "shapely-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45112a5be0b745b49e50f8829ce490eb67fefb0cea8d4f8ac5764bfedaa83d2d"}, + {file = "shapely-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c10ce6f11904d65e9bbb3e41e774903c944e20b3f0b282559885302f52f224a"}, + {file = "shapely-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:61168010dfe4e45f956ffbbaf080c88afce199ea81eb1f0ac43230065df320bd"}, + {file = "shapely-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cacf067cdff741cd5c56a21c52f54ece4e4dad9d311130493a791997da4a886b"}, + {file = "shapely-2.1.1-cp310-cp310-win32.whl", hash = "sha256:23b8772c3b815e7790fb2eab75a0b3951f435bc0fce7bb146cb064f17d35ab4f"}, + {file = "shapely-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c7b2b6143abf4fa77851cef8ef690e03feade9a0d48acd6dc41d9e0e78d7ca6"}, + {file = "shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7"}, + {file = "shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea"}, + {file = "shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7"}, + {file = "shapely-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586f6aee1edec04e16227517a866df3e9a2e43c1f635efc32978bb3dc9c63753"}, + {file = "shapely-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9878b9e37ad26c72aada8de0c9cfe418d9e2ff36992a1693b7f65a075b28647"}, + {file = "shapely-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9a531c48f289ba355e37b134e98e28c557ff13965d4653a5228d0f42a09aed0"}, + {file = "shapely-2.1.1-cp311-cp311-win32.whl", hash = "sha256:4866de2673a971820c75c0167b1f1cd8fb76f2d641101c23d3ca021ad0449bab"}, + {file = "shapely-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:20a9d79958b3d6c70d8a886b250047ea32ff40489d7abb47d01498c704557a93"}, + {file = "shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43"}, + {file = "shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad"}, + {file = "shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9"}, + {file = "shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef"}, + {file = "shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1"}, + {file = "shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d"}, + {file = "shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8"}, + {file = "shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a"}, + {file = "shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3004a644d9e89e26c20286d5fdc10f41b1744c48ce910bd1867fdff963fe6c48"}, + {file = "shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1415146fa12d80a47d13cfad5310b3c8b9c2aa8c14a0c845c9d3d75e77cb54f6"}, + {file = "shapely-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21fcab88b7520820ec16d09d6bea68652ca13993c84dffc6129dc3607c95594c"}, + {file = "shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ce6a5cc52c974b291237a96c08c5592e50f066871704fb5b12be2639d9026a"}, + {file = "shapely-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:04e4c12a45a1d70aeb266618d8cf81a2de9c4df511b63e105b90bfdfb52146de"}, + {file = "shapely-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ca74d851ca5264aae16c2b47e96735579686cb69fa93c4078070a0ec845b8d8"}, + {file = "shapely-2.1.1-cp313-cp313-win32.whl", hash = "sha256:fd9130501bf42ffb7e0695b9ea17a27ae8ce68d50b56b6941c7f9b3d3453bc52"}, + {file = "shapely-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:ab8d878687b438a2f4c138ed1a80941c6ab0029e0f4c785ecfe114413b498a97"}, + {file = "shapely-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c062384316a47f776305ed2fa22182717508ffdeb4a56d0ff4087a77b2a0f6d"}, + {file = "shapely-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4ecf6c196b896e8f1360cc219ed4eee1c1e5f5883e505d449f263bd053fb8c05"}, + {file = "shapely-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb00070b4c4860f6743c600285109c273cca5241e970ad56bb87bef0be1ea3a0"}, + {file = "shapely-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14a9afa5fa980fbe7bf63706fdfb8ff588f638f145a1d9dbc18374b5b7de913"}, + {file = "shapely-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b640e390dabde790e3fb947198b466e63223e0a9ccd787da5f07bcb14756c28d"}, + {file = "shapely-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:69e08bf9697c1b73ec6aa70437db922bafcea7baca131c90c26d59491a9760f9"}, + {file = "shapely-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:ef2d09d5a964cc90c2c18b03566cf918a61c248596998a0301d5b632beadb9db"}, + {file = "shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7"}, + {file = "shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772"}, ] [package.dependencies] -numpy = ">=1.14,<3" +numpy = ">=1.21" [package.extras] docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] -test = ["pytest", "pytest-cov"] +test = ["pytest", "pytest-cov", "scipy-doctest"] [[package]] name = "texttable" @@ -370,7 +490,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -408,17 +528,17 @@ files = [ [[package]] name = "typing-extensions" -version = "4.13.0" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, - {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] [metadata] lock-version = "2.1" -python-versions = ">= 3.9,<4.0" -content-hash = "b5069de5668b64e7aca3900bb5500076cac669898f5867f96e8c763abe7073a7" +python-versions = ">= 3.10,<4.0" +content-hash = "0e36508251e760c60ff81cf54359d364cd2e0b4d318de16c8769009bf018fb9f" diff --git a/pyproject.toml b/pyproject.toml index e560133..e9caa6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,17 +7,18 @@ authors = [ ] license = {text = "GPL v3"} readme = "README.md" -requires-python = ">= 3.9,<4.0" +requires-python = ">= 3.10,<4.0" dependencies = [ "pycairo (>=1.27.0,<2.0.0)", "pygobject (>=3.52.3,<4.0.0)", - "packaging (>=24.2,<25.0)", - "python-igraph (>=0.11.8,<0.12.0)", - "shapely (==2.0.7)", + "packaging (>=25.0)", + "python-igraph (>=0.11.9,<0.12.0)", + "shapely (>=2.1.1,<3.0.0)", ] [tool.ruff.lint] extend-select = ["W291"] +ignore = ["E402"] [tool.poetry] @@ -25,7 +26,13 @@ extend-select = ["W291"] pygobject-stubs = "^2.13.0" mypy = "^1.15.0" ruff = "^0.11.2" +pytest = "^8.0.0" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" [build-system] -requires = ["poetry-core>=2.0.0,<3.0.0"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +requires = ["meson-python>=0.11.0"] +build-backend = "mesonpy" + diff --git a/run.py b/run.py index 9b1077c..d6ec44d 100755 --- a/run.py +++ b/run.py @@ -1,33 +1,68 @@ #!/usr/bin/python3 -# -*- coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- # # Copyright (C) Koichi Akabe 2012 -# +# # GGate is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # GGate is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program. If not, see . # import gi import sys +import os +import subprocess -gi.require_version("Gtk", '4.0') -gi.require_version('PangoCairo', '1.0') +gi.require_version("Gtk", "4.0") +gi.require_version("PangoCairo", "1.0") gi.require_version("Adw", "1") +from gi.repository import Gio from ggate.MainFrame import GLogicApplication from ggate import __version__ + +def build_dev_resources(): + base = os.path.dirname(os.path.abspath(__file__)) + images_dir = os.path.join(base, "data", "images") + xml = os.path.join(images_dir, "dev-resources.xml") + target = os.path.join(base, "dev-resources.gresource") + + try: + subprocess.run( + [ + sys.executable, + os.path.join(base, "build-aux", "generate-resources-xml.py"), + "--prefix", "/org/astralco/ggate/Dev", + "--output", xml, + "--source-dir", images_dir, + ], + check=True, + ) + except (OSError, subprocess.CalledProcessError): + pass + + try: + subprocess.run( + ["glib-compile-resources", "--sourcedir", images_dir, "--target", target, xml], + check=True, + ) + except (OSError, subprocess.CalledProcessError): + pass + if __name__ == "__main__": + build_dev_resources() + resource = Gio.Resource.load("./dev-resources.gresource") + resource._register() + app = GLogicApplication() if len(sys.argv) > 1 and sys.argv[1] == "--version": print(f"GGate {__version__}") diff --git a/screenshots/ggate-pref.png b/screenshots/ggate-pref.png deleted file mode 100644 index 43904f5..0000000 Binary files a/screenshots/ggate-pref.png and /dev/null differ diff --git a/screenshots/ggate-toolbar.png b/screenshots/ggate-toolbar.png deleted file mode 100644 index 1855734..0000000 Binary files a/screenshots/ggate-toolbar.png and /dev/null differ diff --git a/screenshots/ggate-window.png b/screenshots/ggate-window.png deleted file mode 100644 index 2bd2f30..0000000 Binary files a/screenshots/ggate-window.png and /dev/null differ diff --git a/setup.py b/setup.py deleted file mode 100644 index 2fe1305..0000000 --- a/setup.py +++ /dev/null @@ -1,134 +0,0 @@ -import os -from setuptools import setup -import pathlib -import ggate - -here = pathlib.Path(__file__).parent -help_translations = [] -translations = [] -pkg_name = 'ggate' -base = 'data/help' -help_files = [] - -os.chdir(here) - -for root, folders, files in os.walk(base): - for folder in folders: - if root+'/'+folder not in help_translations: - help_translations.append(root+'/'+folder) - if root == base: - for folder in folders: - translations.append(folder) - -for folder in help_translations: - if folder.split('/')[1] in translations: - parts = list(folder.split('/')) - parts.insert(2, pkg_name) - parts = ['share'] + parts - fd = "/".join(parts) - help_files.append((fd, [folder+'/'+x for x in os.listdir(folder) if os.path.isfile(folder+'/'+x)])) - -icons = { - "apps": { - '16': ['data/images/apps/16x16/ggate.png'], - '32': ['data/images/apps/32x32/ggate.png'], - '24': ['data/images/apps/24x24/ggate.png'], - '48': ['data/images/apps/48x48/ggate.png'], - '256': ['data/images/apps/256x256/ggate.png'], - - '64': ['data/images/apps/64x64/ggate.png'], - '96': ['data/images/apps/96x96/ggate.png'], - '128':['data/images/apps/128x128/ggate.png'], - '512': ['data/images/apps/512x512/ggate.png'] - }, - "scalable": ['data/images/scalable/ggate.svg'], - "mime": { - '16': ['data/images/mime/16x16/text-glc.png'], - '32': ['data/images/mime/32x32/text-glc.png'], - '24': ['data/images/mime/24x24/text-glc.png'], - '48': ['data/images/mime/48x48/text-glc.png'], - '256': ['data/images/mime/256x256/text-glc.png'], - - '64': ['data/images/mime/64x64/text-glc.png'], - '96': ['data/images/mime/96x96/text-glc.png'], - '128':['data/images/mime/128x128/text-glc.png'], - '512': ['data/images/mime/512x512/text-glc.png'] - } -} - -mime_sizes = { - 'yaru': ['16', '32', '24', '48', '256'] -} - -mimes = [] -for sizes in icons['mime'].keys(): - mimes.append((f'share/icons/hicolor/{sizes}x{sizes}/mimetypes', icons['mime'][sizes])) - if sizes in mime_sizes['yaru']: - mimes.append((f'share/icons/Yaru/{sizes}x{sizes}/mimetypes', icons['mime'][sizes])) - -apps = [] -for sizes in icons['apps'].keys(): - apps.append((f'share/icons/hicolor/{sizes}x{sizes}/apps', icons['apps'][sizes])) - -os.system(f'chmod +777 {here}/data/org.astralco.ggate.desktop') -os.system(f'chmod +777 {here}/bin/ggate') - -setup( - name="ggate", - version=ggate.__version__, - author="Koichi Akabe ", - author_email='vbkaisetsu@gmail.com', - maintainer='Ekure Edem', - maintainer_email="ekureedem480@gmail.com", - description='A logic gate simulator for linux developed with Gtk and python', - long_description=str(open('README.md').read()), - long_description_content_type="text/markdown", - license="GPL v3", - keywords="logic circuit simulation simulate gates computers buffers", - python_requires=">=3", - scripts=['bin/ggate'], - packages=['ggate', 'ggate.Components'], - package_data={ - 'ggate': [ - 'images/ggate.ico', - 'images/add-net.png', - 'images/ggate.png', - 'images/add-component.png', - 'images/components', - 'images/ggate-small.ico', - 'images/components/piso.png', - 'images/components/siso.png', - 'images/components/gnd.png', - 'images/components/dff.png', - 'images/components/tribuff.png', - 'images/components/or.png', - 'images/components/osc.png', - 'images/components/vdd.png', - 'images/components/probe.png', - 'images/components/and.png', - 'images/components/rsff.png', - 'images/components/counter.png', - 'images/components/text.png', - 'images/components/led.png', - 'images/components/sipo.png', - 'images/components/nand.png', - 'images/components/7seg.png', - 'images/components/pipo.png', - 'images/components/not.png', - 'images/components/tff.png', - 'images/components/nor.png', - 'images/components/jkff.png', - 'images/components/adder.png', - 'images/components/xor.png', - 'images/components/sw.png' - ] - }, - data_files=[ - *help_files, - *mimes, - *apps, - ("share/applications", ["data/org.astralco.ggate.desktop"]), - ("share/mime/packages", ["data/ggate.xml"]), - ("share/icons/hicolor/scalable/apps", icons["scalable"]), - ] -) diff --git a/test.py b/test.py index c17e862..df25c85 100644 --- a/test.py +++ b/test.py @@ -1,35 +1,63 @@ import gi gi.require_version('Gtk', '4.0') -from gi.repository import Gtk - -# Create the main application window -class MyWindow(Gtk.ApplicationWindow): - def __init__(self, app): - super().__init__(application=app, title="Alert Dialog Example") - self.set_default_size(400, 300) - - # Create a button that will trigger the alert dialog - button = Gtk.Button(label="Show Alert") - button.connect("clicked", self.on_button_clicked) - self.set_child(button) - - def on_button_clicked(self, widget): - # Create an AlertDialog instance - alert = Gtk.NativeDialog.newv() - alert.set_transient_for(self) - - # Show the alert dialog - alert.show(parent=self) - -# Create the application -class MyApplication(Gtk.Application): +gi.require_version('Adw', '1') +from gi.repository import Gtk, Adw +import math + +class CatWindow(Adw.ApplicationWindow): + def __init__(self, application=None): + super().__init__(title="Cute Cat with Cairo", application=application) + self.set_default_size(300, 300) + + drawing_area = Gtk.DrawingArea() + drawing_area.set_draw_func(self.on_draw) + self.set_content(drawing_area) + self.present() + + def on_draw(self, area, ctx, width, height): + # Center and scale + ctx.translate(width / 2, height / 2 + 20) + scale = min(width, height) / 200.0 + ctx.scale(scale, scale) + + # Draw head (circle) + ctx.set_source_rgb(0.2, 0.2, 0.2) + ctx.arc(0, -40, 40, 0, 2 * math.pi) + ctx.fill() + + # Draw ears + ctx.move_to(-30, -70); ctx.line_to(-60, -110); ctx.line_to(-10, -80) + ctx.move_to(30, -70); ctx.line_to(60, -110); ctx.line_to(10, -80) + ctx.fill() + + # Draw body + ctx.arc(0, 40, 60, 0, math.pi) + ctx.fill() + + # Draw tail + ctx.set_line_width(12) + ctx.move_to(60, 20) + ctx.curve_to(110, 0, 110, 80, 60, 60) + ctx.stroke() + + # Draw eyes + ctx.set_source_rgb(1, 1, 1) + for x in (-15, 15): + ctx.arc(x, -50, 8, 0, 2 * math.pi) + ctx.fill() + ctx.set_source_rgb(0, 0, 0) + ctx.arc(x, -48, 3, 0, 2 * math.pi) + ctx.fill() + ctx.set_source_rgb(1, 1, 1) # reset for next eye + +class CatApp(Adw.Application): def __init__(self): - super().__init__(application_id="com.example.AlertDialogExample") + super().__init__(application_id="com.example.CatApp") def do_activate(self): - win = MyWindow(self) + win = CatWindow(application=self) win.present() -# Run the application -app = MyApplication() -app.run() +if __name__ == "__main__": + app = CatApp() + app.run()