2 * Copyright (c) 2024 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-cell.h>
43 #include <dali/internal/accessibility/bridge/bridge-table.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:
130 * @copydoc Dali::Accessibility::Bridge::Pause()
132 void Pause() override
139 mDirectReadingClient.method<DBus::ValueOrError<void>(bool)>("PauseResume").asyncCall([](DBus::ValueOrError<void> msg) {
142 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
149 * @copydoc Dali::Accessibility::Bridge::Resume()
151 void Resume() override
158 mDirectReadingClient.method<DBus::ValueOrError<void>(bool)>("PauseResume").asyncCall([](DBus::ValueOrError<void> msg) {
161 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
168 * @copydoc Dali::Accessibility::Bridge::StopReading()
170 void StopReading(bool alsoNonDiscardable) override
177 mDirectReadingClient.method<DBus::ValueOrError<void>(bool)>("StopReading").asyncCall([](DBus::ValueOrError<void> msg) {
180 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
187 * @copydoc Dali::Accessibility::Bridge::Say()
189 void Say(const std::string& text, bool discardable, std::function<void(std::string)> callback) override
196 mDirectReadingClient.method<DBus::ValueOrError<std::string, bool, int32_t>(std::string, bool)>("ReadCommand").asyncCall([=](DBus::ValueOrError<std::string, bool, int32_t> msg) {
199 LOG() << "Direct reading command failed (" << msg.getError().message << ")\n";
203 mDirectReadingCallbacks.emplace(std::get<2>(msg), callback);
211 * @copydoc Dali::Accessibility::Bridge::ForceDown()
213 void ForceDown() override
217 if(mData->mCurrentlyHighlightedActor && mData->mHighlightActor)
219 mData->mCurrentlyHighlightedActor.Remove(mData->mHighlightActor);
221 mData->mCurrentlyHighlightedActor = {};
222 mData->mHighlightActor = {};
224 mDisabledSignal.Emit();
225 UnembedSocket(mApplication.GetAddress(), {AtspiDbusNameRegistry, "root"});
226 ReleaseBusName(mPreferredBusName);
229 mHighlightedActor = {};
230 mHighlightClearAction = {};
231 BridgeAccessible::ForceDown();
232 mRegistryClient = {};
233 mDirectReadingClient = {};
234 mDirectReadingCallbacks.clear();
235 mApplication.mChildren.clear();
243 mInitializeTimer.Stop();
244 mInitializeTimer.Reset();
247 if(mReadIsEnabledTimer)
249 mReadIsEnabledTimer.Stop();
250 mReadIsEnabledTimer.Reset();
253 if(mReadScreenReaderEnabledTimer)
255 mReadScreenReaderEnabledTimer.Stop();
256 mReadScreenReaderEnabledTimer.Reset();
261 mForceUpTimer.Stop();
262 mForceUpTimer.Reset();
267 * @copydoc Dali::Accessibility::Bridge::Terminate()
269 void Terminate() override
273 // The ~Window() after this point cannot emit DESTROY, because Bridge is not available. So emit DESTROY here.
274 for(auto windowAccessible : mApplication.mChildren)
276 BridgeObject::Emit(windowAccessible, WindowEvent::DESTROY);
278 mData->mCurrentlyHighlightedActor = {};
279 mData->mHighlightActor = {};
282 if((NULL != mIdleCallback) && Dali::Adaptor::IsAvailable())
284 Dali::Adaptor::Get().RemoveIdle(mIdleCallback);
286 mAccessibilityStatusClient = {};
291 bool ForceUpTimerCallback()
293 if(ForceUp() != ForceUpResult::FAILED)
301 * @copydoc Dali::Accessibility::Bridge::ForceUp()
303 ForceUpResult ForceUp() override
305 auto forceUpResult = BridgeAccessible::ForceUp();
306 if(forceUpResult == ForceUpResult::ALREADY_UP)
308 return forceUpResult;
310 else if(forceUpResult == ForceUpResult::FAILED)
314 mForceUpTimer = Dali::Timer::New(RETRY_INTERVAL);
315 mForceUpTimer.TickSignal().Connect(this, &BridgeImpl::ForceUpTimerCallback);
316 mForceUpTimer.Start();
318 return forceUpResult;
321 BridgeObject::RegisterInterfaces();
322 BridgeAccessible::RegisterInterfaces();
323 BridgeComponent::RegisterInterfaces();
324 BridgeCollection::RegisterInterfaces();
325 BridgeAction::RegisterInterfaces();
326 BridgeValue::RegisterInterfaces();
327 BridgeText::RegisterInterfaces();
328 BridgeEditableText::RegisterInterfaces();
329 BridgeSelection::RegisterInterfaces();
330 BridgeApplication::RegisterInterfaces();
331 BridgeHypertext::RegisterInterfaces();
332 BridgeHyperlink::RegisterInterfaces();
333 BridgeSocket::RegisterInterfaces();
334 BridgeTable::RegisterInterfaces();
335 BridgeTableCell::RegisterInterfaces();
337 RegisterOnBridge(&mApplication);
339 mRegistryClient = {AtspiDbusNameRegistry, AtspiDbusPathDec, Accessible::GetInterfaceName(AtspiInterface::DEVICE_EVENT_CONTROLLER), mConnectionPtr};
340 mDirectReadingClient = DBus::DBusClient{DirectReadingDBusName, DirectReadingDBusPath, DirectReadingDBusInterface, mConnectionPtr};
342 mDirectReadingClient.addSignal<void(int32_t, std::string)>("ReadingStateChanged", [=](int32_t id, std::string readingState) {
343 auto it = mDirectReadingCallbacks.find(id);
344 if(it != mDirectReadingCallbacks.end())
346 it->second(readingState);
347 if(readingState != "ReadingPaused" && readingState != "ReadingResumed" && readingState != "ReadingStarted")
349 mDirectReadingCallbacks.erase(it);
354 RequestBusName(mPreferredBusName);
356 auto parentAddress = EmbedSocket(mApplication.GetAddress(), {AtspiDbusNameRegistry, "root"});
357 mApplication.mParent.SetAddress(std::move(parentAddress));
358 mEnabledSignal.Emit();
360 return ForceUpResult::JUST_STARTED;
364 * @brief Sends a signal to dbus that the window is created.
366 * @param[in] window The window to be created
367 * @see BridgeObject::Emit()
369 void EmitCreated(Dali::Window window)
371 auto windowAccessible = mApplication.GetWindowAccessible(window);
374 windowAccessible->Emit(WindowEvent::CREATE, 0);
379 * @brief Sends a signal to dbus that the window is shown.
381 * @param[in] window The window to be shown
382 * @see Accessible::EmitShowing() and BridgeObject::EmitStateChanged()
384 void EmitShown(Dali::Window window)
386 auto windowAccessible = mApplication.GetWindowAccessible(window);
389 windowAccessible->EmitShowing(true);
394 * @brief Sends a signal to dbus that the window is hidden.
396 * @param[in] window The window to be hidden
397 * @see Accessible::EmitShowing() and BridgeObject::EmitStateChanged()
399 void EmitHidden(Dali::Window window)
401 auto windowAccessible = mApplication.GetWindowAccessible(window);
404 windowAccessible->EmitShowing(false);
409 * @brief Sends a signal to dbus that the window is activated.
411 * @param[in] window The window to be activated
412 * @see BridgeObject::Emit()
414 void EmitActivate(Dali::Window window)
416 auto windowAccessible = mApplication.GetWindowAccessible(window);
419 windowAccessible->Emit(WindowEvent::ACTIVATE, 0);
424 * @brief Sends a signal to dbus that the window is deactivated.
426 * @param[in] window The window to be deactivated
427 * @see BridgeObject::Emit()
429 void EmitDeactivate(Dali::Window window)
431 auto windowAccessible = mApplication.GetWindowAccessible(window);
434 windowAccessible->Emit(WindowEvent::DEACTIVATE, 0);
439 * @brief Sends a signal to dbus that the window is minimized.
441 * @param[in] window The window to be minimized
442 * @see BridgeObject::Emit()
444 void EmitMinimize(Dali::Window window)
446 auto windowAccessible = mApplication.GetWindowAccessible(window);
449 windowAccessible->Emit(WindowEvent::MINIMIZE, 0);
454 * @brief Sends a signal to dbus that the window is restored.
456 * @param[in] window The window to be restored
457 * @param[in] detail Restored window state
458 * @see BridgeObject::Emit()
460 void EmitRestore(Dali::Window window, Dali::Accessibility::WindowRestoreType detail)
462 auto windowAccessible = mApplication.GetWindowAccessible(window);
465 windowAccessible->Emit(WindowEvent::RESTORE, static_cast<unsigned int>(detail));
470 * @brief Sends a signal to dbus that the window is maximized.
472 * @param[in] window The window to be maximized
473 * @see BridgeObject::Emit()
475 void EmitMaximize(Dali::Window window)
477 auto windowAccessible = mApplication.GetWindowAccessible(window);
480 windowAccessible->Emit(WindowEvent::MAXIMIZE, 0);
485 * @copydoc Dali::Accessibility::Bridge::WindowCreated()
487 void WindowCreated(Dali::Window window) override
496 * @copydoc Dali::Accessibility::Bridge::WindowShown()
498 void WindowShown(Dali::Window window) override
507 * @copydoc Dali::Accessibility::Bridge::WindowHidden()
509 void WindowHidden(Dali::Window window) override
518 * @copydoc Dali::Accessibility::Bridge::WindowFocused()
520 void WindowFocused(Dali::Window window) override
524 EmitActivate(window);
529 * @copydoc Dali::Accessibility::Bridge::WindowUnfocused()
531 void WindowUnfocused(Dali::Window window) override
535 EmitDeactivate(window);
540 * @copydoc Dali::Accessibility::Bridge::WindowMinimized()
542 void WindowMinimized(Dali::Window window) override
546 EmitMinimize(window);
551 * @copydoc Dali::Accessibility::Bridge::WindowRestored()
553 void WindowRestored(Dali::Window window, WindowRestoreType detail) override
557 EmitRestore(window, detail);
562 * @copydoc Dali::Accessibility::Bridge::WindowMaximized()
564 void WindowMaximized(Dali::Window window) override
568 EmitMaximize(window);
573 * @copydoc Dali::Accessibility::Bridge::SuppressScreenReader()
575 void SuppressScreenReader(bool suppress) override
577 if(mIsScreenReaderSuppressed == suppress)
581 mIsScreenReaderSuppressed = suppress;
582 ReadScreenReaderEnabledProperty();
587 if((!mIsScreenReaderSuppressed && mIsScreenReaderEnabled) || mIsEnabled)
597 bool ReadIsEnabledTimerCallback()
599 ReadIsEnabledProperty();
603 void ReadIsEnabledProperty()
605 mAccessibilityStatusClient.property<bool>("IsEnabled").asyncGet([this](DBus::ValueOrError<bool> msg) {
608 DALI_LOG_ERROR("Get IsEnabled property error: %s\n", msg.getError().message.c_str());
609 if(msg.getError().errorType == DBus::ErrorType::INVALID_REPLY)
611 if(!mReadIsEnabledTimer)
613 mReadIsEnabledTimer = Dali::Timer::New(RETRY_INTERVAL);
614 mReadIsEnabledTimer.TickSignal().Connect(this, &BridgeImpl::ReadIsEnabledTimerCallback);
616 mReadIsEnabledTimer.Start();
621 if(mReadIsEnabledTimer)
623 mReadIsEnabledTimer.Stop();
624 mReadIsEnabledTimer.Reset();
627 mIsEnabled = std::get<0>(msg);
632 void ListenIsEnabledProperty()
634 mAccessibilityStatusClient.addPropertyChangedEvent<bool>("IsEnabled", [this](bool res) {
640 bool ReadScreenReaderEnabledTimerCallback()
642 ReadScreenReaderEnabledProperty();
646 void ReadScreenReaderEnabledProperty()
648 // can be true because of SuppressScreenReader before init
649 if(!mAccessibilityStatusClient)
654 mAccessibilityStatusClient.property<bool>("ScreenReaderEnabled").asyncGet([this](DBus::ValueOrError<bool> msg) {
657 DALI_LOG_ERROR("Get ScreenReaderEnabled property error: %s\n", msg.getError().message.c_str());
658 if(msg.getError().errorType == DBus::ErrorType::INVALID_REPLY)
660 if(!mReadScreenReaderEnabledTimer)
662 mReadScreenReaderEnabledTimer = Dali::Timer::New(RETRY_INTERVAL);
663 mReadScreenReaderEnabledTimer.TickSignal().Connect(this, &BridgeImpl::ReadScreenReaderEnabledTimerCallback);
665 mReadScreenReaderEnabledTimer.Start();
670 if(mReadScreenReaderEnabledTimer)
672 mReadScreenReaderEnabledTimer.Stop();
673 mReadScreenReaderEnabledTimer.Reset();
676 mIsScreenReaderEnabled = std::get<0>(msg);
681 void EmitScreenReaderEnabledSignal()
683 if(mIsScreenReaderEnabled)
685 mScreenReaderEnabledSignal.Emit();
689 mScreenReaderDisabledSignal.Emit();
693 void ListenScreenReaderEnabledProperty()
695 mAccessibilityStatusClient.addPropertyChangedEvent<bool>("ScreenReaderEnabled", [this](bool res) {
696 mIsScreenReaderEnabled = res;
697 EmitScreenReaderEnabledSignal();
702 void ReadAndListenProperties()
704 ReadIsEnabledProperty();
705 ListenIsEnabledProperty();
707 ReadScreenReaderEnabledProperty();
708 ListenScreenReaderEnabledProperty();
711 bool InitializeAccessibilityStatusClient()
713 mAccessibilityStatusClient = DBus::DBusClient{A11yDbusName, A11yDbusPath, A11yDbusStatusInterface, DBus::ConnectionType::SESSION};
715 if(!mAccessibilityStatusClient)
717 DALI_LOG_ERROR("Accessibility Status DbusClient is not ready\n");
724 bool InitializeTimerCallback()
726 if(InitializeAccessibilityStatusClient())
728 ReadAndListenProperties();
736 if(InitializeAccessibilityStatusClient())
738 ReadAndListenProperties();
739 mIdleCallback = NULL;
743 if(!mInitializeTimer)
745 mInitializeTimer = Dali::Timer::New(RETRY_INTERVAL);
746 mInitializeTimer.TickSignal().Connect(this, &BridgeImpl::InitializeTimerCallback);
748 mInitializeTimer.Start();
750 mIdleCallback = NULL;
755 * @copydoc Dali::Accessibility::Bridge::Initialize()
757 void Initialize() override
759 if(InitializeAccessibilityStatusClient())
761 ReadAndListenProperties();
765 // Initialize failed. Try it again on Idle
766 if(Dali::Adaptor::IsAvailable())
768 Dali::Adaptor& adaptor = Dali::Adaptor::Get();
769 if(NULL == mIdleCallback)
771 mIdleCallback = MakeCallback(this, &BridgeImpl::OnIdleSignal);
772 if(DALI_UNLIKELY(!adaptor.AddIdle(mIdleCallback, true)))
774 DALI_LOG_ERROR("Fail to add idle callback for bridge initialize. Call it synchronously.\n");
782 * @copydoc Dali::Accessibility::Bridge::GetScreenReaderEnabled()
784 bool GetScreenReaderEnabled() override
786 return mIsScreenReaderEnabled;
790 * @copydoc Dali::Accessibility::Bridge::IsEnabled()
792 bool IsEnabled() override
797 Address EmbedSocket(const Address& plug, const Address& socket) override
799 auto client = CreateSocketClient(socket);
800 auto reply = client.method<Address(Address)>("Embed").call(plug);
804 DALI_LOG_ERROR("Failed to embed socket %s: %s", socket.ToString().c_str(), reply.getError().message.c_str());
808 return std::get<0>(reply.getValues());
811 void EmbedAtkSocket(const Address& plug, const Address& socket) override
813 auto client = CreateSocketClient(socket);
815 client.method<void(std::string)>("Embedded").asyncCall([](DBus::ValueOrError<void>) {}, ATSPI_PREFIX_PATH + plug.GetPath());
818 void UnembedSocket(const Address& plug, const Address& socket) override
820 auto client = CreateSocketClient(socket);
822 client.method<void(Address)>("Unembed").asyncCall([](DBus::ValueOrError<void>) {}, plug);
825 void SetSocketOffset(ProxyAccessible* socket, std::int32_t x, std::int32_t y) override
827 AddCoalescableMessage(CoalescableMessages::SET_OFFSET, socket, 1.0f, [=]() {
828 auto client = CreateSocketClient(socket->GetAddress());
830 client.method<void(std::int32_t, std::int32_t)>("SetOffset").asyncCall([](DBus::ValueOrError<void>) {}, x, y);
834 void SetExtentsOffset(std::int32_t x, std::int32_t y) override
838 mData->mExtentsOffset = {x, y};
842 void SetPreferredBusName(std::string_view preferredBusName) override
844 if(preferredBusName == mPreferredBusName)
849 std::string oldPreferredBusName = std::move(mPreferredBusName);
850 mPreferredBusName = std::string{preferredBusName};
854 ReleaseBusName(oldPreferredBusName);
855 RequestBusName(mPreferredBusName);
857 // else: request/release will be handled by ForceUp/ForceDown, respectively
861 DBus::DBusClient CreateSocketClient(const Address& socket)
863 return {socket.GetBus(), ATSPI_PREFIX_PATH + socket.GetPath(), Accessible::GetInterfaceName(AtspiInterface::SOCKET), mConnectionPtr};
866 void RequestBusName(const std::string& busName)
873 DBus::requestBusName(mConnectionPtr, busName);
876 void ReleaseBusName(const std::string& busName)
883 DBus::releaseBusName(mConnectionPtr, busName);
887 namespace // unnamed namespace
889 bool INITIALIZED_BRIDGE = false;
892 * @brief Creates BridgeImpl instance.
894 * @return The BridgeImpl instance
895 * @note This method is to check environment variable first. If ATSPI is disable using env, it returns dummy bridge instance.
897 std::shared_ptr<Bridge> CreateBridge()
899 INITIALIZED_BRIDGE = true;
903 /* check environment variable first */
904 const char* envAtspiDisabled = Dali::EnvironmentVariable::GetEnvironmentVariable(DALI_ENV_DISABLE_ATSPI);
905 if(envAtspiDisabled && std::atoi(envAtspiDisabled) != 0)
907 return Dali::Accessibility::DummyBridge::GetInstance();
910 return std::make_shared<BridgeImpl>();
912 catch(const std::exception&)
914 DALI_LOG_ERROR("Failed to initialize AT-SPI bridge");
915 return Dali::Accessibility::DummyBridge::GetInstance();
919 } // unnamed namespace
921 // Dali::Accessibility::Bridge class implementation
923 std::shared_ptr<Bridge> Bridge::GetCurrentBridge()
925 static std::shared_ptr<Bridge> bridge;
931 else if(mAutoInitState == AutoInitState::ENABLED)
933 bridge = CreateBridge();
935 /* check environment variable for suppressing screen-reader */
936 const char* envSuppressScreenReader = Dali::EnvironmentVariable::GetEnvironmentVariable(DALI_ENV_SUPPRESS_SCREEN_READER);
937 if(envSuppressScreenReader && std::atoi(envSuppressScreenReader) != 0)
939 bridge->SuppressScreenReader(true);
945 return Dali::Accessibility::DummyBridge::GetInstance();
948 void Bridge::DisableAutoInit()
950 if(INITIALIZED_BRIDGE)
952 DALI_LOG_ERROR("Bridge::DisableAutoInit() called after bridge auto-initialization");
955 mAutoInitState = AutoInitState::DISABLED;
958 void Bridge::EnableAutoInit()
960 mAutoInitState = AutoInitState::ENABLED;
962 if(INITIALIZED_BRIDGE)
967 auto rootLayer = Dali::Stage::GetCurrent().GetRootLayer(); // A root layer of the default window.
968 auto window = Dali::DevelWindow::Get(rootLayer);
969 auto applicationName = Dali::Internal::Adaptor::Adaptor::GetApplicationPackageName();
971 auto accessible = Accessibility::Accessible::Get(rootLayer);
973 auto bridge = Bridge::GetCurrentBridge();
974 bridge->AddTopLevelWindow(accessible);
975 bridge->SetApplicationName(applicationName);
976 bridge->Initialize();
978 if(window && window.IsVisible())
980 bridge->WindowShown(window);
984 std::string Bridge::MakeBusNameForWidget(std::string_view widgetInstanceId)
986 // The bus name should consist of dot-separated alphanumeric elements, e.g. "com.example.BusName123".
987 // Allowed characters in each element: "[A-Z][a-z][0-9]_", but no element may start with a digit.
989 static const char prefix[] = "com.samsung.dali.widget_";
990 static const char underscore = '_';
992 std::stringstream tmp;
996 for(char ch : widgetInstanceId)
998 tmp << (std::isalnum(ch) ? ch : underscore);