From 9b27b6e5098aee7a050d4c3f3f14050c509f74ec Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Wed, 14 Dec 2022 19:41:07 +0200 Subject: SL-13610 [MAC] WIP List HID available devices in joystick selection Doesn't filter the list yet, just shows full list of usb devices Selecting visible devices doesn't work yet --- indra/llui/lltransutil.cpp | 4 +- indra/llwindow/llwindow.h | 8 +- indra/llwindow/llwindowmacosx-objc.mm | 1 + indra/llwindow/llwindowmacosx.cpp | 315 ++++++++++++++++++++- indra/llwindow/llwindowmacosx.h | 5 + indra/llwindow/llwindowwin32.cpp | 5 +- indra/llwindow/llwindowwin32.h | 5 +- indra/llxml/llxmlnode.cpp | 2 +- indra/newview/llfilepicker.cpp | 6 +- indra/newview/llfloaterjoystick.cpp | 14 +- indra/newview/llfloaterjoystick.h | 1 + indra/newview/llviewerjoystick.cpp | 25 +- indra/newview/llviewerjoystick.h | 6 +- .../skins/default/xui/en/floater_about_land.xml | 12 + 14 files changed, 384 insertions(+), 25 deletions(-) diff --git a/indra/llui/lltransutil.cpp b/indra/llui/lltransutil.cpp index 5da722a72b..ecd065a4ef 100644 --- a/indra/llui/lltransutil.cpp +++ b/indra/llui/lltransutil.cpp @@ -44,8 +44,8 @@ bool LLTransUtil::parseStrings(const std::string& xml_filename, const std::setdumpCurrentDirectories(LLError::LEVEL_WARN); - LL_ERRS() << "Couldn't load string table " << xml_filename << ". Please reinstall viewer from https://secondlife.com/support/downloads/ and contact https://support.secondlife.com if issue persists after reinstall." << LL_ENDL; + //gDirUtilp->dumpCurrentDirectories(LLError::LEVEL_WARN); + LL_ERRS() << "Couldn't load string table " << xml_filename << " " << errno << ". Please reinstall viewer from https://secondlife.com/support/downloads/ and contact https://support.secondlife.com if issue persists after reinstall." << LL_ENDL; return false; } diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h index ac4848579f..2d27092e0a 100644 --- a/indra/llwindow/llwindow.h +++ b/indra/llwindow/llwindow.h @@ -196,7 +196,13 @@ public: // windows only DirectInput8 for joysticks virtual void* getDirectInput8() { return NULL; }; - virtual bool getInputDevices(U32 device_type_filter, void * devices_callback, void* userdata) { return false; }; + virtual bool getInputDevices(U32 device_type_filter, + std::function osx_callback, + void* win_callback, + void* userdata) + { + return false; + }; virtual S32 getRefreshRate() { return mRefreshRate; } protected: diff --git a/indra/llwindow/llwindowmacosx-objc.mm b/indra/llwindow/llwindowmacosx-objc.mm index 690fe058db..2e75d309ea 100644 --- a/indra/llwindow/llwindowmacosx-objc.mm +++ b/indra/llwindow/llwindowmacosx-objc.mm @@ -27,6 +27,7 @@ #include #include +#include #include "llopenglview-objc.h" #include "llwindowmacosx-objc.h" #include "llappdelegate-objc.h" diff --git a/indra/llwindow/llwindowmacosx.cpp b/indra/llwindow/llwindowmacosx.cpp index 2c841d4703..bd13138a7d 100644 --- a/indra/llwindow/llwindowmacosx.cpp +++ b/indra/llwindow/llwindowmacosx.cpp @@ -43,6 +43,13 @@ #include #include +#include +#include +#include +#include +#include +#include + extern BOOL gDebugWindowProc; BOOL gHiDPISupport = TRUE; @@ -212,13 +219,16 @@ bool callKeyUp(NSKeyEventRef event, unsigned short key, unsigned int mask) bool callKeyDown(NSKeyEventRef event, unsigned short key, unsigned int mask, wchar_t character) { - if((key == gKeyboard->inverseTranslateKey('Z')) && (character == 'y')) - { - key = gKeyboard->inverseTranslateKey('Y'); - } - else if ((key == gKeyboard->inverseTranslateKey('Y')) && (character == 'z')) + //if (mask!=MASK_NONE) { - key = gKeyboard->inverseTranslateKey('Z'); + if((key == gKeyboard->inverseTranslateKey('Z')) && (character == 'y')) + { + key = gKeyboard->inverseTranslateKey('Y'); + } + else if ((key == gKeyboard->inverseTranslateKey('Y')) && (character == 'z')) + { + key = gKeyboard->inverseTranslateKey('Z'); + } } mRawKeyEvent = event; @@ -1803,6 +1813,299 @@ void LLWindowMacOSX::spawnWebBrowser(const std::string& escaped_url, bool async) } } + +// Device and Element Interfaces + +typedef enum HIDElementTypeMask +{ + kHIDElementTypeInput = 1 << 1, + kHIDElementTypeOutput = 1 << 2, + kHIDElementTypeFeature = 1 << 3, + kHIDElementTypeCollection = 1 << 4, + kHIDElementTypeIO = kHIDElementTypeInput | kHIDElementTypeOutput | kHIDElementTypeFeature, + kHIDElementTypeAll = kHIDElementTypeIO | kHIDElementTypeCollection +}HIDElementTypeMask; + +struct hu_element_t +{ + unsigned long type; // the type defined by IOHIDElementType in IOHIDKeys.h + long usage; // usage within above page from IOUSBHIDParser.h which defines specific usage + long usagePage; // usage page from IOUSBHIDParser.h which defines general usage + void* cookie; // unique value( within device of specific vendorID and productID ) which identifies element, will NOT change + long min; // reported min value possible + long max; // reported max value possible + long scaledMin; // reported scaled min value possible + long scaledMax; // reported scaled max value possible + long size; // size in bits of data return from element + unsigned char relative; // are reports relative to last report( deltas ) + unsigned char wrapping; // does element wrap around( one value higher than max is min ) + unsigned char nonLinear; // are the values reported non-linear relative to element movement + unsigned char preferredState; // does element have a preferred state( such as a button ) + unsigned char nullState; // does element have null state + long units; // units value is reported in( not used very often ) + long unitExp; // exponent for units( also not used very often ) + char name[256]; // name of element( c string ) + + // runtime variables + long initialCenter; // center value at start up + unsigned char hasCenter; // whether or not to use center for calibration + long minReport; // min returned value + long maxReport; // max returned value( calibrate call ) + long userMin; // user set value to scale to( scale call ) + long userMax; + + struct hu_element_t* pPrevious; // previous element( NULL at list head ) + struct hu_element_t* pChild; // next child( only of collections ) + struct hu_element_t* pSibling; // next sibling( for elements and collections ) + + long depth; +}; + +struct HidDevice +{ // interface to device, NULL = no interface + char mProduct[256]; // name of product + long mlocalID; // long representing location in USB( or other I/O ) chain which device is pluged into, can identify specific device on machine + long mUsage; // usage page from IOUSBHID Parser.h which defines general usage + long mUsagePage; // usage within above page from IOUSBHID Parser.h which defines specific usage +}; + +/************************************************************************* +* +* hu_BuildDevice( inHIDDevice ) +* +* Purpose: given a IO device object build a flat device record including device info and all elements +* +* Notes: handles NULL lists properly +* +* Inputs: inHIDDevice - the I/O device object +* +* Returns: hu_device_t* - the address of the new device record +*/ + + +static void populate_device_info( io_object_t io_obj_p, CFDictionaryRef device_dic, HidDevice* devicep ) +{ + CFMutableDictionaryRef io_properties = nil; + io_registry_entry_t entry1; + io_registry_entry_t entry2; + kern_return_t rc; + + // Mac OS X currently is not mirroring all USB properties to HID page so need to look at USB device page also + // get dictionary for usb properties: step up two levels and get CF dictionary for USB properties + // try to get parent1 + rc = IORegistryEntryGetParentEntry( io_obj_p, kIOServicePlane, &entry1 ); + if ( KERN_SUCCESS == rc ) + { + rc = IORegistryEntryGetParentEntry( entry1, kIOServicePlane, &entry2 ); + + IOObjectRelease( entry1 ); + + if ( KERN_SUCCESS == rc ) + { + rc = IORegistryEntryCreateCFProperties( entry2, &io_properties, kCFAllocatorDefault, kNilOptions ); + // either way, release parent2 + IOObjectRelease( entry2 ); + } + } + if ( KERN_SUCCESS == rc ) + { + // IORegistryEntryCreateCFProperties() succeeded + if ( io_properties != nil ) + { + CFTypeRef dict_element = 0; + // get device info + // try hid dictionary first, if fail then go to usb dictionary + + + dict_element = CFDictionaryGetValue( device_dic, CFSTR(kIOHIDProductKey) ); + if ( !dict_element ) + { + dict_element = CFDictionaryGetValue( io_properties, CFSTR( "USB Product Name" ) ); + } + if ( dict_element ) + { + bool res = CFStringGetCString((CFStringRef)dict_element, devicep->mProduct, 256, kCFStringEncodingUTF8); + if ( !res ) + { + LL_WARNS("Joystick") << "Failed to populate mProduct" << LL_ENDL; + } + } + + dict_element = CFDictionaryGetValue( device_dic, CFSTR( kIOHIDLocationIDKey ) ); + if ( !dict_element ) + { + dict_element = CFDictionaryGetValue( io_properties, CFSTR( "locationID" ) ); + } + if ( dict_element ) + { + bool res = CFNumberGetValue( (CFNumberRef)dict_element, kCFNumberLongType, &devicep->mlocalID ); + if ( !res ) + { + LL_WARNS("Joystick") << "Failed to populate mLocalID" << LL_ENDL; + } + } + + dict_element = CFDictionaryGetValue( device_dic, CFSTR( kIOHIDPrimaryUsagePageKey ) ); + if ( dict_element ) + { + bool res = CFNumberGetValue( (CFNumberRef)dict_element, kCFNumberLongType, &devicep->mUsagePage ); + if ( !res ) + { + LL_WARNS("Joystick") << "Failed to populate mUsagePage" << LL_ENDL; + } + dict_element = CFDictionaryGetValue( device_dic, CFSTR( kIOHIDPrimaryUsageKey ) ); + if ( dict_element ) + { + if ( !CFNumberGetValue( (CFNumberRef)dict_element, kCFNumberLongType, &devicep->mUsage ) ) + { + LL_WARNS("Joystick") << "Failed to populate mUsage" << LL_ENDL; + } + } + } + CFRelease(io_properties); + } + else + { + LL_WARNS("Joystick") << "Failed to populate fields" << LL_ENDL; + } + } +} + +HidDevice populate_device( io_object_t io_obj ) +{ + void* interfacep = nullptr; + HidDevice device; + memset( &device, 0, sizeof( HidDevice ) ); + CFMutableDictionaryRef device_dic = 0; + kern_return_t result = IORegistryEntryCreateCFProperties( io_obj, &device_dic, kCFAllocatorDefault, kNilOptions ); + + if ( KERN_SUCCESS == result + && device_dic ) + { + IOReturn io_result = kIOReturnSuccess; + HRESULT query_result = S_OK; + SInt32 the_score = 0; + IOCFPlugInInterface **the_interface = NULL; + + + io_result = IOCreatePlugInInterfaceForService( io_obj, kIOHIDDeviceUserClientTypeID, + kIOCFPlugInInterfaceID, &the_interface, &the_score ); + if ( io_result == kIOReturnSuccess ) + { + query_result = ( *the_interface )->QueryInterface( the_interface, CFUUIDGetUUIDBytes( kIOHIDDeviceInterfaceID ), ( LPVOID * ) & ( interfacep ) ); + if ( query_result != S_OK ) + { + LL_WARNS("Joystick") << "QueryInterface failed" << LL_ENDL; + } + IODestroyPlugInInterface( the_interface ); + } + else + { + LL_WARNS("Joystick") << "IOCreatePlugInInterfaceForService failed" << LL_ENDL; + } + + if ( interfacep ) + { + result = ( *( IOHIDDeviceInterface** )interfacep )->open( interfacep, 0 ); + + if ( result != kIOReturnSuccess) + { + LL_WARNS("Joystick") << "open failed" << LL_ENDL; + } + } + // extract needed fields + populate_device_info( io_obj, device_dic, &device ); + + // Release interface + if ( interfacep ) + { + ( *( IOHIDDeviceInterface** ) interfacep )->close( interfacep ); + + ( *( IOHIDDeviceInterface** ) interfacep )->Release( interfacep ); + + interfacep = NULL; + } + + CFRelease( device_dic ); + } + else + { + LL_WARNS("Joystick") << "populate_device failed" << LL_ENDL; + } + + return device; +} + +static void get_devices(std::list &list_of_devices, + io_iterator_t inIODeviceIterator) +{ + IOReturn result = kIOReturnSuccess; // assume success( optimist! ) + io_object_t io_obj = 0; + + while ( 0 != (io_obj = IOIteratorNext( inIODeviceIterator ) ) ) + { + HidDevice device = populate_device( io_obj ); + + list_of_devices.push_back(device); + + // release the device object, it is no longer needed + result = IOObjectRelease( io_obj ); + if ( KERN_SUCCESS != result ) + { + LL_WARNS("Joystick") << "IOObjectRelease failed" << LL_ENDL; + } + } +} + +bool LLWindowMacOSX::getInputDevices(U32 device_type_filter, + std::function osx_callback, + void* win_callback, + void* userdata) +{ + CFMutableDictionaryRef device_dict_ref; + IOReturn result = kIOReturnSuccess; // assume success( optimist! ) + + // Set up matching dictionary to search the I/O Registry for HID devices we are interested in. Dictionary reference is NULL if error. + + // A dictionary to match devices to? + device_dict_ref = IOServiceMatching( kIOHIDDeviceKey ); + + // BUG FIX! one reference is consumed by IOServiceGetMatchingServices + CFRetain( device_dict_ref ); + io_iterator_t io_iter = 0; + + // create an IO object iterator + result = IOServiceGetMatchingServices( kIOMasterPortDefault, device_dict_ref, &io_iter ); + if ( kIOReturnSuccess != result ) + { + LL_WARNS("Joystick") << "IOServiceGetMatchingServices failed" << LL_ENDL; + } + + if ( io_iter ) + { + // add all existing devices + std::list device_list; + + get_devices(device_list, io_iter); + + std::list::iterator iter; + + for (iter = device_list.begin(); iter != device_list.end(); ++iter) + { + S32 size = sizeof(long); + LLSD::Binary data; //just an std::vector + data.resize(size); + memcpy(&data[0], &iter->mlocalID, size); + std::string label(iter->mProduct); + + osx_callback(label, data, userdata); + } + } + + CFRelease( device_dict_ref ); + return false; // todo: should be true once UI part gets done +} + LLSD LLWindowMacOSX::getNativeKeyData() { LLSD result = LLSD::emptyMap(); diff --git a/indra/llwindow/llwindowmacosx.h b/indra/llwindow/llwindowmacosx.h index 851c860017..cec4f86b25 100644 --- a/indra/llwindow/llwindowmacosx.h +++ b/indra/llwindow/llwindowmacosx.h @@ -112,6 +112,11 @@ public: void interruptLanguageTextInput() override; void spawnWebBrowser(const std::string& escaped_url, bool async) override; F32 getSystemUISize() override; + + bool getInputDevices(U32 device_type_filter, + std::function osx_callback, + void* win_callback, + void* userdata) override; static std::vector getDisplaysResolutionList(); diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 2e560ddb0a..b959c03406 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -4495,7 +4495,10 @@ void* LLWindowWin32::getDirectInput8() return &gDirectInput8; } -bool LLWindowWin32::getInputDevices(U32 device_type_filter, void * di8_devices_callback, void* userdata) +bool LLWindowWin32::getInputDevices(U32 device_type_filter + std::function osx_callback, + void * di8_devices_callback, + void* userdata) { if (gDirectInput8 != NULL) { diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index b391acc12d..6fbb956539 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -129,7 +129,10 @@ public: static void setDPIAwareness(); /*virtual*/ void* getDirectInput8(); - /*virtual*/ bool getInputDevices(U32 device_type_filter, void * di8_devices_callback, void* userdata); + /*virtual*/ bool getInputDevices(U32 device_type_filter, + std::function osx_callback, + void* win_callback, + void* userdata); U32 getRawWParam() { return mRawWParam; } diff --git a/indra/llxml/llxmlnode.cpp b/indra/llxml/llxmlnode.cpp index 455df13e48..43d4adbc26 100644 --- a/indra/llxml/llxmlnode.cpp +++ b/indra/llxml/llxmlnode.cpp @@ -837,7 +837,7 @@ bool LLXMLNode::getLayeredXMLNode(LLXMLNodePtr& root, if (!LLXMLNode::parseFile(filename, root, NULL)) { - LL_WARNS() << "Problem reading UI description file: " << filename << LL_ENDL; + LL_WARNS() << "Problem reading UI description file: " << filename << " " << errno << LL_ENDL; return false; } diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index f1f156c2e0..e416172eb2 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -599,6 +599,9 @@ std::unique_ptr> LLFilePicker::navOpenFilterProc(ELoadF switch(filter) { case FFLOAD_ALL: + case FFLOAD_EXE: + allowedv->push_back("app"); + allowedv->push_back("exe"); allowedv->push_back("wav"); allowedv->push_back("bvh"); allowedv->push_back("anim"); @@ -617,9 +620,6 @@ std::unique_ptr> LLFilePicker::navOpenFilterProc(ELoadF allowedv->push_back("tpic"); allowedv->push_back("png"); break; - case FFLOAD_EXE: - allowedv->push_back("app"); - allowedv->push_back("exe"); break; case FFLOAD_WAV: allowedv->push_back("wav"); diff --git a/indra/newview/llfloaterjoystick.cpp b/indra/newview/llfloaterjoystick.cpp index 558b14bba7..5265411e54 100644 --- a/indra/newview/llfloaterjoystick.cpp +++ b/indra/newview/llfloaterjoystick.cpp @@ -250,6 +250,12 @@ void LLFloaterJoystick::refresh() initFromSettings(); } +void LLFloaterJoystick::addDeviceCallback(std::string &name, LLSD::Binary& value, void* userdata) +{ + LLFloaterJoystick * floater = (LLFloaterJoystick*)userdata; + floater->mJoysticksCombo->add(name, value, ADD_BOTTOM, 1); +} + void LLFloaterJoystick::addDevice(std::string &name, LLSD& value) { mJoysticksCombo->add(name, value, ADD_BOTTOM, 1); @@ -264,19 +270,21 @@ void LLFloaterJoystick::refreshListOfDevices() mHasDeviceList = false; + void* win_calback = nullptr; // di8_devices_callback callback is immediate and happens in scope of getInputDevices() #if LL_WINDOWS && !LL_MESA_HEADLESS // space navigator is marked as DI8DEVCLASS_GAMECTRL in ndof lib U32 device_type = DI8DEVCLASS_GAMECTRL; - void* callback = &di8_list_devices_callback; + win_calback = di8_list_devices_callback; +#elif LL_DARWIN + U32 device_type = 0; #else // MAC doesn't support device search yet // On MAC there is an ndof_idsearch and it is possible to specify product // and manufacturer in NDOF_Device for ndof_init_first to pick specific one U32 device_type = 0; - void* callback = NULL; #endif - if (gViewerWindow->getWindow()->getInputDevices(device_type, callback, this)) + if (gViewerWindow->getWindow()->getInputDevices(device_type, addDeviceCallback, win_calback, this)) { mHasDeviceList = true; } diff --git a/indra/newview/llfloaterjoystick.h b/indra/newview/llfloaterjoystick.h index 1d46efd3f6..912d9b5310 100644 --- a/indra/newview/llfloaterjoystick.h +++ b/indra/newview/llfloaterjoystick.h @@ -46,6 +46,7 @@ public: virtual void draw(); static void setSNDefaults(); + static void addDeviceCallback(std::string &name, LLSD::Binary& value, void* userdata); void addDevice(std::string &name, LLSD& value); protected: diff --git a/indra/newview/llviewerjoystick.cpp b/indra/newview/llviewerjoystick.cpp index e35cb26ce1..26a626f60f 100644 --- a/indra/newview/llviewerjoystick.cpp +++ b/indra/newview/llviewerjoystick.cpp @@ -227,8 +227,17 @@ std::string string_from_guid(const GUID &guid) return res; } +#elif LL_DARWIN + +bool macos_devices_callback(std::string &product, LLSD::Binary &data, void* userdata) +{ + //LLViewerJoystick::getInstance()->initDevice(&device, product_name, data); + return false; +} + #endif + // ----------------------------------------------------------------------------- void LLViewerJoystick::updateEnabled(bool autoenable) { @@ -365,19 +374,21 @@ void LLViewerJoystick::init(bool autoenable) { if (mNdofDev) { + void* win_callback = nullptr; + std::function osx_callback; // di8_devices_callback callback is immediate and happens in scope of getInputDevices() #if LL_WINDOWS && !LL_MESA_HEADLESS // space navigator is marked as DI8DEVCLASS_GAMECTRL in ndof lib U32 device_type = DI8DEVCLASS_GAMECTRL; - void* callback = &di8_devices_callback; + win_callback = &di8_devices_callback; #else // MAC doesn't support device search yet // On MAC there is an ndof_idsearch and it is possible to specify product // and manufacturer in NDOF_Device for ndof_init_first to pick specific one U32 device_type = 0; - void* callback = NULL; + osx_callback = macos_devices_callback; #endif - if (!gViewerWindow->getWindow()->getInputDevices(device_type, callback, NULL)) + if (!gViewerWindow->getWindow()->getInputDevices(device_type, osx_callback, win_callback, NULL)) { LL_INFOS("Joystick") << "Failed to gather devices from window. Falling back to ndof's init" << LL_ENDL; // Failed to gather devices from windows, init first suitable one @@ -438,21 +449,23 @@ void LLViewerJoystick::initDevice(LLSD &guid) { #if LIB_NDOF mLastDeviceUUID = guid; + void* win_callback = nullptr; + std::function osx_callback; #if LL_WINDOWS && !LL_MESA_HEADLESS // space navigator is marked as DI8DEVCLASS_GAMECTRL in ndof lib U32 device_type = DI8DEVCLASS_GAMECTRL; - void* callback = &di8_devices_callback; + win_callback = &di8_devices_callback; #else // MAC doesn't support device search yet // On MAC there is an ndof_idsearch and it is possible to specify product // and manufacturer in NDOF_Device for ndof_init_first to pick specific one U32 device_type = 0; - void* callback = NULL; + osx_callback = macos_devices_callback; #endif mDriverState = JDS_INITIALIZING; - if (!gViewerWindow->getWindow()->getInputDevices(device_type, callback, NULL)) + if (!gViewerWindow->getWindow()->getInputDevices(device_type, osx_callback, win_callback, NULL)) { LL_INFOS("Joystick") << "Failed to gather devices from window. Falling back to ndof's init" << LL_ENDL; // Failed to gather devices from windows, init first suitable one diff --git a/indra/newview/llviewerjoystick.h b/indra/newview/llviewerjoystick.h index 3b4f898710..49f8c8eabf 100644 --- a/indra/newview/llviewerjoystick.h +++ b/indra/newview/llviewerjoystick.h @@ -103,7 +103,11 @@ private: bool mCameraUpdated; bool mOverrideCamera; U32 mJoystickRun; - LLSD mLastDeviceUUID; // _GUID as U8 binary map, integer 1 for no device/ndof's device + + // Windows: _GUID as U8 binary map + // MacOS: long as an U8 binary map + // Else: integer 1 for no device/ndof's default device + LLSD mLastDeviceUUID; static F32 sLastDelta[7]; static F32 sDelta[7]; diff --git a/indra/newview/skins/default/xui/en/floater_about_land.xml b/indra/newview/skins/default/xui/en/floater_about_land.xml index b1ed245378..d0d4b4ebc0 100644 --- a/indra/newview/skins/default/xui/en/floater_about_land.xml +++ b/indra/newview/skins/default/xui/en/floater_about_land.xml @@ -435,6 +435,18 @@ left_pad="5" top_pad="7" width="180" /> + + Place Page: +