2 * Copyright (c) 2022 Samsung Electronics Co., Ltd.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
21 #include <dali/devel-api/common/stage.h>
22 #include <dali/integration-api/debug.h>
23 #include <dali/public-api/actors/layer.h>
25 #include <unordered_map>
28 #include <dali/devel-api/adaptor-framework/environment-variable.h>
29 #include <dali/devel-api/adaptor-framework/window-devel.h>
30 #include <dali/internal/accessibility/bridge/accessibility-common.h>
31 #include <dali/internal/accessibility/bridge/bridge-accessible.h>
32 #include <dali/internal/accessibility/bridge/bridge-action.h>
33 #include <dali/internal/accessibility/bridge/bridge-application.h>
34 #include <dali/internal/accessibility/bridge/bridge-collection.h>
35 #include <dali/internal/accessibility/bridge/bridge-component.h>
36 #include <dali/internal/accessibility/bridge/bridge-editable-text.h>
37 #include <dali/internal/accessibility/bridge/bridge-hyperlink.h>
38 #include <dali/internal/accessibility/bridge/bridge-hypertext.h>
39 #include <dali/internal/accessibility/bridge/bridge-object.h>
40 #include <dali/internal/accessibility/bridge/bridge-selection.h>
41 #include <dali/internal/accessibility/bridge/bridge-socket.h>
42 #include <dali/internal/accessibility/bridge/bridge-table.h>
43 #include <dali/internal/accessibility/bridge/bridge-table-cell.h>
44 #include <dali/internal/accessibility/bridge/bridge-text.h>
45 #include <dali/internal/accessibility/bridge/bridge-value.h>
46 #include <dali/internal/accessibility/bridge/dummy/dummy-atspi.h>
47 #include <dali/internal/adaptor/common/adaptor-impl.h>
48 #include <dali/internal/system/common/environment-variables.h>
50 using namespace Dali::Accessibility;
52 namespace // unnamed namespace
54 const int RETRY_INTERVAL = 1000;
56 } // unnamed namespace
59 * @brief The BridgeImpl class is to implement some Bridge functions.
61 class BridgeImpl : public virtual BridgeBase,
62 public BridgeAccessible,
64 public BridgeComponent,
65 public BridgeCollection,
69 public BridgeEditableText,
70 public BridgeSelection,
71 public BridgeApplication,
72 public BridgeHypertext,
73 public BridgeHyperlink,
76 public BridgeTableCell
78 DBus::DBusClient mAccessibilityStatusClient{};
79 DBus::DBusClient mRegistryClient{};
80 DBus::DBusClient mDirectReadingClient{};
81 bool mIsScreenReaderEnabled{false};
82 bool mIsEnabled{false};
83 std::unordered_map<int32_t, std::function<void(std::string)>> mDirectReadingCallbacks{};
84 Dali::Actor mHighlightedActor;
85 std::function<void(Dali::Actor)> mHighlightClearAction{nullptr};
86 Dali::CallbackBase* mIdleCallback{};
87 Dali::Timer mInitializeTimer;
88 Dali::Timer mReadIsEnabledTimer;
89 Dali::Timer mReadScreenReaderEnabledTimer;
90 Dali::Timer mForceUpTimer;
91 std::string mPreferredBusName;
94 BridgeImpl() = default;
97 * @copydoc Dali::Accessibility::Bridge::Emit()
99 Consumed Emit(KeyEventType type, unsigned int keyCode, const std::string& keyName, unsigned int timeStamp, bool isText) override
106 unsigned int keyType = 0;
110 case KeyEventType::KEY_PRESSED:
115 case KeyEventType::KEY_RELEASED:
126 auto methodObject = mRegistryClient.method<bool(std::tuple<uint32_t, int32_t, int32_t, int32_t, int32_t, std::string, bool>)>("NotifyListenersSync");
127 auto result = methodObject.call(std::tuple<uint32_t, int32_t, int32_t, int32_t, int32_t, std::string, bool>{keyType, 0, static_cast<int32_t>(keyCode), 0, static_cast<int32_t>(timeStamp), keyName, isText ? 1 : 0});
130 LOG() << result.getError().message;
133 return std::get<0>(result) ? Consumed::YES : Consumed::NO;
137 * @copydoc Dali::Accessibility::Bridge::Pause()
139 void Pause() override
146 mDirectReadingClient.method<DBus::ValueOrError<void>(bool)>("PauseResume").asyncCall([](DBus::ValueOrError<void> msg) {
149 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
156 * @copydoc Dali::Accessibility::Bridge::Resume()
158 void Resume() override
165 mDirectReadingClient.method<DBus::ValueOrError<void>(bool)>("PauseResume").asyncCall([](DBus::ValueOrError<void> msg) {
168 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
175 * @copydoc Dali::Accessibility::Bridge::StopReading()
177 void StopReading(bool alsoNonDiscardable) override
184 mDirectReadingClient.method<DBus::ValueOrError<void>(bool)>("StopReading").asyncCall([](DBus::ValueOrError<void> msg) {
187 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
194 * @copydoc Dali::Accessibility::Bridge::Say()
196 void Say(const std::string& text, bool discardable, std::function<void(std::string)> callback) override
203 mDirectReadingClient.method<DBus::ValueOrError<std::string, bool, int32_t>(std::string, bool)>("ReadCommand").asyncCall([=](DBus::ValueOrError<std::string, bool, int32_t> msg) {
206 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
210 mDirectReadingCallbacks.emplace(std::get<2>(msg), callback);
218 * @copydoc Dali::Accessibility::Bridge::ForceDown()
220 void ForceDown() override
224 if(mData->mCurrentlyHighlightedActor && mData->mHighlightActor)
226 mData->mCurrentlyHighlightedActor.Remove(mData->mHighlightActor);
228 mData->mCurrentlyHighlightedActor = {};
229 mData->mHighlightActor = {};
231 mDisabledSignal.Emit();
232 UnembedSocket(mApplication.GetAddress(), {AtspiDbusNameRegistry, "root"});
233 ReleaseBusName(mPreferredBusName);
236 mHighlightedActor = {};
237 mHighlightClearAction = {};
238 BridgeAccessible::ForceDown();
239 mRegistryClient = {};
240 mDirectReadingClient = {};
241 mDirectReadingCallbacks.clear();
242 mApplication.mChildren.clear();
250 mInitializeTimer.Stop();
251 mInitializeTimer.Reset();
254 if(mReadIsEnabledTimer)
256 mReadIsEnabledTimer.Stop();
257 mReadIsEnabledTimer.Reset();
260 if(mReadScreenReaderEnabledTimer)
262 mReadScreenReaderEnabledTimer.Stop();
263 mReadScreenReaderEnabledTimer.Reset();
268 mForceUpTimer.Stop();
269 mForceUpTimer.Reset();
274 * @copydoc Dali::Accessibility::Bridge::Terminate()
276 void Terminate() override
280 // The ~Window() after this point cannot emit DESTROY, because Bridge is not available. So emit DESTROY here.
281 for(auto windowAccessible : mApplication.mChildren)
283 BridgeObject::Emit(windowAccessible, WindowEvent::DESTROY);
285 mData->mCurrentlyHighlightedActor = {};
286 mData->mHighlightActor = {};
289 if((NULL != mIdleCallback) && Dali::Adaptor::IsAvailable())
291 Dali::Adaptor::Get().RemoveIdle(mIdleCallback);
293 mAccessibilityStatusClient = {};
298 bool ForceUpTimerCallback()
300 if(ForceUp() != ForceUpResult::FAILED)
308 * @copydoc Dali::Accessibility::Bridge::ForceUp()
310 ForceUpResult ForceUp() override
312 auto forceUpResult = BridgeAccessible::ForceUp();
313 if(forceUpResult == ForceUpResult::ALREADY_UP)
315 return forceUpResult;
317 else if(forceUpResult == ForceUpResult::FAILED)
321 mForceUpTimer = Dali::Timer::New(RETRY_INTERVAL);
322 mForceUpTimer.TickSignal().Connect(this, &BridgeImpl::ForceUpTimerCallback);
323 mForceUpTimer.Start();
325 return forceUpResult;
328 BridgeObject::RegisterInterfaces();
329 BridgeAccessible::RegisterInterfaces();
330 BridgeComponent::RegisterInterfaces();
331 BridgeCollection::RegisterInterfaces();
332 BridgeAction::RegisterInterfaces();
333 BridgeValue::RegisterInterfaces();
334 BridgeText::RegisterInterfaces();
335 BridgeEditableText::RegisterInterfaces();
336 BridgeSelection::RegisterInterfaces();
337 BridgeApplication::RegisterInterfaces();
338 BridgeHypertext::RegisterInterfaces();
339 BridgeHyperlink::RegisterInterfaces();
340 BridgeSocket::RegisterInterfaces();
341 BridgeTable::RegisterInterfaces();
342 BridgeTableCell::RegisterInterfaces();
344 RegisterOnBridge(&mApplication);
346 mRegistryClient = {AtspiDbusNameRegistry, AtspiDbusPathDec, Accessible::GetInterfaceName(AtspiInterface::DEVICE_EVENT_CONTROLLER), mConnectionPtr};
347 mDirectReadingClient = DBus::DBusClient{DirectReadingDBusName, DirectReadingDBusPath, DirectReadingDBusInterface, mConnectionPtr};
349 mDirectReadingClient.addSignal<void(int32_t, std::string)>("ReadingStateChanged", [=](int32_t id, std::string readingState) {
350 auto it = mDirectReadingCallbacks.find(id);
351 if(it != mDirectReadingCallbacks.end())
353 it->second(readingState);
354 if(readingState != "ReadingPaused" && readingState != "ReadingResumed" && readingState != "ReadingStarted")
356 mDirectReadingCallbacks.erase(it);
361 RequestBusName(mPreferredBusName);
363 auto parentAddress = EmbedSocket(mApplication.GetAddress(), {AtspiDbusNameRegistry, "root"});
364 mApplication.mParent.SetAddress(std::move(parentAddress));
365 mEnabledSignal.Emit();
367 return ForceUpResult::JUST_STARTED;
371 * @brief Sends a signal to dbus that the window is shown.
373 * @param[in] window The window to be shown
374 * @see Accessible::EmitShowing() and BridgeObject::EmitStateChanged()
376 void EmitShown(Dali::Window window)
378 auto windowAccessible = mApplication.GetWindowAccessible(window);
381 windowAccessible->EmitShowing(true);
386 * @brief Sends a signal to dbus that the window is hidden.
388 * @param[in] window The window to be hidden
389 * @see Accessible::EmitShowing() and BridgeObject::EmitStateChanged()
391 void EmitHidden(Dali::Window window)
393 auto windowAccessible = mApplication.GetWindowAccessible(window);
396 windowAccessible->EmitShowing(false);
401 * @brief Sends a signal to dbus that the window is activated.
403 * @param[in] window The window to be activated
404 * @see BridgeObject::Emit()
406 void EmitActivate(Dali::Window window)
408 auto windowAccessible = mApplication.GetWindowAccessible(window);
411 windowAccessible->Emit(WindowEvent::ACTIVATE, 0);
416 * @brief Sends a signal to dbus that the window is deactivated.
418 * @param[in] window The window to be deactivated
419 * @see BridgeObject::Emit()
421 void EmitDeactivate(Dali::Window window)
423 auto windowAccessible = mApplication.GetWindowAccessible(window);
426 windowAccessible->Emit(WindowEvent::DEACTIVATE, 0);
431 * @brief Sends a signal to dbus that the window is minimized.
433 * @param[in] window The window to be minimized
434 * @see BridgeObject::Emit()
436 void EmitMinimize(Dali::Window window)
438 auto windowAccessible = mApplication.GetWindowAccessible(window);
441 windowAccessible->Emit(WindowEvent::MINIMIZE, 0);
446 * @brief Sends a signal to dbus that the window is restored.
448 * @param[in] window The window to be restored
449 * @param[in] detail Restored window state
450 * @see BridgeObject::Emit()
452 void EmitRestore(Dali::Window window, Dali::Accessibility::WindowRestoreType detail)
454 auto windowAccessible = mApplication.GetWindowAccessible(window);
457 windowAccessible->Emit(WindowEvent::RESTORE, static_cast<unsigned int>(detail));
462 * @brief Sends a signal to dbus that the window is maximized.
464 * @param[in] window The window to be maximized
465 * @see BridgeObject::Emit()
467 void EmitMaximize(Dali::Window window)
469 auto windowAccessible = mApplication.GetWindowAccessible(window);
472 windowAccessible->Emit(WindowEvent::MAXIMIZE, 0);
477 * @copydoc Dali::Accessibility::Bridge::WindowShown()
479 void WindowShown(Dali::Window window) override
488 * @copydoc Dali::Accessibility::Bridge::WindowHidden()
490 void WindowHidden(Dali::Window window) override
499 * @copydoc Dali::Accessibility::Bridge::WindowFocused()
501 void WindowFocused(Dali::Window window) override
505 EmitActivate(window);
510 * @copydoc Dali::Accessibility::Bridge::WindowUnfocused()
512 void WindowUnfocused(Dali::Window window) override
516 EmitDeactivate(window);
521 * @copydoc Dali::Accessibility::Bridge::WindowMinimized()
523 void WindowMinimized(Dali::Window window) override
527 EmitMinimize(window);
532 * @copydoc Dali::Accessibility::Bridge::WindowRestored()
534 void WindowRestored(Dali::Window window, WindowRestoreType detail) override
538 EmitRestore(window, detail);
543 * @copydoc Dali::Accessibility::Bridge::WindowMaximized()
545 void WindowMaximized(Dali::Window window) override
549 EmitMaximize(window);
554 * @copydoc Dali::Accessibility::Bridge::SuppressScreenReader()
556 void SuppressScreenReader(bool suppress) override
558 if(mIsScreenReaderSuppressed == suppress)
562 mIsScreenReaderSuppressed = suppress;
563 ReadScreenReaderEnabledProperty();
568 if((!mIsScreenReaderSuppressed && mIsScreenReaderEnabled) || mIsEnabled)
578 bool ReadIsEnabledTimerCallback()
580 ReadIsEnabledProperty();
584 void ReadIsEnabledProperty()
586 mAccessibilityStatusClient.property<bool>("IsEnabled").asyncGet([this](DBus::ValueOrError<bool> msg) {
589 DALI_LOG_ERROR("Get IsEnabled property error: %s\n", msg.getError().message.c_str());
590 if(msg.getError().errorType == DBus::ErrorType::INVALID_REPLY)
592 if(!mReadIsEnabledTimer)
594 mReadIsEnabledTimer = Dali::Timer::New(RETRY_INTERVAL);
595 mReadIsEnabledTimer.TickSignal().Connect(this, &BridgeImpl::ReadIsEnabledTimerCallback);
597 mReadIsEnabledTimer.Start();
602 if(mReadIsEnabledTimer)
604 mReadIsEnabledTimer.Stop();
605 mReadIsEnabledTimer.Reset();
608 mIsEnabled = std::get<0>(msg);
613 void ListenIsEnabledProperty()
615 mAccessibilityStatusClient.addPropertyChangedEvent<bool>("IsEnabled", [this](bool res) {
621 bool ReadScreenReaderEnabledTimerCallback()
623 ReadScreenReaderEnabledProperty();
627 void ReadScreenReaderEnabledProperty()
629 // can be true because of SuppressScreenReader before init
630 if(!mAccessibilityStatusClient)
635 mAccessibilityStatusClient.property<bool>("ScreenReaderEnabled").asyncGet([this](DBus::ValueOrError<bool> msg) {
638 DALI_LOG_ERROR("Get ScreenReaderEnabled property error: %s\n", msg.getError().message.c_str());
639 if(msg.getError().errorType == DBus::ErrorType::INVALID_REPLY)
641 if(!mReadScreenReaderEnabledTimer)
643 mReadScreenReaderEnabledTimer = Dali::Timer::New(RETRY_INTERVAL);
644 mReadScreenReaderEnabledTimer.TickSignal().Connect(this, &BridgeImpl::ReadScreenReaderEnabledTimerCallback);
646 mReadScreenReaderEnabledTimer.Start();
651 if(mReadScreenReaderEnabledTimer)
653 mReadScreenReaderEnabledTimer.Stop();
654 mReadScreenReaderEnabledTimer.Reset();
657 mIsScreenReaderEnabled = std::get<0>(msg);
662 void EmitScreenReaderEnabledSignal()
664 if(mIsScreenReaderEnabled)
666 mScreenReaderEnabledSignal.Emit();
670 mScreenReaderDisabledSignal.Emit();
674 void ListenScreenReaderEnabledProperty()
676 mAccessibilityStatusClient.addPropertyChangedEvent<bool>("ScreenReaderEnabled", [this](bool res) {
677 mIsScreenReaderEnabled = res;
678 EmitScreenReaderEnabledSignal();
683 void ReadAndListenProperties()
685 ReadIsEnabledProperty();
686 ListenIsEnabledProperty();
688 ReadScreenReaderEnabledProperty();
689 ListenScreenReaderEnabledProperty();
692 bool InitializeAccessibilityStatusClient()
694 mAccessibilityStatusClient = DBus::DBusClient{A11yDbusName, A11yDbusPath, A11yDbusStatusInterface, DBus::ConnectionType::SESSION};
696 if(!mAccessibilityStatusClient)
698 DALI_LOG_ERROR("Accessibility Status DbusClient is not ready\n");
705 bool InitializeTimerCallback()
707 if(InitializeAccessibilityStatusClient())
709 ReadAndListenProperties();
717 if(InitializeAccessibilityStatusClient())
719 ReadAndListenProperties();
720 mIdleCallback = NULL;
724 if(!mInitializeTimer)
726 mInitializeTimer = Dali::Timer::New(RETRY_INTERVAL);
727 mInitializeTimer.TickSignal().Connect(this, &BridgeImpl::InitializeTimerCallback);
729 mInitializeTimer.Start();
731 mIdleCallback = NULL;
736 * @copydoc Dali::Accessibility::Bridge::Initialize()
738 void Initialize() override
740 if(InitializeAccessibilityStatusClient())
742 ReadAndListenProperties();
746 // Initialize failed. Try it again on Idle
747 if(Dali::Adaptor::IsAvailable())
749 Dali::Adaptor& adaptor = Dali::Adaptor::Get();
750 if(NULL == mIdleCallback)
752 mIdleCallback = MakeCallback(this, &BridgeImpl::OnIdleSignal);
753 adaptor.AddIdle(mIdleCallback, true);
759 * @copydoc Dali::Accessibility::Bridge::GetScreenReaderEnabled()
761 bool GetScreenReaderEnabled() override
763 return mIsScreenReaderEnabled;
767 * @copydoc Dali::Accessibility::Bridge::IsEnabled()
769 bool IsEnabled() override
774 Address EmbedSocket(const Address& plug, const Address& socket) override
776 auto client = CreateSocketClient(socket);
777 auto reply = client.method<Address(Address)>("Embed").call(plug);
781 DALI_LOG_ERROR("Failed to embed socket %s: %s", socket.ToString().c_str(), reply.getError().message.c_str());
785 return std::get<0>(reply.getValues());
788 void EmbedAtkSocket(const Address& plug, const Address& socket) override
790 auto client = CreateSocketClient(socket);
792 client.method<void(std::string)>("Embedded").asyncCall([](DBus::ValueOrError<void>) {}, ATSPI_PREFIX_PATH + plug.GetPath());
795 void UnembedSocket(const Address& plug, const Address& socket) override
797 auto client = CreateSocketClient(socket);
799 client.method<void(Address)>("Unembed").asyncCall([](DBus::ValueOrError<void>) {}, plug);
802 void SetSocketOffset(ProxyAccessible* socket, std::int32_t x, std::int32_t y) override
804 AddCoalescableMessage(CoalescableMessages::SET_OFFSET, socket, 1.0f, [=]() {
805 auto client = CreateSocketClient(socket->GetAddress());
807 client.method<void(std::int32_t, std::int32_t)>("SetOffset").asyncCall([](DBus::ValueOrError<void>) {}, x, y);
811 void SetExtentsOffset(std::int32_t x, std::int32_t y) override
815 mData->mExtentsOffset = {x, y};
819 void SetPreferredBusName(std::string_view preferredBusName) override
821 if(preferredBusName == mPreferredBusName)
826 std::string oldPreferredBusName = std::move(mPreferredBusName);
827 mPreferredBusName = std::string{preferredBusName};
831 ReleaseBusName(oldPreferredBusName);
832 RequestBusName(mPreferredBusName);
834 // else: request/release will be handled by ForceUp/ForceDown, respectively
838 DBus::DBusClient CreateSocketClient(const Address& socket)
840 return {socket.GetBus(), ATSPI_PREFIX_PATH + socket.GetPath(), Accessible::GetInterfaceName(AtspiInterface::SOCKET), mConnectionPtr};
843 void RequestBusName(const std::string& busName)
850 DBus::requestBusName(mConnectionPtr, busName);
853 void ReleaseBusName(const std::string& busName)
860 DBus::releaseBusName(mConnectionPtr, busName);
864 namespace // unnamed namespace
866 bool INITIALIZED_BRIDGE = false;
869 * @brief Creates BridgeImpl instance.
871 * @return The BridgeImpl instance
872 * @note This method is to check environment variable first. If ATSPI is disable using env, it returns dummy bridge instance.
874 std::shared_ptr<Bridge> CreateBridge()
876 INITIALIZED_BRIDGE = true;
880 /* check environment variable first */
881 const char* envAtspiDisabled = Dali::EnvironmentVariable::GetEnvironmentVariable(DALI_ENV_DISABLE_ATSPI);
882 if(envAtspiDisabled && std::atoi(envAtspiDisabled) != 0)
884 return Dali::Accessibility::DummyBridge::GetInstance();
887 return std::make_shared<BridgeImpl>();
889 catch(const std::exception&)
891 DALI_LOG_ERROR("Failed to initialize AT-SPI bridge");
892 return Dali::Accessibility::DummyBridge::GetInstance();
896 } // unnamed namespace
898 // Dali::Accessibility::Bridge class implementation
900 std::shared_ptr<Bridge> Bridge::GetCurrentBridge()
902 static std::shared_ptr<Bridge> bridge;
908 else if(mAutoInitState == AutoInitState::ENABLED)
910 bridge = CreateBridge();
912 /* check environment variable for suppressing screen-reader */
913 const char* envSuppressScreenReader = Dali::EnvironmentVariable::GetEnvironmentVariable(DALI_ENV_SUPPRESS_SCREEN_READER);
914 if(envSuppressScreenReader && std::atoi(envSuppressScreenReader) != 0)
916 bridge->SuppressScreenReader(true);
922 return Dali::Accessibility::DummyBridge::GetInstance();
925 void Bridge::DisableAutoInit()
927 if(INITIALIZED_BRIDGE)
929 DALI_LOG_ERROR("Bridge::DisableAutoInit() called after bridge auto-initialization");
932 mAutoInitState = AutoInitState::DISABLED;
935 void Bridge::EnableAutoInit()
937 mAutoInitState = AutoInitState::ENABLED;
939 if(INITIALIZED_BRIDGE)
944 auto rootLayer = Dali::Stage::GetCurrent().GetRootLayer(); // A root layer of the default window.
945 auto window = Dali::DevelWindow::Get(rootLayer);
946 auto applicationName = Dali::Internal::Adaptor::Adaptor::GetApplicationPackageName();
948 auto accessible = Accessibility::Accessible::Get(rootLayer);
950 auto bridge = Bridge::GetCurrentBridge();
951 bridge->AddTopLevelWindow(accessible);
952 bridge->SetApplicationName(applicationName);
953 bridge->Initialize();
955 if(window && window.IsVisible())
957 bridge->WindowShown(window);
961 std::string Bridge::MakeBusNameForWidget(std::string_view widgetInstanceId)
963 // The bus name should consist of dot-separated alphanumeric elements, e.g. "com.example.BusName123".
964 // Allowed characters in each element: "[A-Z][a-z][0-9]_", but no element may start with a digit.
966 static const char prefix[] = "com.samsung.dali.widget_";
967 static const char underscore = '_';
969 std::stringstream tmp;
973 for(char ch : widgetInstanceId)
975 tmp << (std::isalnum(ch) ? ch : underscore);