فهرست منبع

feat(demos): full gtk-demo catalogue, name-based registry and browser

Lay the foundation for a gtk4-demo-parity port:

- tools/extract_demos.py reads the GTK C demo sources and generates
  demos/DemoCatalog.def/.mod: the metadata (name, title, keywords,
  description) of all 146 demos.
- GtkDemo is now a name-based registry of *ported* demos (NameOf,
  BuildByName, IsPorted), so demos can be added incrementally.
- m2gtkdemo is a gtk-demo-style browser: a searchable sidebar listing
  every catalogue entry, a search filter over title/keywords, an info
  pane (title, description, ported/not-ported status) and Run; activating
  a row opens the demo window.
- Makefile/run_tests.sh build DemoCatalog alongside DemoImpl/GtkDemo;
  test_demos updated to the new API.

The browser currently registers the 5 demos ported so far; the remaining
141 are listed and shown as "not ported yet".  Subsequent commits port
them in batches.

Test suite: 45 passed.
Eric Streit 3 روز پیش
والد
کامیت
a8038bedcb
9فایلهای تغییر یافته به همراه1040 افزوده شده و 130 حذف شده
  1. 8 3
      Makefile
  2. 17 0
      demos/DemoCatalog.def
  3. 630 0
      demos/DemoCatalog.mod
  4. 10 11
      demos/GtkDemo.def
  5. 28 23
      demos/GtkDemo.mod
  6. 175 64
      demos/m2gtkdemo.mod
  7. 3 1
      tests/run_tests.sh
  8. 27 28
      tests/test_demos.mod
  9. 142 0
      tools/extract_demos.py

+ 8 - 3
Makefile

@@ -87,17 +87,22 @@ DEMO_DIR := demos
 $(BLD)/demos:
 	mkdir -p $(BLD)/demos
 
+$(OBJDIR)/DemoCatalog.o: $(DEMO_DIR)/DemoCatalog.mod $(DEMO_DIR)/DemoCatalog.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
+
 $(OBJDIR)/DemoImpl.o: $(DEMO_DIR)/DemoImpl.mod $(DEMO_DIR)/DemoImpl.def | $(OBJDIR)
 	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
 
 $(OBJDIR)/GtkDemo.o: $(DEMO_DIR)/GtkDemo.mod $(DEMO_DIR)/GtkDemo.def | $(OBJDIR)
 	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
 
-demos: $(BLD)/demos $(OBJS) $(OBJDIR)/DemoImpl.o $(OBJDIR)/GtkDemo.o $(BLD)/demos/m2gtkdemo
+DEMO_OBJS := $(OBJDIR)/DemoCatalog.o $(OBJDIR)/DemoImpl.o $(OBJDIR)/GtkDemo.o
+
+demos: $(BLD)/demos $(OBJS) $(DEMO_OBJS) $(BLD)/demos/m2gtkdemo
 
-$(BLD)/demos/m2gtkdemo: $(DEMO_DIR)/m2gtkdemo.mod $(OBJS) $(OBJDIR)/DemoImpl.o $(OBJDIR)/GtkDemo.o | $(BLD)/demos
+$(BLD)/demos/m2gtkdemo: $(DEMO_DIR)/m2gtkdemo.mod $(OBJS) $(DEMO_OBJS) | $(BLD)/demos
 	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) $(GTKCFG) $< \
-	     $(OBJS) $(OBJDIR)/DemoImpl.o $(OBJDIR)/GtkDemo.o -o $@ $(GTKLIBS)
+	     $(OBJS) $(DEMO_OBJS) -o $@ $(GTKLIBS)
 
 check: tests
 	./tests/run_tests.sh

+ 17 - 0
demos/DemoCatalog.def

@@ -0,0 +1,17 @@
+DEFINITION MODULE DemoCatalog ;
+
+(*
+   m2-GTK4 - the catalogue of gtk-demo demos, generated from the
+   GTK C demo sources by tools/extract_demos.py.  Provides the
+   title/keywords metadata used by the demo browser.
+*)
+
+EXPORT UNQUALIFIED DemoCatalogCount, DemoCatalogGet;
+
+PROCEDURE DemoCatalogCount () : CARDINAL;
+
+(* copy the metadata of demo i into the caller buffers *)
+PROCEDURE DemoCatalogGet (i: CARDINAL; VAR name, title, keywords,
+               description: ARRAY OF CHAR);
+
+END DemoCatalog.

+ 630 - 0
demos/DemoCatalog.mod

@@ -0,0 +1,630 @@
+IMPLEMENTATION MODULE DemoCatalog ;
+
+TYPE
+   Entry = RECORD
+      name: ARRAY [0..47] OF CHAR;
+      title: ARRAY [0..79] OF CHAR;
+      keywords: ARRAY [0..255] OF CHAR;
+      description: ARRAY [0..511] OF CHAR;
+   END;
+
+VAR
+   entries: ARRAY [0..145] OF Entry;
+   n: CARDINAL;
+
+PROCEDURE Copy (VAR dst: ARRAY OF CHAR; src: ARRAY OF CHAR);
+VAR i: CARDINAL;
+BEGIN
+   i := 0;
+   WHILE (i <= HIGH(dst)) AND (i <= HIGH(src)) AND (src[i] # 0C) DO
+      dst[i] := src[i]; INC(i)
+   END;
+   IF i <= HIGH(dst) THEN dst[i] := 0C END
+END Copy;
+
+PROCEDURE DemoCatalogCount () : CARDINAL;
+BEGIN
+   RETURN n
+END DemoCatalogCount;
+
+PROCEDURE DemoCatalogGet (i: CARDINAL; VAR name, title, keywords,
+               description: ARRAY OF CHAR);
+BEGIN
+   IF i < n THEN
+      Copy(name, entries[i].name);
+      Copy(title, entries[i].title);
+      Copy(keywords, entries[i].keywords);
+      Copy(description, entries[i].description)
+   ELSE
+      name[0] := 0C; title[0] := 0C; keywords[0] := 0C;
+      description[0] := 0C
+   END
+END DemoCatalogGet;
+
+BEGIN
+   n := 146;
+   entries[0].name := "application";
+   entries[0].title := "application";
+   entries[0].keywords := "";
+   entries[0].description := "";
+   entries[1].name := "application_demo";
+   entries[1].title := "Application Class";
+   entries[1].keywords := "";
+   entries[1].description := "Demonstrates a simple application. This example uses GtkApplication, GtkApplicationWindow, GtkBuilder as well as GMenu and GResource. Due to the way GtkApplication is structured, it is run as a separate process.";
+   entries[2].name := "aspect_frame";
+   entries[2].title := "Aspect Frame";
+   entries[2].keywords := "";
+   entries[2].description := "GtkAspectFrame makes sure its child gets a specific aspect ratio.";
+   entries[3].name := "assistant";
+   entries[3].title := "Assistant";
+   entries[3].keywords := "";
+   entries[3].description := "Demonstrates a sample multi-step assistant with GtkAssistant. Assistants are used to divide an operation into several simpler sequential steps, and to guide the user through these steps.";
+   entries[4].name := "badapple";
+   entries[4].title := "Background Blur";
+   entries[4].keywords := "";
+   entries[4].description := "Blurs the background with a video. If the system does not support blurred backgrounds, this will not be very impressive. For effect, there are no decorations. You can close the window using Alt-F4 or via the right-click menu.";
+   entries[5].name := "fishbowl";
+   entries[5].title := "Benchmark/Fishbowl";
+   entries[5].keywords := "";
+   entries[5].description := "This demo models the fishbowl demos seen on the web in a GTK way. It's also a neat little tool to see how fast your computer (or your GTK version) is.";
+   entries[6].name := "frames";
+   entries[6].title := "Benchmark/Frames";
+   entries[6].keywords := "";
+   entries[6].description := "This demo is intentionally as simple as possible, to see what framerate the windowing system can deliver on its own. It does nothing but change the drawn color, for every frame.";
+   entries[7].name := "iconscroll";
+   entries[7].title := "Benchmark/Scrolling";
+   entries[7].keywords := "GtkScrolledWindow";
+   entries[7].description := "This demo scrolls a view with various content.";
+   entries[8].name := "themes";
+   entries[8].title := "Benchmark/Themes";
+   entries[8].keywords := "";
+   entries[8].description := "This demo continuously switches themes, like some of you. Warning: This demo involves rapidly flashing changes and may be hazardous to photosensitive viewers.";
+   entries[9].name := "layoutmanager2";
+   entries[9].title := "Benchmark/Transformation";
+   entries[9].keywords := "GtkLayoutManager, GskTransform";
+   entries[9].description := "This demo shows how to use transforms in a nontrivial way with a custom layout manager. The layout manager places content on a rotating sphere. The direction of the rotation can be changed using arrow keys.";
+   entries[10].name := "builder";
+   entries[10].title := "Builder";
+   entries[10].keywords := "GMenu, GtkPopoverMenuBar, GtkBuilder, GtkShortcutController, toolbar";
+   entries[10].description := "Demonstrates a traditional interface, loaded from a XML description, and shows how to connect actions to the menu items and toolbar buttons.";
+   entries[11].name := "clipboard";
+   entries[11].title := "Clipboard";
+   entries[11].keywords := "drag-and-drop, dnd";
+   entries[11].description := "GdkClipboard is used for clipboard handling. This demo shows how to copy and paste text, images, colors or files to and from the clipboard. You can also use Drag-And-Drop to copy the data from the source to the target.";
+   entries[12].name := "combobox";
+   entries[12].title := "Combo Boxes";
+   entries[12].keywords := "GtkCellRenderer";
+   entries[12].description := "The GtkComboBox widget allows to select one option out of a list. The GtkComboBoxEntry additionally allows the user to enter a value that is not in the list of options. How the options are displayed is controlled by cell renderers.";
+   entries[13].name := "component_filter";
+   entries[13].title := "component filter";
+   entries[13].keywords := "";
+   entries[13].description := "";
+   entries[14].name := "constraints_builder";
+   entries[14].title := "Constraints/Builder";
+   entries[14].keywords := "";
+   entries[14].description := "GtkConstraintLayouts can be created in .ui files, and constraints can be set up at that time as well, as this example demonstrates. It shows various ways to do spacing and sizing with constraints. Make the window wider to see the rows react differently";
+   entries[15].name := "constraints_interactive";
+   entries[15].title := "Constraints/Interactive Constraints";
+   entries[15].keywords := "GtkConstraintLayout";
+   entries[15].description := "This example shows how constraints can be updated during user interaction. The vertical edge between the buttons can be dragged with the mouse.";
+   entries[16].name := "constraints";
+   entries[16].title := "Constraints/Simple Constraints";
+   entries[16].keywords := "GtkLayoutManager";
+   entries[16].description := "GtkConstraintLayout provides a layout manager that uses relations between widgets (also known as ?constraints?) to compute the position and size of each child. In addition to child widgets, the constraints can involve spacer objects (also known as ?guides?). This example has a guide between the two buttons in the top row. Try resizing the window to see how the constraints react to update the layout.";
+   entries[17].name := "constraints_vfl";
+   entries[17].title := "Constraints/VFL";
+   entries[17].keywords := "";
+   entries[17].description := "GtkConstraintLayout allows defining constraints using a compact syntax called Visual Format Language, or VFL. A typical example of a VFL specification looks like this: H:|-[button1(==button2)]-12-[button2]-|";
+   entries[18].name := "range-editor";
+   entries[18].title := "Copyright ? 2025 Red Hat, Inc";
+   entries[18].keywords := "";
+   entries[18].description := "This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have receiv";
+   entries[19].name := "cursors";
+   entries[19].title := "Cursors";
+   entries[19].keywords := "";
+   entries[19].description := "Demonstrates a useful set of available cursors. The cursors shown here are the ones defined by CSS, which we assume to be available. The example shows creating cursors by name or from an image, with or without a fallback.";
+   entries[20].name := "demo2layout";
+   entries[20].title := "demo2layout";
+   entries[20].keywords := "";
+   entries[20].description := "";
+   entries[21].name := "demo2widget";
+   entries[21].title := "demo2widget";
+   entries[21].keywords := "";
+   entries[21].description := "";
+   entries[22].name := "demo4widget";
+   entries[22].title := "demo4widget";
+   entries[22].keywords := "";
+   entries[22].description := "";
+   entries[23].name := "demochild";
+   entries[23].title := "demochild";
+   entries[23].keywords := "";
+   entries[23].description := "";
+   entries[24].name := "demoimage";
+   entries[24].title := "demoimage";
+   entries[24].keywords := "";
+   entries[24].description := "";
+   entries[25].name := "demolayout";
+   entries[25].title := "demolayout";
+   entries[25].keywords := "";
+   entries[25].description := "";
+   entries[26].name := "demowidget";
+   entries[26].title := "demowidget";
+   entries[26].keywords := "";
+   entries[26].description := "";
+   entries[27].name := "dialog";
+   entries[27].title := "Dialogs";
+   entries[27].keywords := "GtkMessageDialog";
+   entries[27].description := "Dialogs are used to pop up transient windows for information and user feedback.";
+   entries[28].name := "dnd";
+   entries[28].title := "Drag-and-Drop";
+   entries[28].keywords := "dnd, menu, popover, gesture";
+   entries[28].description := "This demo shows dragging colors and widgets. The items in this demo can be moved, recolored and rotated. The demo also has an example for creating a menu-like popover without using a menu model.";
+   entries[29].name := "drawingarea";
+   entries[29].title := "Drawing Area";
+   entries[29].keywords := "GtkDrawingArea";
+   entries[29].description := "GtkDrawingArea is a blank area where you can draw custom displays of various kinds. This demo has two drawing areas. The checkerboard area shows how you can just draw something; all you have to do is set a function via gtk_drawing_area_set_draw_func(), as shown here. The 'scribble' area is a bit more advanced, and shows how to handle events such as button presses and mouse motion. Click the mouse and drag in the scribble area to draw squiggles. Resize the window to clear the area.";
+   entries[30].name := "entry_completion";
+   entries[30].title := "Entry/Completion";
+   entries[30].keywords := "";
+   entries[30].description := "GtkEntryCompletion provides a mechanism for adding support for completion in GtkEntry.";
+   entries[31].name := "password_entry";
+   entries[31].title := "Entry/Password Entry";
+   entries[31].keywords := "";
+   entries[31].description := "GtkPasswordEntry provides common functionality of entries that are used to enter passwords and other secrets. It will display a warning if CapsLock is on, and it can optionally provide a way to see the text.";
+   entries[32].name := "search_entry";
+   entries[32].title := "Entry/Search Entry";
+   entries[32].keywords := "";
+   entries[32].description := "GtkSearchEntry provides an entry that is ready for search. Search entries have their 'search-changed' signal delayed and should be used when the search operation is slow, such as big datasets to search, or online searches. GtkSearchBar allows have a hidden search entry that 'springs into action' upon keyboard input.";
+   entries[33].name := "tagged_entry";
+   entries[33].title := "Entry/Tagged Entry";
+   entries[33].keywords := "";
+   entries[33].description := "This example shows how to build a complex composite entry using GtkText, outside of GTK. This tagged entry can display tags and other widgets inside the entry area.";
+   entries[34].name := "entry_undo";
+   entries[34].title := "Entry/Undo and Redo";
+   entries[34].keywords := "";
+   entries[34].description := "GtkEntry can provide basic Undo/Redo support using standard keyboard accelerators such as Control+z to undo and Control+Shift+z to redo. Additionally, Control+y can be used to redo. Use gtk_entry_set_enable_undo() to enable undo/redo support.";
+   entries[35].name := "errorstates";
+   entries[35].title := "Error States";
+   entries[35].keywords := "";
+   entries[35].description := "GtkLabel and GtkEntry can indicate errors if you set the .error style class on them. This examples shows how this can be used in a dialog for input validation. It also shows how pass callbacks and objects to GtkBuilder with GtkBuilderScope and gtk_builder_expose_object().";
+   entries[36].name := "expander";
+   entries[36].title := "Expander";
+   entries[36].keywords := "";
+   entries[36].description := "GtkExpander allows to provide additional content that is initially hidden. This is also known as 'disclosure triangle'. This example also shows how to make the window resizable only if the expander is expanded.";
+   entries[37].name := "filter_paintable";
+   entries[37].title := "filter paintable";
+   entries[37].keywords := "";
+   entries[37].description := "";
+   entries[38].name := "fixed";
+   entries[38].title := "Fixed Layout / Cube";
+   entries[38].keywords := "GtkLayoutManager";
+   entries[38].description := "GtkFixed is a container that allows placing and transforming widgets manually. This demo uses a GtkFixed to create a cube out of child widgets.";
+   entries[39].name := "fixed2";
+   entries[39].title := "Fixed Layout / Transformations";
+   entries[39].keywords := "GtkLayoutManager";
+   entries[39].description := "GtkFixed is a container that allows placing and transforming widgets manually. This demo shows how to rotate and scale a child widget using a transform.";
+   entries[40].name := "flowbox";
+   entries[40].title := "Flow Box";
+   entries[40].keywords := "";
+   entries[40].description := "GtkFlowBox allows flexible and responsive grids which reflow as needed and support sorting and filtering. The children of a GtkFlowBox are regular widgets The dataset used here has 665 colors.";
+   entries[41].name := "fontify";
+   entries[41].title := "fontify";
+   entries[41].keywords := "";
+   entries[41].description := "";
+   entries[42].name := "four_point_transform";
+   entries[42].title := "four point transform";
+   entries[42].keywords := "";
+   entries[42].description := "";
+   entries[43].name := "gestures";
+   entries[43].title := "Gestures";
+   entries[43].keywords := "GtkGesture";
+   entries[43].description := "Perform gestures on touchscreens and other input devices. This demo reacts to long presses and swipes from all devices, plus multi-touch rotate and zoom gestures.";
+   entries[44].name := "graphwidget";
+   entries[44].title := "graphwidget";
+   entries[44].keywords := "";
+   entries[44].description := "";
+   entries[45].name := "demotaggedentry";
+   entries[45].title := "GTK - The GIMP Toolkit";
+   entries[45].keywords := "";
+   entries[45].description := "Copyright (C) 2019 Red Hat, Inc. Authors: - Matthias Clasen <mclasen@redhat.com> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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";
+   entries[46].name := "fontplane";
+   entries[46].title := "GTK - The GIMP Toolkit";
+   entries[46].keywords := "";
+   entries[46].description := "Copyright (C) 2012 Red Hat, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more ";
+   entries[47].name := "gtkfishbowl";
+   entries[47].title := "GTK - The GIMP Toolkit";
+   entries[47].keywords := "";
+   entries[47].description := "Copyright (C) 2017 Benjamin Otte <otte@gnome.org> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Lesser General Public ";
+   entries[48].name := "gtkshadertoy";
+   entries[48].title := "gtkshadertoy";
+   entries[48].keywords := "";
+   entries[48].description := "";
+   entries[49].name := "headerbar";
+   entries[49].title := "Header Bar";
+   entries[49].keywords := "GtkWindowHandle, GtkWindowControls";
+   entries[49].description := "GtkHeaderBar is a container that is suitable for implementing window titlebars. One of its features is that it can position a title centered with regard to the full width, regardless of variable-width content at the left or right. It is commonly used with gtk_window_set_titlebar()";
+   entries[50].name := "hsla";
+   entries[50].title := "hsla";
+   entries[50].keywords := "";
+   entries[50].description := "";
+   entries[51].name := "iconview_edit";
+   entries[51].title := "Icon View/Editing and Drag-and-Drop";
+   entries[51].keywords := "dnd";
+   entries[51].description := "The GtkIconView widget supports Editing and Drag-and-Drop. This example also demonstrates using the generic GtkCellLayout interface to set up cell renderers in an icon view.";
+   entries[52].name := "iconview";
+   entries[52].title := "Icon View/Icon View Basics";
+   entries[52].keywords := "";
+   entries[52].description := "The GtkIconView widget is used to display and manipulate icons. It uses a GtkTreeModel for data storage, so the list store example might be helpful.";
+   entries[53].name := "image_filtering";
+   entries[53].title := "Image Filtering";
+   entries[53].keywords := "brightness, contrast, saturation, sepia, blur, color, posterize, graph";
+   entries[53].description := "Show some image filters. This includes both TV-style brightness and contrast controls, as well as more complex effects such as sepia. All of the filters are applied on the GPU. Also demonstrate how to use GskPath for drawing simple graphs.";
+   entries[54].name := "image_scaling";
+   entries[54].title := "Image Scaling";
+   entries[54].keywords := "zoom, scale, filter, action, menu";
+   entries[54].description := "The custom widget we create here is similar to a GtkPicture, but allows setting a zoom level and filtering mode for the displayed paintable. It also demonstrates how to add a context menu to a custom widget and connect it with widget actions. The context menu has items to change the zoom level.";
+   entries[55].name := "images";
+   entries[55].title := "Images";
+   entries[55].keywords := "GdkPaintable, GtkWidgetPaintable";
+   entries[55].description := "GtkImage and GtkPicture are used to display an image; the image can be in a number of formats. GtkImage is the widget used to display icons or images that should be sized and styled like an icon, while GtkPicture is used for images that should be displayed as-is. This demo code shows some of the more obscure cases, in the simple case a call to gtk_picture_new_for_file() or gtk_image_new_from_icon_name() is all you need.";
+   entries[56].name := "imageview";
+   entries[56].title := "imageview";
+   entries[56].keywords := "";
+   entries[56].description := "";
+   entries[57].name := "infobar";
+   entries[57].title := "Info Bars";
+   entries[57].keywords := "GtkInfoBar";
+   entries[57].description := "Info bar widgets are used to report important messages to the user.";
+   entries[58].name := "language-names";
+   entries[58].title := "language-names";
+   entries[58].keywords := "";
+   entries[58].description := "";
+   entries[59].name := "layoutmanager";
+   entries[59].title := "Layout Manager/Transition";
+   entries[59].keywords := "GtkLayoutManager";
+   entries[59].description := "This demo shows a simple example of a custom layout manager and a widget using it. The layout manager places the children of the widget in a grid or a circle. The widget is animating the transition between the two layouts. Click to start the transition.";
+   entries[60].name := "links";
+   entries[60].title := "Links";
+   entries[60].keywords := "";
+   entries[60].description := "GtkLabel can show hyperlinks. The default action is to call gtk_show_uri() on their URI, but it is possible to override this with a custom handler.";
+   entries[61].name := "listbox";
+   entries[61].title := "List Box/Complex";
+   entries[61].keywords := "";
+   entries[61].description := "GtkListBox allows lists with complicated layouts, using regular widgets supporting sorting and filtering.";
+   entries[62].name := "listbox_controls";
+   entries[62].title := "List Box/Controls";
+   entries[62].keywords := "";
+   entries[62].description := "GtkListBox is well-suited for creating ?button strips? ? lists of controls for use in preference dialogs or settings panels. To create this style of list, use the .rich-list style class.";
+   entries[63].name := "listview_settings2";
+   entries[63].title := "Lists/Alternative Settings";
+   entries[63].keywords := "GtkListHeaderFactory, GtkSectionModel";
+   entries[63].description := "This demo shows an alternative settings viewer for GSettings. It demonstrates how to implement support for sections with GtkListView. It also shows how to quickly flatten a large tree of items into a list that can be filtered to find the items one is looking for.";
+   entries[64].name := "listview_applauncher";
+   entries[64].title := "Lists/Application launcher";
+   entries[64].keywords := "GtkListItemFactory, GListModel";
+   entries[64].description := "This demo uses the GtkListView widget as a fancy application launcher. It is also a very small introduction to listviews.";
+   entries[65].name := "listview_ucd";
+   entries[65].title := "Lists/Characters";
+   entries[65].keywords := "";
+   entries[65].description := "This demo shows a multi-column representation of some parts of the Unicode Character Database, or UCD. It also demonstrates the use of sections with headings to group items. The dataset used here has 33 796 items.";
+   entries[66].name := "listview_clocks";
+   entries[66].title := "Lists/Clocks";
+   entries[66].keywords := "GtkGridView, GtkListItemFactory, GListModel";
+   entries[66].description := "This demo displays the time in different timezones. The goal is to show how to set up expressions that track changes in objects and make them update widgets. For that, we create a clock object that updates its time every second and then use various ways to display that time. Typically, this will be done using GtkBuilder .ui files with the help of the <binding> tag, but this demo shows the code that runs behind that.";
+   entries[67].name := "listview_colors";
+   entries[67].title := "Lists/Colors";
+   entries[67].keywords := "GtkSortListModel, GtkMultiSelection";
+   entries[67].description := "This demo displays a grid of colors. It is using a GtkGridView, and shows how to display and sort the data in various ways. The controls for this are implemented using GtkDropDown. The dataset used here has up to 16 777 216 items. Note that this demo also functions as a performance test for some of the list model machinery, and the biggest sizes here can lock up the application for extended times when used with sorting.";
+   entries[68].name := "listview_filebrowser";
+   entries[68].title := "Lists/File browser";
+   entries[68].keywords := "GListModel";
+   entries[68].description := "This demo shows off the different layouts that are quickly achievable with GtkListview and GtkGridView by implementing a file browser with different views.";
+   entries[69].name := "listview_minesweeper";
+   entries[69].title := "Lists/Minesweeper";
+   entries[69].keywords := "GtkGridView, GListModel, game";
+   entries[69].description := "This demo shows how to develop a user interface for small game using a grid view. It demonstrates how to use the activate signal and single-press behavior to implement rather different interaction behavior to a typical list.";
+   entries[70].name := "listview_selections";
+   entries[70].title := "Lists/Selections";
+   entries[70].keywords := "suggestion, completion";
+   entries[70].description := "The GtkDropDown widget is a modern alternative to GtkComboBox. It uses list models instead of tree models, and the content is displayed using widgets instead of cell renderers. This example also shows a custom widget that can replace GtkEntryCompletion or GtkComboBoxText. It is not currently part of GTK.";
+   entries[71].name := "listview_settings";
+   entries[71].title := "Lists/Settings";
+   entries[71].keywords := "GtkListItemFactory, GListModel";
+   entries[71].description := "This demo shows a settings viewer for GSettings. It demonstrates how to implement support for trees with GtkListView. It also shows how to set up sorting and filtering for columns in a GtkColumnView. It also demonstrates different styles of list. The tree on the left uses the ?.navigation-sidebar style class, the list on the right uses the ?.data-table style class.";
+   entries[72].name := "listview_weather";
+   entries[72].title := "Lists/Weather";
+   entries[72].keywords := "";
+   entries[72].description := "This demo shows a few of the rarer features of GtkListView and how they can be used to display weather information. The hourly weather info uses a horizontal listview. This is easy to achieve because GtkListView implements the GtkOrientable interface. To make the items in the list stand out more, the listview uses separators. A GtkNoSelectionModel is used to make sure no item in the list can be selected. All other interactions with the items is still possible. The dataset used here has 70 000 items.";
+   entries[73].name := "listview_words";
+   entries[73].title := "Lists/Words";
+   entries[73].keywords := "GtkListView, GtkFilterListModel, GtkInscription";
+   entries[73].description := "This demo shows filtering a long list - of words. You should have the file `/usr/share/dict/words` installed for this demo to work.";
+   entries[74].name := "mask";
+   entries[74].title := "Masking";
+   entries[74].keywords := "";
+   entries[74].description := "Demonstrates mask nodes. This demo uses a text node as mask for an animated linear gradient.";
+   entries[75].name := "minigraph";
+   entries[75].title := "minigraph";
+   entries[75].keywords := "";
+   entries[75].description := "";
+   entries[76].name := "nodewidget";
+   entries[76].title := "nodewidget";
+   entries[76].keywords := "";
+   entries[76].description := "";
+   entries[77].name := "gears";
+   entries[77].title := "OpenGL/Gears";
+   entries[77].keywords := "";
+   entries[77].description := "This is a classic OpenGL demo, running in a GtkGLArea.";
+   entries[78].name := "glarea";
+   entries[78].title := "OpenGL/OpenGL Area";
+   entries[78].keywords := "";
+   entries[78].description := "GtkGLArea is a widget that allows custom drawing using OpenGL calls.";
+   entries[79].name := "shadertoy";
+   entries[79].title := "OpenGL/Shadertoy";
+   entries[79].keywords := "GtkGLArea";
+   entries[79].description := "Generate pixels using a custom fragment shader. The names of the uniforms are compatible with the shaders on shadertoy.com, so many of the shaders there work here too.";
+   entries[80].name := "overlay_decorative";
+   entries[80].title := "Overlay/Decorative Overlay";
+   entries[80].keywords := "GtkOverlay";
+   entries[80].description := "Another example of an overlay with some decorative and some interactive controls.";
+   entries[81].name := "number";
+   entries[81].title := "Overlay/Interactive Overlay";
+   entries[81].keywords := "GtkOverlay";
+   entries[81].description := "Shows widgets in static positions over a main widget. The overlaid widgets can be interactive controls such as the entry in this example, or just decorative, like the big blue label.";
+   entries[82].name := "transparent";
+   entries[82].title := "Overlay/Transparency";
+   entries[82].keywords := "GtkOverlay, GtkSnapshot, blur, backdrop-filter";
+   entries[82].description := "Blur the background behind an overlay.";
+   entries[83].name := "paint";
+   entries[83].title := "Paint";
+   entries[83].keywords := "GdkDrawingArea, GtkGesture";
+   entries[83].description := "Demonstrates practical handling of drawing tablets in a real world usecase.";
+   entries[84].name := "puzzlepiece";
+   entries[84].title := "Paintable/A simple paintable";
+   entries[84].keywords := "";
+   entries[84].description := "GdkPaintable is an interface used by GTK for drawings of any sort that do not require layouting or positioning. This demo code gives a simple example on how a paintable can be created. Paintables can be used in many places inside GTK widgets, but the most common usage is inside GtkImage and that's what we're going to do here.";
+   entries[85].name := "paintable_animated";
+   entries[85].title := "Paintable/Animated Paintable";
+   entries[85].keywords := "";
+   entries[85].description := "GdkPaintable also allows paintables to change. This demo code gives an example of how this could work. It builds on the previous simple example. Paintables can also change their size, this works similarly, but we will not demonstrate this here as our icon does not have any size.";
+   entries[86].name := "paintable_emblem";
+   entries[86].title := "Paintable/Emblems";
+   entries[86].keywords := "";
+   entries[86].description := "This demo shows how GdkPaintable can be used to overlay an emblem on top of an icon. The emblem can either be another icon, or an arbitrary paintable.";
+   entries[87].name := "paintable_mediastream";
+   entries[87].title := "Paintable/Media Stream";
+   entries[87].keywords := "";
+   entries[87].description := "GdkPaintable is also used by the GtkMediaStream class. This demo code turns the nuclear media_stream into the object GTK uses for videos. This allows treating the icon like a regular video, so we can for example attach controls to it. After all, what good is a media_stream if one cannot pause it.";
+   entries[88].name := "paintable";
+   entries[88].title := "Paintable/Simple Paintable";
+   entries[88].keywords := "";
+   entries[88].description := "GdkPaintable is an interface used by GTK for drawings of any sort that do not require layouting or positioning. This demo code gives a simple example on how a paintable can be created. Paintables can be used in many places inside GTK widgets, but the most common usage is inside GtkImage and that's what we're going to do here.";
+   entries[89].name := "paintable_svg";
+   entries[89].title := "Paintable/SVG Paintable";
+   entries[89].keywords := "symbolic";
+   entries[89].description := "This demo shows using a GtkSvg paintable to display an SVG image in a GtkPicture that can be scaled by resizing the window.";
+   entries[90].name := "paintable_symbolic";
+   entries[90].title := "Paintable/Symbolic Paintable";
+   entries[90].keywords := "";
+   entries[90].description := "GdkPaintables can be made to follow the theme's colors. GTK calls icons that do this symbolic icons, paintables that want to have the same effect can implement the GtkSymbolicPaintable interface. We will adapt the original paintable example by adding the ability to recolor the paintable based on the symbolic colors.";
+   entries[91].name := "panes";
+   entries[91].title := "Paned Widgets";
+   entries[91].keywords := "";
+   entries[91].description := "The GtkPaned Widget divides its content area into two panes with a divider in between that the user can adjust. A separate child is placed into each pane. GtkPaned widgets can be split horizontally or vertically. This test contains both a horizontal and a vertical GtkPaned widget. There are a number of options that can be set for each pane. You can use the Inspector to adjust the options for each side of each widget.";
+   entries[92].name := "font_features";
+   entries[92].title := "Pango/Font Explorer";
+   entries[92].keywords := "";
+   entries[92].description := "This example demonstrates support for OpenType font features with Pango attributes. The attributes can be used manually or via Pango markup. It can also be used to explore available features in OpenType fonts and their effect. If the selected font supports OpenType font variations, then the axes are also offered for customization.";
+   entries[93].name := "fontrendering";
+   entries[93].title := "Pango/Font Rendering";
+   entries[93].keywords := "";
+   entries[93].description := "Demonstrates various aspects of font rendering, such as hinting, antialiasing and grid alignment. The demo lets you explore font rendering options interactively to get a feeling for they affect the shape and positioning of the glyphs.";
+   entries[94].name := "rotated_text";
+   entries[94].title := "Pango/Rotated Text";
+   entries[94].keywords := "";
+   entries[94].description := "This demo shows how to use PangoCairo to draw rotated and transformed text. The right pane shows a rotated GtkLabel widget. In both cases, a custom PangoCairo shape renderer is installed to draw a red heart using cairo drawing operations instead of the Unicode heart character.";
+   entries[95].name := "textmask";
+   entries[95].title := "Pango/Text Mask";
+   entries[95].keywords := "";
+   entries[95].description := "This demo shows how to use PangoCairo to draw text with more than just a single color.";
+   entries[96].name := "path_explorer";
+   entries[96].title := "path explorer";
+   entries[96].keywords := "";
+   entries[96].description := "";
+   entries[97].name := "path_fill";
+   entries[97].title := "Path/Fill and Stroke";
+   entries[97].keywords := "";
+   entries[97].description := "This demo shows how to use GskPath to draw shapes that are (a bit) more complex than a rounded rectangle. It also demonstrates printing to a stream with GtkPrintDialog. Finally, it shows how to use GtkPopoverBin to add a context menu to a widget that does not have one.";
+   entries[98].name := "path_maze";
+   entries[98].title := "Path/Maze";
+   entries[98].keywords := "";
+   entries[98].description := "This demo shows how to use a GskPath to create a maze and use gsk_path_get_closest_point() to check the mouse stays on the path. It also shows off the performance of GskPath (or not) as this is a rather complex path.";
+   entries[99].name := "path_explorer_demo";
+   entries[99].title := "Path/Path Explorer";
+   entries[99].keywords := "";
+   entries[99].description := "This demo lets you explore some of the features of GskPath.";
+   entries[100].name := "path_spinner";
+   entries[100].title := "Path/Spinner";
+   entries[100].keywords := "";
+   entries[100].description := "This demo shows how to use GskPath to draw a simple animation that could be used as a spinner.";
+   entries[101].name := "path_sweep";
+   entries[101].title := "Path/Sweep";
+   entries[101].keywords := "";
+   entries[101].description := "This demo shows how path intersections can be used. The world map that is used here is a path with 211 lines and 1569 cubic B?zier segments in 121 contours.";
+   entries[102].name := "path_text";
+   entries[102].title := "Path/Text";
+   entries[102].keywords := "";
+   entries[102].description := "This demo shows how to use GskPath to transform a path along another path. It also demonstrates that paths can be filled with more interesting content than just plain colors.";
+   entries[103].name := "path_walk";
+   entries[103].title := "Path/Walk";
+   entries[103].keywords := "";
+   entries[103].description := "This demo draws a world map and shows how to animate objects along a GskPath. The world map that is used here is a path with 211 lines and 1569 cubic B?zier segments in 121 contours.";
+   entries[104].name := "peg_solitaire";
+   entries[104].title := "Peg Solitaire";
+   entries[104].keywords := "GtkGridView, game, drag-and-drop, dnd";
+   entries[104].description := "This demo demonstrates how to use drag-and-drop to implement peg solitaire.";
+   entries[105].name := "pickers";
+   entries[105].title := "Pickers and Launchers";
+   entries[105].keywords := "GtkColorDialog, GtkFontDialog, GtkFileDialog, GtkPrintDialog, GtkFileLauncher, GtkUriLauncher";
+   entries[105].description := "The dialogs are mainly intended for use in preference dialogs. They allow to select colors, fonts and files. There is also a print dialog. The launchers let you open files or URIs in applications that can handle them.";
+   entries[106].name := "pixbufpaintable";
+   entries[106].title := "pixbufpaintable";
+   entries[106].keywords := "";
+   entries[106].description := "";
+   entries[107].name := "pagesetup";
+   entries[107].title := "Printing/Page Setup";
+   entries[107].keywords := "GtkPageSetup";
+   entries[107].description := "GtkPageSetupUnixDialog can be used if page setup is needed independent of a full printing dialog.";
+   entries[108].name := "printing";
+   entries[108].title := "Printing/Printing";
+   entries[108].keywords := "";
+   entries[108].description := "GtkPrintOperation offers a simple API to support printing in a cross-platform way.";
+   entries[109].name := "read_more";
+   entries[109].title := "Read More";
+   entries[109].keywords := "GtkInscription";
+   entries[109].description := "A simple implementation of a widget that can either display a lot of text or just the first few lines with a 'Read More' button.";
+   entries[110].name := "revealer";
+   entries[110].title := "Revealer";
+   entries[110].keywords := "";
+   entries[110].description := "GtkRevealer is a container that animates showing and hiding of its sole child with nice transitions.";
+   entries[111].name := "scale";
+   entries[111].title := "Scales";
+   entries[111].keywords := "";
+   entries[111].description := "GtkScale is a way to select a value from a range. Scales can have marks to help pick special values, and they can also restrict the values that can be chosen.";
+   entries[112].name := "script-names";
+   entries[112].title := "script-names";
+   entries[112].keywords := "";
+   entries[112].description := "";
+   entries[113].name := "settings-key";
+   entries[113].title := "settings-key";
+   entries[113].keywords := "";
+   entries[113].description := "";
+   entries[114].name := "shortcut_triggers";
+   entries[114].title := "Shortcuts";
+   entries[114].keywords := "GtkShortcutController";
+   entries[114].description := "GtkShortcut is the abstraction used by GTK to handle shortcuts from keyboard or other input devices. Shortcut triggers can be used to weave complex sequences of key presses into sophisticated mechanisms to activate shortcuts. This demo code shows creative ways to do that.";
+   entries[115].name := "shortcuts";
+   entries[115].title := "Shortcuts Window";
+   entries[115].keywords := "";
+   entries[115].description := "GtkShortcutsWindow is a window that provides a help overlay for shortcuts and gestures in an application.";
+   entries[116].name := "singular_value_decomposition";
+   entries[116].title := "singular value decomposition";
+   entries[116].keywords := "";
+   entries[116].description := "";
+   entries[117].name := "sizegroup";
+   entries[117].title := "Size Groups";
+   entries[117].keywords := "";
+   entries[117].description := "GtkSizeGroup provides a mechanism for grouping a number of widgets together so they all request the same amount of space. This is typically useful when you want a column of widgets to have the same size, but you can't use a GtkTable widget. Note that size groups only affect the amount of space requested, not the size that the widgets finally receive. If you want the widgets in a GtkSizeGroup to actually be the same size, you need to pack them in such a way that they get the size they request and not more. ";
+   entries[118].name := "sliding_puzzle";
+   entries[118].title := "Sliding Puzzle";
+   entries[118].keywords := "GdkPaintable, GdkGesture, GtkShortcutController, game";
+   entries[118].description := "This demo demonstrates how to use gestures and paintables to create a small sliding puzzle game.";
+   entries[119].name := "snapping";
+   entries[119].title := "Snapping";
+   entries[119].keywords := "qr code";
+   entries[119].description := "Demonstrates how to snap content. This demo uses a grid of tiles and scales it without seams when resizing the window.";
+   entries[120].name := "spinbutton";
+   entries[120].title := "Spin Buttons";
+   entries[120].keywords := "GtkEntry";
+   entries[120].description := "GtkSpinButton provides convenient ways to input data that can be seen as a value in a range. The examples here show that this does not necessarily mean numeric values, and it can include custom formatting.";
+   entries[121].name := "spinner";
+   entries[121].title := "Spinner";
+   entries[121].keywords := "";
+   entries[121].description := "GtkSpinner allows to show that background activity is on-going.";
+   entries[122].name := "stack";
+   entries[122].title := "Stack";
+   entries[122].keywords := "";
+   entries[122].description := "GtkStack is a container that shows a single child at a time, with nice transitions when the visible child changes. GtkStackSwitcher adds buttons to control which child is visible.";
+   entries[123].name := "sidebar";
+   entries[123].title := "Stack Sidebar";
+   entries[123].keywords := "";
+   entries[123].description := "GtkStackSidebar provides an automatic sidebar widget to control navigation of a GtkStack object. This widget automatically updates its content based on what is presently available in the GtkStack object, and using the 'title' child property to set the display labels.";
+   entries[124].name := "suggestionentry";
+   entries[124].title := "suggestionentry";
+   entries[124].keywords := "";
+   entries[124].description := "";
+   entries[125].name := "svg_widget";
+   entries[125].title := "SVG";
+   entries[125].keywords := "";
+   entries[125].description := "This demo uses a GtkSvgWidget to display a scalable, animated, interactive SVG image. Hover over the faces of the cube to see tooltips, click on them to activate links. Keyboard navigation with Tab and Enter works as well.";
+   entries[126].name := "textscroll";
+   entries[126].title := "Text View/Automatic Scrolling";
+   entries[126].keywords := "GtkTextView, GtkScrolledWindow";
+   entries[126].description := "This example demonstrates how to use the gravity of GtkTextMarks to keep a text view scrolled to the bottom while appending text.";
+   entries[127].name := "hypertext";
+   entries[127].title := "Text View/Hypertext";
+   entries[127].keywords := "GtkTextView, GtkTextBuffer";
+   entries[127].description := "Usually, tags modify the appearance of text in the view, e.g. making it bold or colored or underlined. But tags are not restricted to appearance. They can also affect the behavior of mouse and key presses, as this demo shows. We also demonstrate adding other things to a text view, such as clickable icons and widgets which can also replace a character (try copying the ghost text).";
+   entries[128].name := "markup";
+   entries[128].title := "Text View/Markup";
+   entries[128].keywords := "GtkTextView";
+   entries[128].description := "GtkTextBuffer lets you define your own tags that can influence text formatting in a variety of ways. In this example, we show that GtkTextBuffer can load Pango markup and automatically generate suitable tags.";
+   entries[129].name := "textview";
+   entries[129].title := "Text View/Multiple Views";
+   entries[129].keywords := "";
+   entries[129].description := "The GtkTextView widget displays a GtkTextBuffer. One GtkTextBuffer can be displayed by multiple GtkTextViews. This demo has two views displaying a single buffer, and shows off the widget's text formatting features.";
+   entries[130].name := "tabs";
+   entries[130].title := "Text View/Tabs";
+   entries[130].keywords := "";
+   entries[130].description := "GtkTextView can position text at fixed positions, using tabs. Tabs can specify alignment, and also allow aligning numbers on the decimal point. The example here has three tabs, with left, numeric and right alignment.";
+   entries[131].name := "textundo";
+   entries[131].title := "Text View/Undo and Redo";
+   entries[131].keywords := "";
+   entries[131].description := "The GtkTextView supports undo and redo through the use of a GtkTextBuffer. You can enable or disable undo support using gtk_text_buffer_set_enable_undo(). Use Control+z to undo and Control+Shift+z or Control+y to redo previously undone operations.";
+   entries[132].name := "gtkgears";
+   entries[132].title := "The rendering code in here is taken from es2gears, which has the";
+   entries[132].keywords := "";
+   entries[132].description := "following copyright notice: Copyright (C) 1999-2001 Brian Paul All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following condit";
+   entries[133].name := "css_pixbufs";
+   entries[133].title := "Theming/Animated Backgrounds";
+   entries[133].keywords := "";
+   entries[133].description := "This demo is in honour of a classic Pixbufs demo. It is done exclusively with CSS as the background of the window.";
+   entries[134].name := "css_accordion";
+   entries[134].title := "Theming/CSS Accordion";
+   entries[134].keywords := "";
+   entries[134].description := "A simple accordion demo written using CSS transitions and multiple backgrounds";
+   entries[135].name := "css_basics";
+   entries[135].title := "Theming/CSS Basics";
+   entries[135].keywords := "";
+   entries[135].description := "GTK themes are written using CSS. Every widget is build of multiple items that you can style very similarly to a regular website.";
+   entries[136].name := "css_blendmodes";
+   entries[136].title := "Theming/CSS Blend Modes";
+   entries[136].keywords := "";
+   entries[136].description := "You can blend multiple backgrounds using the CSS blend modes available.";
+   entries[137].name := "css_multiplebgs";
+   entries[137].title := "Theming/Multiple Backgrounds";
+   entries[137].keywords := "";
+   entries[137].description := "GTK themes are written using CSS. Every widget is build of multiple items that you can style very similarly to a regular website.";
+   entries[138].name := "css_shadows";
+   entries[138].title := "Theming/Shadows";
+   entries[138].keywords := "";
+   entries[138].description := "This demo shows how to use CSS shadows.";
+   entries[139].name := "theming_style_classes";
+   entries[139].title := "Theming/Style Classes";
+   entries[139].keywords := "";
+   entries[139].description := "GTK uses CSS for theming. Style classes can be associated with widgets to inform the theme about intended rendering. This demo shows some common examples where theming features of GTK are used for certain effects: primary toolbars and linked buttons.";
+   entries[140].name := "editable_cells";
+   entries[140].title := "Tree View/Editable Cells";
+   entries[140].keywords := "";
+   entries[140].description := "This demo demonstrates the use of editable cells in a GtkTreeView. If you're new to the GtkTreeView widgets and associates, look into the GtkListStore example first. It also shows how to use the GtkCellRenderer::editing-started signal to do custom setup of the editable widget. The cell renderers used in this demo are GtkCellRendererText, GtkCellRendererCombo and GtkCellRendererProgress.";
+   entries[141].name := "filtermodel";
+   entries[141].title := "Tree View/Filter Model";
+   entries[141].keywords := "GtkTreeView";
+   entries[141].description := "This example demonstrates how GtkTreeModelFilter can be used not just to show a subset of the rows, but also to compute columns that are not actually present in the underlying model.";
+   entries[142].name := "list_store";
+   entries[142].title := "Tree View/List Store";
+   entries[142].keywords := "";
+   entries[142].description := "The GtkListStore is used to store data in list form, to be used later on by a GtkTreeView to display it. This demo builds a simple GtkListStore and displays it.";
+   entries[143].name := "tree_store";
+   entries[143].title := "Tree View/Tree Store";
+   entries[143].keywords := "";
+   entries[143].description := "The GtkTreeStore is used to store data in tree form, to be used later on by a GtkTreeView to display it. This demo builds a simple GtkTreeStore and displays it. If you're new to the GtkTreeView widgets and associates, look into the GtkListStore example first.";
+   entries[144].name := "unicode-names";
+   entries[144].title := "unicode-names";
+   entries[144].keywords := "";
+   entries[144].description := "";
+   entries[145].name := "video_player";
+   entries[145].title := "Video Player";
+   entries[145].keywords := "GtkMediaControls";
+   entries[145].description := "This is a simple video player using just GTK widgets.";
+END DemoCatalog.

+ 10 - 11
demos/GtkDemo.def

@@ -1,26 +1,25 @@
 DEFINITION MODULE GtkDemo ;
 
 (*
-   m2-GTK4 - the demo registry, mirroring the gtk-demo model: a list of
-   demos, each with a title, search keywords and a build function of the
-   form
+   m2-GTK4 - the registry of *ported* gtk-demo demos: maps a demo name
+   (as used by tools/extract_demos.py and the C sources, e.g.
+   "aspect_frame") to its build procedure.  The full catalogue of demos
+   lives in DemoCatalog; this registry only knows which ones are
+   implemented.
 
-      BuildProc = PROCEDURE (ADDRESS) : ADDRESS;
-
-   which creates (once), toggles and returns the demo's window.
+   BuildByName() returns NIL when a demo has not been ported yet.
 *)
 
 FROM SYSTEM IMPORT ADDRESS;
 
-EXPORT UNQUALIFIED BuildProc,
-   Count, TitleOf, KeywordsOf, BuildByTitle;
+EXPORT UNQUALIFIED BuildProc, Count, NameOf, BuildByName, IsPorted;
 
 TYPE
    BuildProc = PROCEDURE (ADDRESS) : ADDRESS;
 
 PROCEDURE Count () : CARDINAL;
-PROCEDURE TitleOf (i: CARDINAL; VAR title: ARRAY OF CHAR);
-PROCEDURE KeywordsOf (i: CARDINAL; VAR keywords: ARRAY OF CHAR);
-PROCEDURE BuildByTitle (title: ARRAY OF CHAR; doWidget: ADDRESS) : ADDRESS;
+PROCEDURE NameOf (i: CARDINAL; VAR name: ARRAY OF CHAR);
+PROCEDURE BuildByName (name: ARRAY OF CHAR; doWidget: ADDRESS) : ADDRESS;
+PROCEDURE IsPorted (name: ARRAY OF CHAR) : BOOLEAN;
 
 END GtkDemo.

+ 28 - 23
demos/GtkDemo.mod

@@ -5,13 +5,12 @@ FROM SYSTEM IMPORT ADDRESS;
 
 TYPE
    Demo = RECORD
-      title: ARRAY [0..63] OF CHAR;
-      keywords: ARRAY [0..127] OF CHAR;
+      name: ARRAY [0..47] OF CHAR;
       build: BuildProc;
    END;
 
 VAR
-   demos: ARRAY [0..31] OF Demo;
+   demos: ARRAY [0..255] OF Demo;
    n: CARDINAL;
 
 PROCEDURE Copy (VAR dst: ARRAY OF CHAR; src: ARRAY OF CHAR);
@@ -38,11 +37,10 @@ BEGIN
    RETURN a[i] = b[i]
 END Equal;
 
-PROCEDURE Register (title, keywords: ARRAY OF CHAR; build: BuildProc);
+PROCEDURE Register (name: ARRAY OF CHAR; build: BuildProc);
 BEGIN
    IF n <= HIGH(demos) THEN
-      Copy(demos[n].title, title);
-      Copy(demos[n].keywords, keywords);
+      Copy(demos[n].name, name);
       demos[n].build := build;
       INC(n)
    END
@@ -53,35 +51,42 @@ BEGIN
    RETURN n
 END Count;
 
-PROCEDURE TitleOf (i: CARDINAL; VAR title: ARRAY OF CHAR);
+PROCEDURE NameOf (i: CARDINAL; VAR name: ARRAY OF CHAR);
 BEGIN
-   IF i < n THEN Copy(title, demos[i].title) ELSE title[0] := 0C END
-END TitleOf;
+   IF i < n THEN Copy(name, demos[i].name) ELSE name[0] := 0C END
+END NameOf;
 
-PROCEDURE KeywordsOf (i: CARDINAL; VAR keywords: ARRAY OF CHAR);
+PROCEDURE BuildByName (name: ARRAY OF CHAR; doWidget: ADDRESS) : ADDRESS;
+VAR
+   i: CARDINAL;
 BEGIN
-   IF i < n THEN Copy(keywords, demos[i].keywords) ELSE keywords[0] := 0C END
-END KeywordsOf;
+   i := 0;
+   WHILE i < n DO
+      IF Equal(demos[i].name, name) THEN
+         RETURN demos[i].build(doWidget)
+      END;
+      INC(i)
+   END;
+   RETURN NIL
+END BuildByName;
 
-PROCEDURE BuildByTitle (title: ARRAY OF CHAR; doWidget: ADDRESS) : ADDRESS;
+PROCEDURE IsPorted (name: ARRAY OF CHAR) : BOOLEAN;
 VAR
    i: CARDINAL;
-   buf: ARRAY [0..63] OF CHAR;
 BEGIN
    i := 0;
    WHILE i < n DO
-      TitleOf(i, buf);
-      IF Equal(buf, title) THEN RETURN demos[i].build(doWidget) END;
+      IF Equal(demos[i].name, name) THEN RETURN TRUE END;
       INC(i)
    END;
-   RETURN NIL
-END BuildByTitle;
+   RETURN FALSE
+END IsPorted;
 
 BEGIN
    n := 0;
-   Register("Links", "hyperlinks markup label uri anchor", DoLinks);
-   Register("List Box", "list rows selection box", DoListBox);
-   Register("Flow Box", "flow grid reflow colors swatches", DoFlowBox);
-   Register("Expander", "disclosure triangle expand collapse", DoExpander);
-   Register("CSS Basics", "css styling theme provider", DoCss)
+   Register("links", DoLinks);
+   Register("listbox", DoListBox);
+   Register("flowbox", DoFlowBox);
+   Register("expander", DoExpander);
+   Register("css_basics", DoCss)
 END GtkDemo.

+ 175 - 64
demos/m2gtkdemo.mod

@@ -3,9 +3,11 @@ MODULE m2gtkdemo ;
 (*
    m2-GTK4 - an m2 gtk-demo.
 
-   A browser window with a searchable sidebar of demos; activating a row
-   (double-click / Enter) or pressing "Run" opens the demo in its own
-   window, mirroring the GTK demo application.
+   A browser in the spirit of GTK's own gtk4-demo: a searchable sidebar
+   listing every demo in DemoCatalog, and an info pane describing the
+   selected demo.  Activating a demo (double-click / Enter) or pressing
+   "Run" opens it in its own window; demos that have not been ported yet
+   are shown as such.
 
    Build:  make demos
    Run:    ./build/demos/m2gtkdemo
@@ -17,16 +19,21 @@ FROM GtkApplicationWindow IMPORT gtk_application_window_new;
 FROM GtkWindow IMPORT gtk_window_set_title, gtk_window_set_default_size,
    gtk_window_set_child, gtk_window_present;
 FROM GtkBox IMPORT gtk_box_new, gtk_box_append, gtk_box_set_spacing;
+FROM GtkPaned IMPORT gtk_paned_new, gtk_paned_set_start_child,
+   gtk_paned_set_end_child, gtk_paned_set_position;
 FROM GtkEnums IMPORT GtkVertical, GtkHorizontal;
 FROM GtkWidget IMPORT gtk_widget_set_size_request, gtk_widget_set_vexpand,
    gtk_widget_set_hexpand, gtk_widget_set_margin_start,
    gtk_widget_set_margin_end, gtk_widget_set_margin_top,
    gtk_widget_set_margin_bottom;
-FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_text;
+FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_text,
+   gtk_label_set_wrap, gtk_label_set_max_width_chars;
 FROM GtkButton IMPORT gtk_button_new_with_label;
 FROM GtkSearchEntry IMPORT gtk_search_entry_new,
    gtk_search_entry_set_placeholder_text;
 FROM GtkEditable IMPORT gtk_editable_get_text;
+FROM GtkScrolledWindow IMPORT gtk_scrolled_window_new,
+   gtk_scrolled_window_set_child;
 FROM GtkListView IMPORT gtk_list_view_new;
 FROM GtkListItem IMPORT gtk_list_item_get_item, gtk_list_item_get_child,
    gtk_list_item_set_child;
@@ -42,7 +49,8 @@ FROM GListModel IMPORT g_list_model_get_item;
 FROM GObject IMPORT g_signal_connect_data, g_object_unref, GConnectDefault;
 FROM GtkClosures IMPORT Connect2, Connect3;
 FROM GtkUtils IMPORT Connect, CStrToM2;
-FROM GtkDemo IMPORT Count, TitleOf, BuildByTitle;
+FROM GtkDemo IMPORT BuildByName, IsPorted;
+FROM DemoCatalog IMPORT DemoCatalogCount, DemoCatalogGet;
 FROM SYSTEM IMPORT ADDRESS, ADR;
 FROM libc IMPORT printf;
 
@@ -51,8 +59,9 @@ CONST
    Invalid = 0FFFFFFFFH;
 
 VAR
-   window, filter, filterModel, selection: ADDRESS;
-   searchText: ARRAY [0..63] OF CHAR;
+   window, filter, filterModel, selection, infoTitle, infoBody, infoStatus,
+   searchEntry: ADDRESS;
+   query: ARRAY [0..63] OF CHAR;
 
 PROCEDURE Contains (hay, needle: ARRAY OF CHAR) : BOOLEAN;
 VAR
@@ -79,49 +88,127 @@ BEGIN
    RETURN FALSE
 END Contains;
 
-(* gboolean match(gpointer item, gpointer user_data) - match the title *)
+PROCEDURE CopyStr (VAR dst: ARRAY OF CHAR; src: ARRAY OF CHAR);
+VAR
+   i: CARDINAL;
+BEGIN
+   i := 0;
+   WHILE (i <= HIGH(dst)) AND (i <= HIGH(src)) AND (src[i] # 0C) DO
+      dst[i] := src[i];
+      INC(i)
+   END;
+   IF i <= HIGH(dst) THEN dst[i] := 0C END
+END CopyStr;
+
+PROCEDURE LookupByName (name: ARRAY OF CHAR;
+                        VAR title, keywords, description: ARRAY OF CHAR);
+VAR
+   i: CARDINAL;
+   n, t, k, d: ARRAY [0..511] OF CHAR;
+BEGIN
+   i := 0;
+   WHILE i < DemoCatalogCount() DO
+      DemoCatalogGet(i, n, t, k, d);
+      IF Contains(n, name) AND Contains(name, n) THEN
+         CopyStr(title, t); CopyStr(keywords, k); CopyStr(description, d);
+         RETURN
+      END;
+      INC(i)
+   END;
+   title[0] := 0C; keywords[0] := 0C; description[0] := 0C
+END LookupByName;
+
+(* gboolean match(gpointer item, gpointer user) - item is a demo name *)
 PROCEDURE Match (item: ADDRESS; user: ADDRESS) : INTEGER;
 VAR
-   buf: ARRAY [0..63] OF CHAR;
+   name, title, keywords, description: ARRAY [0..511] OF CHAR;
 BEGIN
-   CStrToM2(gtk_string_object_get_string(item), buf);
-   IF Contains(buf, searchText) THEN RETURN 1 ELSE RETURN 0 END
+   CStrToM2(gtk_string_object_get_string(item), name);
+   LookupByName(name, title, keywords, description);
+   IF Contains(title, query) OR Contains(keywords, query) THEN
+      RETURN 1
+   ELSE
+      RETURN 0
+   END
 END Match;
 
-PROCEDURE OnSearchChanged (entry: ADDRESS; user: ADDRESS);
+PROCEDURE UpdateInfo (name: ARRAY OF CHAR);
+VAR
+   title, keywords, description: ARRAY [0..1023] OF CHAR;
 BEGIN
-   CStrToM2(gtk_editable_get_text(entry), searchText);
-   gtk_custom_filter_set_filter_func(filter, ADR(Match), NIL, NIL)
-END OnSearchChanged;
+   LookupByName(name, title, keywords, description);
 
-PROCEDURE RunPosition (position: CARDINAL);
+   gtk_label_set_text(infoTitle, title);
+   gtk_label_set_text(infoBody, description);
+   IF IsPorted(name) THEN
+      gtk_label_set_text(infoStatus, "Ported to Modula-2.  Press Run.")
+   ELSE
+      gtk_label_set_text(infoStatus, "Not ported yet.")
+   END
+END UpdateInfo;
+
+PROCEDURE NameAtSelection (VAR name: ARRAY OF CHAR);
 VAR
-   item, demoWindow: ADDRESS;
-   title: ARRAY [0..63] OF CHAR;
+   index: CARDINAL;
+   item: ADDRESS;
 BEGIN
-   item := g_list_model_get_item(filterModel, position);
-   CStrToM2(gtk_string_object_get_string(item), title);
-   g_object_unref(item);
-   demoWindow := BuildByTitle(title, window);
-   IF demoWindow = NIL THEN
-      printf("no demo named %s\n", title)
+   name[0] := 0C;
+   index := gtk_single_selection_get_selected(selection);
+   IF index # Invalid THEN
+      item := g_list_model_get_item(filterModel, index);
+      CStrToM2(gtk_string_object_get_string(item), name);
+      g_object_unref(item)
    END
-END RunPosition;
+END NameAtSelection;
+
+PROCEDURE RunSelected;
+VAR
+   name: ARRAY [0..511] OF CHAR;
+   demoWindow: ADDRESS;
+BEGIN
+   NameAtSelection(name);
+   IF name[0] # 0C THEN
+      UpdateInfo(name);
+      IF IsPorted(name) THEN
+         demoWindow := BuildByName(name, window);
+         IF demoWindow = NIL THEN
+            printf("demo %s could not be built\n", name)
+         END
+      END
+   END
+END RunSelected;
 
 (* void activate(GtkListView *list, guint position, gpointer user) *)
 PROCEDURE OnActivate (list: ADDRESS; position: CARDINAL; user: ADDRESS);
+VAR
+   item, demoWindow: ADDRESS;
+   name: ARRAY [0..511] OF CHAR;
 BEGIN
-   RunPosition(position)
+   item := g_list_model_get_item(filterModel, position);
+   CStrToM2(gtk_string_object_get_string(item), name);
+   g_object_unref(item);
+   UpdateInfo(name);
+   IF IsPorted(name) THEN
+      demoWindow := BuildByName(name, window)
+   END
 END OnActivate;
 
 PROCEDURE OnRun (button: ADDRESS; user: ADDRESS);
-VAR
-   index: CARDINAL;
 BEGIN
-   index := gtk_single_selection_get_selected(selection);
-   IF index # Invalid THEN RunPosition(index) END
+   RunSelected
 END OnRun;
 
+PROCEDURE OnSelectionChanged (sel: ADDRESS; user: ADDRESS);
+BEGIN
+   RunSelected
+END OnSelectionChanged;
+
+PROCEDURE OnSearchChanged (entry: ADDRESS; user: ADDRESS);
+BEGIN
+   CStrToM2(gtk_editable_get_text(entry), query);
+   gtk_custom_filter_set_filter_func(filter, ADR(Match), NIL, NIL)
+END OnSearchChanged;
+
 PROCEDURE RowSetup (factory: ADDRESS; item: ADDRESS; user: ADDRESS);
 BEGIN
    gtk_list_item_set_child(item, gtk_label_new(""))
@@ -130,27 +217,28 @@ END RowSetup;
 PROCEDURE RowBind (factory: ADDRESS; item: ADDRESS; user: ADDRESS);
 VAR
    obj, child: ADDRESS;
-   buf: ARRAY [0..63] OF CHAR;
+   name, title, keywords, description: ARRAY [0..511] OF CHAR;
 BEGIN
    obj := gtk_list_item_get_item(item);
-   CStrToM2(gtk_string_object_get_string(obj), buf);
+   CStrToM2(gtk_string_object_get_string(obj), name);
+   LookupByName(name, title, keywords, description);
    child := gtk_list_item_get_child(item);
-   gtk_label_set_text(child, buf)
+   gtk_label_set_text(child, title)
 END RowBind;
 
 PROCEDURE OnActivateApp (application: ADDRESS; data: ADDRESS);
 VAR
-   root, content, search, listView, factory, store, runButton, hint: ADDRESS;
-   title: ARRAY [0..63] OF CHAR;
+   root, paned, sidebar, listView, factory, store, runButton, info: ADDRESS;
+   name, title, keywords, description: ARRAY [0..511] OF CHAR;
    i: CARDINAL;
 BEGIN
-   searchText[0] := 0C;
+   query[0] := 0C;
 
    store := gtk_string_list_new(NIL);
    i := 0;
-   WHILE i < Count() DO
-      TitleOf(i, title);
-      gtk_string_list_append(store, title);
+   WHILE i < DemoCatalogCount() DO
+      DemoCatalogGet(i, name, title, keywords, description);
+      gtk_string_list_append(store, name);
       INC(i)
    END;
 
@@ -161,42 +249,65 @@ BEGIN
    factory := gtk_signal_list_item_factory_new();
    Connect3(factory, "setup", RowSetup, NIL);
    Connect3(factory, "bind", RowBind, NIL);
+
    listView := gtk_list_view_new(selection, factory);
-   gtk_widget_set_size_request(listView, 200, -1);
    gtk_widget_set_vexpand(listView, 1);
    g_signal_connect_data(listView, "activate", ADR(OnActivate), NIL, NIL,
                          GConnectDefault);
+   Connect2(selection, "notify::selected", OnSelectionChanged, NIL);
+
+   searchEntry := gtk_search_entry_new();
+   gtk_search_entry_set_placeholder_text(searchEntry, "Search demos");
+   Connect2(searchEntry, "search-changed", OnSearchChanged, NIL);
+
+   sidebar := gtk_box_new(GtkVertical, 6);
+   gtk_box_set_spacing(sidebar, 6);
+   gtk_box_append(sidebar, searchEntry);
+   gtk_box_append(sidebar, listView);
+   gtk_widget_set_size_request(sidebar, 240, -1);
+
+   infoTitle := gtk_label_new("");
+   gtk_label_set_wrap(infoTitle, 1);
+   gtk_label_set_max_width_chars(infoTitle, 40);
+   gtk_widget_set_margin_start(infoTitle, 12);
+   gtk_widget_set_margin_end(infoTitle, 12);
+   gtk_widget_set_margin_top(infoTitle, 12);
+
+   infoBody := gtk_label_new("");
+   gtk_label_set_wrap(infoBody, 1);
+   gtk_label_set_max_width_chars(infoBody, 50);
+   gtk_widget_set_margin_start(infoBody, 12);
+   gtk_widget_set_margin_end(infoBody, 12);
+
+   infoStatus := gtk_label_new("");
+   gtk_label_set_wrap(infoStatus, 1);
+   gtk_widget_set_margin_start(infoStatus, 12);
+   gtk_widget_set_margin_end(infoStatus, 12);
 
    runButton := gtk_button_new_with_label("Run selected demo");
+   gtk_widget_set_margin_start(runButton, 12);
+   gtk_widget_set_margin_end(runButton, 12);
+   gtk_widget_set_margin_bottom(runButton, 12);
    Connect2(runButton, "clicked", OnRun, NIL);
 
-   hint := gtk_label_new("Pick a demo, then Run (or double-click).");
-   gtk_widget_set_margin_start(hint, 8);
-
-   search := gtk_search_entry_new();
-   gtk_search_entry_set_placeholder_text(search, "Search demos");
-   Connect2(search, "search-changed", OnSearchChanged, NIL);
-
-   content := gtk_box_new(GtkHorizontal, 8);
-   gtk_box_set_spacing(content, 8);
-   gtk_widget_set_vexpand(content, 1);
-   gtk_box_append(content, listView);
-   gtk_box_append(content, runButton);
-   gtk_widget_set_hexpand(runButton, 0);
-
-   root := gtk_box_new(GtkVertical, 8);
-   gtk_box_set_spacing(root, 8);
-   gtk_widget_set_margin_start(root, 10);
-   gtk_widget_set_margin_end(root, 10);
-   gtk_widget_set_margin_top(root, 10);
-   gtk_widget_set_margin_bottom(root, 10);
-   gtk_box_append(root, search);
-   gtk_box_append(root, content);
-   gtk_box_append(root, hint);
+   info := gtk_box_new(GtkVertical, 10);
+   gtk_box_set_spacing(info, 10);
+   gtk_box_append(info, infoTitle);
+   gtk_box_append(info, infoBody);
+   gtk_box_append(info, infoStatus);
+   gtk_box_append(info, runButton);
+
+   paned := gtk_paned_new(GtkHorizontal);
+   gtk_paned_set_start_child(paned, sidebar);
+   gtk_paned_set_end_child(paned, info);
+   gtk_paned_set_position(paned, 260);
+
+   root := gtk_box_new(GtkVertical, 0);
+   gtk_box_append(root, paned);
 
    window := gtk_application_window_new(application);
    gtk_window_set_title(window, "m2-GTK4 Demo");
-   gtk_window_set_default_size(window, 480, 360);
+   gtk_window_set_default_size(window, 760, 520);
    gtk_window_set_child(window, root);
    gtk_window_present(window)
 END OnActivateApp;

+ 3 - 1
tests/run_tests.sh

@@ -52,6 +52,8 @@ ${CC:-cc} $CFLAGS -c "$ROOT/lib/m2gtkshim.c" -o "$OBJ/m2gtkshim.o"
 
 # the m2 gtk-demo registry (demos/) is compiled once and linked into test_demos.
 if [ -d "$ROOT/demos" ]; then
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemoCatalog.mod" -o "$OBJ/DemoCatalog.o"
   # shellcheck disable=SC2086
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemoImpl.mod" -o "$OBJ/DemoImpl.o"
   # shellcheck disable=SC2086
@@ -71,7 +73,7 @@ pass=0; fail=0; skip=0
 for t in $TESTS; do
   echo "== $t =="
   DEMO_OBJS=""
-  case "$t" in test_demos) DEMO_OBJS="$OBJ/DemoImpl.o $OBJ/GtkDemo.o" ;; esac
+  case "$t" in test_demos) DEMO_OBJS="$OBJ/DemoCatalog.o $OBJ/DemoImpl.o $OBJ/GtkDemo.o" ;; esac
   # shellcheck disable=SC2086
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS $CFLAGS \
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" \

+ 27 - 28
tests/test_demos.mod

@@ -1,14 +1,15 @@
 MODULE test_demos ;
 
 (*
-   m2-GTK4 test - the m2 gtk-demo registry (demos/).  Checks that the
-   registry is populated and that every demo can be built and torn down.
-   Requires a display.
+   m2-GTK4 test - the m2 gtk-demo catalogue and registry (demos/).
+   Checks that the full catalogue is present and that ported demos can be
+   built and torn down.  Requires a display.
 *)
 
 FROM GtkApplication IMPORT gtk_application_new;
 FROM Gio IMPORT g_application_run, g_application_quit;
-FROM GtkDemo IMPORT Count, TitleOf, KeywordsOf, BuildByTitle;
+FROM GtkDemo IMPORT Count, NameOf, IsPorted, BuildByName;
+FROM DemoCatalog IMPORT DemoCatalogCount, DemoCatalogGet;
 FROM GtkUtils IMPORT Connect;
 FROM SYSTEM IMPORT ADDRESS, ADR;
 FROM libc IMPORT printf;
@@ -43,35 +44,37 @@ BEGIN
 END Check;
 
 (* build a demo once (creates + shows its window), then toggle it off. *)
-PROCEDURE BuildAndClose (title: ARRAY OF CHAR);
+PROCEDURE BuildAndClose (name: ARRAY OF CHAR);
 VAR
    w: ADDRESS;
 BEGIN
-   w := BuildByTitle(title, NIL);
-   Check(w # NIL, title);
-   IF w # NIL THEN
-      w := BuildByTitle(title, NIL)
-   END
+   w := BuildByName(name, NIL);
+   Check(w # NIL, name);
+   IF w # NIL THEN w := BuildByName(name, NIL) END
 END BuildAndClose;
 
 PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
 VAR
-   t, kw: ARRAY [0..127] OF CHAR;
+   name, title, keywords, description: ARRAY [0..511] OF CHAR;
 BEGIN
    failures := 0;
 
-   Check(Count() = 5, "registry reports 5 demos");
-   TitleOf(0, t);
-   Check(StrEqual(t, "Links"), "demo 0 is 'Links'");
-   KeywordsOf(0, kw);
-   Check(NOT StrEqual(kw, ""), "demo 0 has search keywords");
-   Check(BuildByTitle("NoSuchDemo", NIL) = NIL, "unknown demo yields NIL");
+   Check(DemoCatalogCount() >= 140, "catalogue has >= 140 demos");
+   DemoCatalogGet(0, name, title, keywords, description);
+   Check(name[0] # 0C, "catalogue entry 0 has a name");
+
+   Check(Count() >= 5, "registry has >= 5 ported demos");
+   NameOf(0, name);
+   Check(name[0] # 0C, "registry entry 0 has a name");
+   Check(IsPorted("links"), "links is ported");
+   Check(IsPorted("aspect_frame") = FALSE, "aspect_frame not ported yet");
+   Check(BuildByName("does_not_exist", NIL) = NIL, "unknown demo yields NIL");
 
-   BuildAndClose("Links");
-   BuildAndClose("List Box");
-   BuildAndClose("Flow Box");
-   BuildAndClose("Expander");
-   BuildAndClose("CSS Basics");
+   BuildAndClose("links");
+   BuildAndClose("listbox");
+   BuildAndClose("flowbox");
+   BuildAndClose("expander");
+   BuildAndClose("css_basics");
 
    IF failures = 0 THEN
       printf("test_demos: PASS\n")
@@ -84,7 +87,7 @@ END OnActivate;
 VAR
    rc: INTEGER;
 BEGIN
-   failures := 1;   (* set to 0 by OnActivate; stays 1 if it never runs *)
+   failures := 1;
    app := gtk_application_new(AppId, 0);
    IF app = NIL THEN
       printf("test_demos: FAIL (no app)\n");
@@ -92,9 +95,5 @@ BEGIN
    END;
    Connect(app, "activate", ADR(OnActivate), NIL);
    rc := g_application_run(app, 0, NIL);
-   IF failures = 0 THEN
-      HALT(0)
-   ELSE
-      HALT(1)
-   END
+   IF failures = 0 THEN HALT(0) ELSE HALT(1) END
 END test_demos.

+ 142 - 0
tools/extract_demos.py

@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""extract_demos.py - build the m2 gtk-demo catalog from the GTK C demo sources.
+
+Reads the per-demo `.c` files under gtk/demos/gtk-demo and emits
+`demos/DemoCatalog.def` / `demos/DemoCatalog.mod`: the metadata (name,
+title, keywords, description) of every demo, so the Modula-2 browser can
+list and search them.
+
+Usage:
+    tools/extract_demos.py [--src gtk/demos/gtk-demo] [--out demos]
+"""
+import argparse
+import glob
+import os
+import re
+import sys
+
+MAX_NAME = 48
+MAX_TITLE = 80
+MAX_KEYWORDS = 256
+MAX_DESCRIPTION = 512
+
+
+def m2_str(text, limit):
+    """Escape a Python string for a Modula-2 string literal of bounded size."""
+    text = " ".join(text.split())
+    if len(text) > limit:
+        text = text[:limit]
+    text = text.encode("ascii", "replace").decode("ascii")
+    text = text.replace("\\", " ").replace('"', "'")
+    return text
+
+
+def parse(path):
+    src = open(path, encoding="utf-8", errors="replace").read()
+    name = os.path.basename(path)[:-2]
+    title = ""
+    keywords = ""
+    description = ""
+    m = re.match(r"\ufeff?\s*/\*(.*?)\*/", src, re.S)
+    if m:
+        lines = [l.strip(" *\t") for l in m.group(1).splitlines()]
+        lines = [l for l in lines if l]
+        if lines:
+            title = lines[0]
+        desc = []
+        for l in lines[1:]:
+            if l.startswith("#Keywords:"):
+                keywords = l.split(":", 1)[1].strip()
+            else:
+                desc.append(l)
+        description = " ".join(desc)
+    # the do_<name> function defines the demo id used by the C registry
+    ids = re.findall(r"\bdo_([a-z0-9_]+)\s*\(", src)
+    if ids:
+        name = ids[0]
+    return name, title, keywords, description
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--src", default="gtk/demos/gtk-demo")
+    ap.add_argument("--out", default="demos")
+    args = ap.parse_args()
+
+    files = sorted(glob.glob(os.path.join(args.src, "*.c")))
+    entries = []
+    for f in files:
+        name, title, kw, desc = parse(f)
+        if name == "main":
+            continue
+        entries.append((name, title or name.replace("_", " "), kw, desc))
+
+    entries.sort(key=lambda e: e[1].lower())
+
+    os.makedirs(args.out, exist_ok=True)
+
+    with open(os.path.join(args.out, "DemoCatalog.def"), "w") as f:
+        f.write(
+            'DEFINITION MODULE DemoCatalog ;\n\n'
+            '(*\n'
+            '   m2-GTK4 - the catalogue of gtk-demo demos, generated from the\n'
+            '   GTK C demo sources by tools/extract_demos.py.  Provides the\n'
+            '   title/keywords metadata used by the demo browser.\n'
+            '*)\n\n'
+            'EXPORT UNQUALIFIED DemoCatalogCount, DemoCatalogGet;\n\n'
+            'PROCEDURE DemoCatalogCount () : CARDINAL;\n\n'
+            '(* copy the metadata of demo i into the caller buffers *)\n'
+            'PROCEDURE DemoCatalogGet (i: CARDINAL; VAR name, title, keywords,\n'
+            '               description: ARRAY OF CHAR);\n\n'
+            'END DemoCatalog.\n'
+        )
+
+    with open(os.path.join(args.out, "DemoCatalog.mod"), "w") as f:
+        f.write("IMPLEMENTATION MODULE DemoCatalog ;\n\n")
+        f.write("TYPE\n   Entry = RECORD\n")
+        f.write("      name: ARRAY [0..%d] OF CHAR;\n" % (MAX_NAME - 1))
+        f.write("      title: ARRAY [0..%d] OF CHAR;\n" % (MAX_TITLE - 1))
+        f.write("      keywords: ARRAY [0..%d] OF CHAR;\n" % (MAX_KEYWORDS - 1))
+        f.write("      description: ARRAY [0..%d] OF CHAR;\n" % (MAX_DESCRIPTION - 1))
+        f.write("   END;\n\n")
+        f.write("VAR\n   entries: ARRAY [0..%d] OF Entry;\n   n: CARDINAL;\n\n" % (len(entries) - 1))
+        f.write(
+            "PROCEDURE Copy (VAR dst: ARRAY OF CHAR; src: ARRAY OF CHAR);\n"
+            "VAR i: CARDINAL;\n"
+            "BEGIN\n"
+            "   i := 0;\n"
+            "   WHILE (i <= HIGH(dst)) AND (i <= HIGH(src)) AND (src[i] # 0C) DO\n"
+            "      dst[i] := src[i]; INC(i)\n"
+            "   END;\n"
+            "   IF i <= HIGH(dst) THEN dst[i] := 0C END\n"
+            "END Copy;\n\n"
+            "PROCEDURE DemoCatalogCount () : CARDINAL;\nBEGIN\n   RETURN n\nEND DemoCatalogCount;\n\n"
+            "PROCEDURE DemoCatalogGet (i: CARDINAL; VAR name, title, keywords,\n"
+            "               description: ARRAY OF CHAR);\n"
+            "BEGIN\n"
+            "   IF i < n THEN\n"
+            "      Copy(name, entries[i].name);\n"
+            "      Copy(title, entries[i].title);\n"
+            "      Copy(keywords, entries[i].keywords);\n"
+            "      Copy(description, entries[i].description)\n"
+            "   ELSE\n"
+            "      name[0] := 0C; title[0] := 0C; keywords[0] := 0C;\n"
+            "      description[0] := 0C\n"
+            "   END\n"
+            "END DemoCatalogGet;\n\n"
+            "BEGIN\n"
+            "   n := %d;\n" % len(entries)
+        )
+        for i, (name, title, kw, desc) in enumerate(entries):
+            f.write('   entries[%d].name := "%s";\n' % (i, m2_str(name, MAX_NAME - 1)))
+            f.write('   entries[%d].title := "%s";\n' % (i, m2_str(title, MAX_TITLE - 1)))
+            f.write('   entries[%d].keywords := "%s";\n' % (i, m2_str(kw, MAX_KEYWORDS - 1)))
+            f.write('   entries[%d].description := "%s";\n' % (i, m2_str(desc, MAX_DESCRIPTION - 1)))
+        f.write("END DemoCatalog.\n")
+
+    sys.stderr.write("extract_demos: %d demos -> %s/DemoCatalog.{def,mod}\n"
+                     % (len(entries), args.out))
+
+
+if __name__ == "__main__":
+    main()