From 341ff232aec77c2b46325389da933315613b6f2d Mon Sep 17 00:00:00 2001 From: Caolán McNamara Date: Mon, 20 Mar 2023 20:37:15 +0000 Subject: gtk4: get a11y to say hello world MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit role mappings are somewhat unsatisfactory, but can serve as placeholders for now Change-Id: I050fdc0e85fc57de6d79823b2432b521dcffcad4 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/149215 Tested-by: Jenkins Reviewed-by: Caolán McNamara --- vcl/Library_vclplug_gtk4.mk | 1 + vcl/inc/unx/gtk/gtkframe.hxx | 4 +- vcl/unx/gtk3/gtkframe.cxx | 48 +---- vcl/unx/gtk4/a11y.cxx | 497 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 503 insertions(+), 47 deletions(-) create mode 100644 vcl/unx/gtk4/a11y.cxx diff --git a/vcl/Library_vclplug_gtk4.mk b/vcl/Library_vclplug_gtk4.mk index d9c19f5521b0..bb9d82ad570f 100644 --- a/vcl/Library_vclplug_gtk4.mk +++ b/vcl/Library_vclplug_gtk4.mk @@ -85,6 +85,7 @@ $(eval $(call gb_Library_add_exception_objects,vclplug_gtk4,\ vcl/unx/gtk4/fpicker/SalGtkFilePicker \ vcl/unx/gtk4/fpicker/SalGtkFolderPicker \ vcl/unx/gtk4/fpicker/SalGtkPicker \ + vcl/unx/gtk4/a11y \ vcl/unx/gtk4/convert3to4 \ vcl/unx/gtk4/customcellrenderer \ vcl/unx/gtk4/gtkdata \ diff --git a/vcl/inc/unx/gtk/gtkframe.hxx b/vcl/inc/unx/gtk/gtkframe.hxx index 27bbf77ffd54..d84abd09f7d3 100644 --- a/vcl/inc/unx/gtk/gtkframe.hxx +++ b/vcl/inc/unx/gtk/gtkframe.hxx @@ -658,14 +658,14 @@ public: void AllowCycleFocusOut(); }; -#if !GTK_CHECK_VERSION(4, 0, 0) extern "C" { GType ooo_fixed_get_type(); +#if !GTK_CHECK_VERSION(4, 0, 0) AtkObject* ooo_fixed_get_accessible(GtkWidget *obj); +#endif } // extern "C" -#endif #if !GTK_CHECK_VERSION(3, 22, 0) enum GdkAnchorHints diff --git a/vcl/unx/gtk3/gtkframe.cxx b/vcl/unx/gtk3/gtkframe.cxx index 383cf5d6edeb..824af112fa5d 100644 --- a/vcl/unx/gtk3/gtkframe.cxx +++ b/vcl/unx/gtk3/gtkframe.cxx @@ -844,48 +844,6 @@ void GtkSalFrame::resizeWindow( tools::Long nWidth, tools::Long nHeight ) window_resize(nWidth, nHeight); } -#if GTK_CHECK_VERSION(4,9,0) - -#define LO_TYPE_DRAWING_AREA (lo_drawing_area_get_type()) -#define LO_DRAWING_AREA(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LO_TYPE_DRAWING_AREA, LODrawingArea)) -#define LO_IS_DRAWING_AREA(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), LO_TYPE_DRAWING_AREA)) - -struct LODrawingArea -{ - GtkDrawingArea parent_instance; -}; - -struct LODrawingAreaClass -{ - GtkDrawingAreaClass parent_class; -}; - -static void lo_drawing_area_accessible_init(GtkAccessibleInterface *iface) -{ - // doesn't actually do anything useful yet, just forward back to base impl for now - GtkAccessibleInterface *parent_iface = static_cast(g_type_interface_peek_parent(iface)); - iface->get_at_context = parent_iface->get_at_context; - iface->get_platform_state = parent_iface->get_platform_state; -} - -G_DEFINE_TYPE_WITH_CODE(LODrawingArea, lo_drawing_area, GTK_TYPE_DRAWING_AREA, - G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, lo_drawing_area_accessible_init)) - -static void lo_drawing_area_class_init(LODrawingAreaClass* /*klass*/) -{ -} - -static void lo_drawing_area_init(LODrawingArea* /*area*/) -{ -} - -GtkWidget* lo_drawing_area_new() -{ - return GTK_WIDGET(g_object_new(LO_TYPE_DRAWING_AREA, nullptr)); -} - -#endif - #if !GTK_CHECK_VERSION(4,0,0) // tdf#124694 GtkFixed takes the max size of all its children as its // preferred size, causing it to not clip its child, but grow instead. @@ -1010,12 +968,12 @@ void GtkSalFrame::InitCommon() m_pDrawingArea = m_pFixedContainer; #else m_pOverlay = GTK_OVERLAY(gtk_overlay_new()); - m_pFixedContainer = GTK_FIXED(gtk_fixed_new()); #if GTK_CHECK_VERSION(4,9,0) - m_pDrawingArea = GTK_DRAWING_AREA(lo_drawing_area_new()); + m_pFixedContainer = GTK_FIXED(g_object_new( ooo_fixed_get_type(), nullptr )); #else - m_pDrawingArea = GTK_DRAWING_AREA(gtk_drawing_area_new()); + m_pFixedContainer = GTK_FIXED(gtk_fixed_new()); #endif + m_pDrawingArea = GTK_DRAWING_AREA(gtk_drawing_area_new()); #endif gtk_widget_set_can_focus(GTK_WIDGET(m_pFixedContainer), true); gtk_widget_set_size_request(GTK_WIDGET(m_pFixedContainer), 1, 1); diff --git a/vcl/unx/gtk4/a11y.cxx b/vcl/unx/gtk4/a11y.cxx new file mode 100644 index 000000000000..b63fccca9f24 --- /dev/null +++ b/vcl/unx/gtk4/a11y.cxx @@ -0,0 +1,497 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include +#include +#include +#include + +#if GTK_CHECK_VERSION(4, 9, 0) + +#define OOO_TYPE_FIXED (ooo_fixed_get_type()) +#define OOO_FIXED(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), OOO_TYPE_FIXED, OOoFixed)) +// #define OOO_IS_FIXED(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), OOO_TYPE_FIXED)) + +struct OOoFixed +{ + GtkFixed parent_instance; + GtkATContext* at_context; +}; + +struct OOoFixedClass +{ + GtkFixedClass parent_class; +}; + +GtkAccessibleRole +map_accessible_role(const css::uno::Reference& rAccessible) +{ + GtkAccessibleRole eRole(GTK_ACCESSIBLE_ROLE_WIDGET); + + if (rAccessible.is()) + { + css::uno::Reference xContext( + rAccessible->getAccessibleContext()); + // https://w3c.github.io/core-aam/#mapping_role + // https://hg.mozilla.org/mozilla-central/file/tip/accessible/base/RoleMap.h + // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gtk/a11y/gtkatspiutils.c + switch (xContext->getAccessibleRole()) + { + case css::accessibility::AccessibleRole::PANEL: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::ROOT_PANE: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::MENU_BAR: + eRole = GTK_ACCESSIBLE_ROLE_MENU_BAR; + break; + case css::accessibility::AccessibleRole::STATUS_BAR: + eRole = GTK_ACCESSIBLE_ROLE_STATUS; + break; + case css::accessibility::AccessibleRole::MENU: + eRole = GTK_ACCESSIBLE_ROLE_MENU; + break; + case css::accessibility::AccessibleRole::SPLIT_PANE: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::TOOL_BAR: + eRole = GTK_ACCESSIBLE_ROLE_TOOLBAR; + break; + case css::accessibility::AccessibleRole::LABEL: + eRole = GTK_ACCESSIBLE_ROLE_LABEL; + break; + case css::accessibility::AccessibleRole::MENU_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM; + break; + case css::accessibility::AccessibleRole::SEPARATOR: + eRole = GTK_ACCESSIBLE_ROLE_SEPARATOR; + break; + case css::accessibility::AccessibleRole::CHECK_MENU_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX; + break; + case css::accessibility::AccessibleRole::RADIO_MENU_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO; + break; + case css::accessibility::AccessibleRole::DOCUMENT: + case css::accessibility::AccessibleRole::DOCUMENT_PRESENTATION: + case css::accessibility::AccessibleRole::DOCUMENT_SPREADSHEET: + case css::accessibility::AccessibleRole::DOCUMENT_TEXT: + eRole = GTK_ACCESSIBLE_ROLE_DOCUMENT; + break; + case css::accessibility::AccessibleRole::RULER: + eRole = GTK_ACCESSIBLE_ROLE_WIDGET; + break; + case css::accessibility::AccessibleRole::PARAGRAPH: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::FILLER: + eRole = GTK_ACCESSIBLE_ROLE_GENERIC; + break; + case css::accessibility::AccessibleRole::PUSH_BUTTON: + eRole = GTK_ACCESSIBLE_ROLE_BUTTON; + break; + case css::accessibility::AccessibleRole::BUTTON_DROPDOWN: + eRole = GTK_ACCESSIBLE_ROLE_BUTTON; + break; + case css::accessibility::AccessibleRole::TOGGLE_BUTTON: + eRole = GTK_ACCESSIBLE_ROLE_TOGGLE_BUTTON; + break; + case css::accessibility::AccessibleRole::TABLE: + eRole = GTK_ACCESSIBLE_ROLE_TABLE; + break; + case css::accessibility::AccessibleRole::TABLE_CELL: + eRole = GTK_ACCESSIBLE_ROLE_CELL; + break; + case css::accessibility::AccessibleRole::PAGE_TAB_LIST: + eRole = GTK_ACCESSIBLE_ROLE_TAB_LIST; + break; + case css::accessibility::AccessibleRole::SHAPE: + eRole = GTK_ACCESSIBLE_ROLE_IMG; + break; + case css::accessibility::AccessibleRole::GRAPHIC: + eRole = GTK_ACCESSIBLE_ROLE_IMG; + break; + default: + SAL_WARN("vcl.gtk", + "unmapped GtkAccessibleRole: " << xContext->getAccessibleRole()); + break; + } + } + + return eRole; +} + +css::uno::Reference get_uno_accessible(GtkWidget* pWidget) +{ + GtkWidget* pTopLevel = widget_get_toplevel(pWidget); + if (!pTopLevel) + return nullptr; + + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + if (!pFrame) + return nullptr; + + vcl::Window* pFrameWindow = pFrame->GetWindow(); + if (!pFrameWindow) + return nullptr; + + vcl::Window* pWindow = pFrameWindow; + + // skip accessible objects already exposed by the frame objects + if (WindowType::BORDERWINDOW == pWindow->GetType()) + pWindow = pFrameWindow->GetAccessibleChildWindow(0); + + if (!pWindow) + return nullptr; + + return pWindow->GetAccessible(); +} + +GType lo_accessible_get_type(); + +#define LO_TYPE_ACCESSIBLE (lo_accessible_get_type()) +#define LO_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LO_TYPE_ACCESSIBLE, LoAccessible)) +// #define LO_IS_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LO_TYPE_ACCESSIBLE)) + +typedef struct _LoAccessible LoAccessible; + +struct _LoAccessible +{ + GObject parent_instance; + GdkDisplay* display; + GtkAccessible* parent; + GtkATContext* at_context; + css::uno::Reference uno_accessible; +}; + +typedef struct _LoAccessibleClass LoAccessibleClass; +struct _LoAccessibleClass +{ + GObjectClass parent_class; +}; + +enum +{ + CHILD_PROP_0, + LAST_CHILD_PROP, + + PROP_ACCESSIBLE_ROLE +}; + +static void lo_accessible_get_property(GObject* object, guint property_id, GValue* value, + GParamSpec* pspec) +{ + LoAccessible* accessible = LO_ACCESSIBLE(object); + + switch (property_id) + { + case PROP_ACCESSIBLE_ROLE: + { + GtkAccessibleRole eRole(map_accessible_role(accessible->uno_accessible)); + g_value_set_enum(value, eRole); + + // for now set GTK_ACCESSIBLE_PROPERTY_LABEL as a proof of concept + if (accessible->uno_accessible) + { + css::uno::Reference xContext( + accessible->uno_accessible->getAccessibleContext()); + css::uno::Reference xAccessibleText( + xContext, css::uno::UNO_QUERY); + if (xAccessibleText) + { + gtk_accessible_update_property( + GTK_ACCESSIBLE(accessible), GTK_ACCESSIBLE_PROPERTY_LABEL, + xAccessibleText->getText().toUtf8().getStr(), -1); + } + } + break; + } + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void lo_accessible_set_property(GObject* object, guint property_id, const GValue* /*value*/, + GParamSpec* pspec) +{ + // LoAccessible* accessible = LO_ACCESSIBLE(object); + + switch (property_id) + { + case PROP_ACCESSIBLE_ROLE: + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void lo_accessible_class_init(LoAccessibleClass* klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS(klass); + + // object_class->finalize = lo_accessible_finalize; + // object_class->dispose = lo_accessible_dispose; + object_class->get_property = lo_accessible_get_property; + object_class->set_property = lo_accessible_set_property; + // object_class->constructed = lo_accessible_constructed; + + // g_object_class_install_properties(object_class, LAST_CHILD_PROP, lo_accessible_props); + g_object_class_override_property(object_class, PROP_ACCESSIBLE_ROLE, "accessible-role"); +} + +static GtkAccessible* lo_accessible_get_accessible_parent(GtkAccessible* accessible) +{ + LoAccessible* lo_accessible = LO_ACCESSIBLE(accessible); + if (!lo_accessible->parent) + return nullptr; + return GTK_ACCESSIBLE(g_object_ref(lo_accessible->parent)); +} + +GtkATContext* lo_accessible_get_at_context(GtkAccessible* self) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + GtkAccessibleRole eRole = map_accessible_role(pAccessible->uno_accessible); + + if (!pAccessible->at_context + || gtk_at_context_get_accessible_role(pAccessible->at_context) != eRole) + { + pAccessible->at_context = gtk_at_context_create(eRole, self, pAccessible->display); + if (!pAccessible->at_context) + return nullptr; + } + + return g_object_ref(pAccessible->at_context); +} + +static GtkAccessible* lo_accessible_get_first_accessible_child(GtkAccessible* self); + +static GtkAccessible* lo_accessible_get_next_accessible_sibling(GtkAccessible* self); + +static gboolean lo_accessible_get_platform_state(GtkAccessible* self, + GtkAccessiblePlatformState state); + +static gboolean lo_accessible_get_bounds(GtkAccessible* accessible, int* x, int* y, int* width, + int* height); + +static void lo_accessible_accessible_init(GtkAccessibleInterface* iface) +{ + iface->get_accessible_parent = lo_accessible_get_accessible_parent; + iface->get_at_context = lo_accessible_get_at_context; + iface->get_bounds = lo_accessible_get_bounds; + iface->get_first_accessible_child = lo_accessible_get_first_accessible_child; + iface->get_next_accessible_sibling = lo_accessible_get_next_accessible_sibling; + iface->get_platform_state = lo_accessible_get_platform_state; +} + +G_DEFINE_TYPE_WITH_CODE(LoAccessible, lo_accessible, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, lo_accessible_accessible_init)) + +LoAccessible* +lo_accessible_new(GdkDisplay* pDisplay, GtkAccessible* pParent, + const css::uno::Reference& rAccessible) +{ + LoAccessible* ret = LO_ACCESSIBLE(g_object_new(LO_TYPE_ACCESSIBLE, nullptr)); + ret->display = pDisplay; + ret->parent = pParent; + ret->uno_accessible = rAccessible; + return ret; +} + +static gboolean lo_accessible_get_bounds(GtkAccessible* self, int* x, int* y, int* width, + int* height) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return false; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + css::uno::Reference xAccessibleComponent( + xContext, css::uno::UNO_QUERY); + if (!xAccessibleComponent) + return false; + + css::awt::Rectangle aBounds = xAccessibleComponent->getBounds(); + *x = aBounds.X; + *y = aBounds.Y; + *width = aBounds.Width; + *height = aBounds.Height; + return true; +} + +static GtkAccessible* lo_accessible_get_first_accessible_child(GtkAccessible* self) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return nullptr; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + if (!xContext->getAccessibleChildCount()) + return nullptr; + css::uno::Reference xFirstChild( + xContext->getAccessibleChild(0)); + if (!xFirstChild) + return nullptr; + + LoAccessible* child_accessible = lo_accessible_new(pAccessible->display, self, xFirstChild); + return GTK_ACCESSIBLE(g_object_ref(child_accessible)); +} + +static GtkAccessible* lo_accessible_get_next_accessible_sibling(GtkAccessible* self) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return nullptr; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + sal_Int64 nThisChildIndex = xContext->getAccessibleIndexInParent(); + assert(nThisChildIndex != -1); + sal_Int64 nNextChildIndex = nThisChildIndex + 1; + + css::uno::Reference xParent = xContext->getAccessibleParent(); + css::uno::Reference xParentContext( + xParent->getAccessibleContext()); + if (nNextChildIndex >= xParentContext->getAccessibleChildCount()) + return nullptr; + css::uno::Reference xNextChild( + xParentContext->getAccessibleChild(nNextChildIndex)); + if (!xNextChild) + return nullptr; + + LoAccessible* child_accessible + = lo_accessible_new(pAccessible->display, pAccessible->parent, xNextChild); + return GTK_ACCESSIBLE(g_object_ref(child_accessible)); +} + +static gboolean lo_accessible_get_platform_state(GtkAccessible* self, + GtkAccessiblePlatformState state) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return false; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + sal_Int64 nStateSet = xContext->getAccessibleStateSet(); + + switch (state) + { + case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSABLE: + return nStateSet & css::accessibility::AccessibleStateType::FOCUSABLE; + case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSED: + return nStateSet & css::accessibility::AccessibleStateType::FOCUSED; + case GTK_ACCESSIBLE_PLATFORM_STATE_ACTIVE: + return nStateSet & css::accessibility::AccessibleStateType::ACTIVE; + } + + return false; +} + +static void lo_accessible_init(LoAccessible* /*iface*/) {} + +GtkATContext* get_at_context(GtkAccessible* self) +{ + OOoFixed* pFixed = OOO_FIXED(self); + + css::uno::Reference xAccessible( + get_uno_accessible(GTK_WIDGET(pFixed))); + GtkAccessibleRole eRole = map_accessible_role(xAccessible); + + if (!pFixed->at_context || gtk_at_context_get_accessible_role(pFixed->at_context) != eRole) + { + // if (pFixed->at_context) + // g_clear_object(&pFixed->at_context); + + pFixed->at_context + = gtk_at_context_create(eRole, self, gtk_widget_get_display(GTK_WIDGET(pFixed))); + if (!pFixed->at_context) + return nullptr; + } + + return g_object_ref(pFixed->at_context); +} + +#if 0 +gboolean get_platform_state(GtkAccessible* self, GtkAccessiblePlatformState state) +{ + return false; +} +#endif + +gboolean get_bounds(GtkAccessible* accessible, int* x, int* y, int* width, int* height) +{ + OOoFixed* pFixed = OOO_FIXED(accessible); + css::uno::Reference xAccessible( + get_uno_accessible(GTK_WIDGET(pFixed))); + css::uno::Reference xContext( + xAccessible->getAccessibleContext()); + css::uno::Reference xAccessibleComponent( + xContext, css::uno::UNO_QUERY); + + css::awt::Rectangle aBounds = xAccessibleComponent->getBounds(); + *x = aBounds.X; + *y = aBounds.Y; + *width = aBounds.Width; + *height = aBounds.Height; + return true; +} + +GtkAccessible* get_first_accessible_child(GtkAccessible* accessible) +{ + OOoFixed* pFixed = OOO_FIXED(accessible); + css::uno::Reference xAccessible( + get_uno_accessible(GTK_WIDGET(pFixed))); + if (!xAccessible) + return nullptr; + css::uno::Reference xContext( + xAccessible->getAccessibleContext()); + if (!xContext->getAccessibleChildCount()) + return nullptr; + css::uno::Reference xFirstChild( + xContext->getAccessibleChild(0)); + LoAccessible* child_accessible + = lo_accessible_new(gtk_widget_get_display(GTK_WIDGET(pFixed)), accessible, xFirstChild); + return GTK_ACCESSIBLE(g_object_ref(child_accessible)); +} + +static void ooo_fixed_accessible_init(GtkAccessibleInterface* iface) +{ + GtkAccessibleInterface* parent_iface + = static_cast(g_type_interface_peek_parent(iface)); + iface->get_at_context = get_at_context; + iface->get_bounds = get_bounds; + iface->get_first_accessible_child = get_first_accessible_child; + iface->get_platform_state = parent_iface->get_platform_state; + // iface->get_platform_state = get_platform_state; +} + +G_DEFINE_TYPE_WITH_CODE(OOoFixed, ooo_fixed, GTK_TYPE_FIXED, + G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, ooo_fixed_accessible_init)) + +static void ooo_fixed_class_init(OOoFixedClass* /*klass*/) {} + +static void ooo_fixed_init(OOoFixed* /*area*/) {} + +GtkWidget* ooo_fixed_new() { return GTK_WIDGET(g_object_new(OOO_TYPE_FIXED, nullptr)); } + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ -- cgit