Files
barrier/lib/platform/COSXScreen.cpp
crs fe044cfab1 Fixed problem with multimonitor on OS X. The bug was simply that
the cursor wasn't being parked in the center of the main screen
but instead at the center of the total display surface.  This could
place it off or dangerously close to the edge of the transparent
window that covers the main screen and prevent synergy from capturing
mouse motion.
2004-10-27 21:22:36 +00:00

982 lines
21 KiB
C++

/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2004 Chris Schoeneman
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file COPYING that should have accompanied this file.
*
* This package 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 General Public License for more details.
*/
#include "COSXScreen.h"
#include "COSXClipboard.h"
#include "COSXEventQueueBuffer.h"
#include "COSXKeyState.h"
#include "COSXScreenSaver.h"
#include "CClipboard.h"
#include "CLog.h"
#include "IEventQueue.h"
#include "TMethodEventJob.h"
//
// COSXScreen
//
COSXScreen::COSXScreen(bool isPrimary) :
m_isPrimary(isPrimary),
m_isOnScreen(m_isPrimary),
m_cursorPosValid(false),
m_cursorHidden(false),
m_keyState(NULL),
m_sequenceNumber(0),
m_screensaver(NULL),
m_screensaverNotify(false),
m_ownClipboard(false),
m_hiddenWindow(NULL),
m_userInputWindow(NULL),
m_displayManagerNotificationUPP(NULL)
{
try {
m_displayID = CGMainDisplayID();
updateScreenShape();
m_screensaver = new COSXScreenSaver();
m_keyState = new COSXKeyState();
if (m_isPrimary) {
// 1x1 window (to minimze the back buffer allocated for this
// window.
Rect bounds = { 100, 100, 101, 101 };
// m_hiddenWindow is a window meant to let us get mouse moves
// when the focus is on another computer. If you get your event
// from the application event target you'll get every mouse
// moves. On the other hand the Window event target will only
// get events when the mouse moves over the window.
// The ignoreClicks attributes makes it impossible for the
// user to click on our invisible window.
CreateNewWindow(kUtilityWindowClass,
kWindowNoShadowAttribute |
kWindowIgnoreClicksAttribute |
kWindowNoActivatesAttribute,
&bounds, &m_hiddenWindow);
// Make it invisible
SetWindowAlpha(m_hiddenWindow, 0);
ShowWindow(m_hiddenWindow);
// m_userInputWindow is a window meant to let us get mouse moves
// when the focus is on this computer.
Rect inputBounds = { 100, 100, 200, 200 };
CreateNewWindow(kUtilityWindowClass,
kWindowNoShadowAttribute |
kWindowOpaqueForEventsAttribute |
kWindowStandardHandlerAttribute,
&inputBounds, &m_userInputWindow);
SetWindowAlpha(m_userInputWindow, 0);
}
m_displayManagerNotificationUPP =
NewDMExtendedNotificationUPP(displayManagerCallback);
OSStatus err = GetCurrentProcess(&m_PSN);
err = DMRegisterExtendedNotifyProc(m_displayManagerNotificationUPP,
this, 0, &m_PSN);
}
catch (...) {
DMRemoveExtendedNotifyProc(m_displayManagerNotificationUPP,
NULL, &m_PSN, 0);
if (m_hiddenWindow) {
ReleaseWindow(m_hiddenWindow);
m_hiddenWindow = NULL;
}
if (m_userInputWindow) {
ReleaseWindow(m_userInputWindow);
m_userInputWindow = NULL;
}
delete m_keyState;
delete m_screensaver;
throw;
}
// install event handlers
EVENTQUEUE->adoptHandler(CEvent::kSystem, IEventQueue::getSystemTarget(),
new TMethodEventJob<COSXScreen>(this,
&COSXScreen::handleSystemEvent));
// install the platform event queue
EVENTQUEUE->adoptBuffer(new COSXEventQueueBuffer);
}
COSXScreen::~COSXScreen()
{
disable();
EVENTQUEUE->adoptBuffer(NULL);
EVENTQUEUE->removeHandler(CEvent::kSystem, IEventQueue::getSystemTarget());
if (m_hiddenWindow) {
ReleaseWindow(m_hiddenWindow);
m_hiddenWindow = NULL;
}
if (m_userInputWindow) {
ReleaseWindow(m_userInputWindow);
m_userInputWindow = NULL;
}
DMRemoveExtendedNotifyProc(m_displayManagerNotificationUPP,
NULL, &m_PSN, 0);
delete m_keyState;
delete m_screensaver;
}
void*
COSXScreen::getEventTarget() const
{
return const_cast<COSXScreen*>(this);
}
bool
COSXScreen::getClipboard(ClipboardID, IClipboard* dst) const
{
COSXClipboard src;
CClipboard::copy(dst, &src);
return true;
}
void
COSXScreen::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const
{
x = m_x;
y = m_y;
w = m_w;
h = m_h;
}
void
COSXScreen::getCursorPos(SInt32& x, SInt32& y) const
{
Point mouse;
GetGlobalMouse(&mouse);
x = mouse.h;
y = mouse.v;
m_cursorPosValid = true;
m_xCursor = x;
m_yCursor = y;
}
void
COSXScreen::reconfigure(UInt32 activeSides)
{
// FIXME
(void)activeSides;
}
void
COSXScreen::warpCursor(SInt32 x, SInt32 y)
{
// move cursor without generating events
CGPoint pos;
pos.x = x;
pos.y = y;
CGWarpMouseCursorPosition(pos);
// save new cursor position
m_xCursor = x;
m_yCursor = y;
m_cursorPosValid = true;
}
SInt32
COSXScreen::getJumpZoneSize() const
{
// FIXME -- is this correct?
return 1;
}
bool
COSXScreen::isAnyMouseButtonDown() const
{
// FIXME
return false;
}
void
COSXScreen::getCursorCenter(SInt32& x, SInt32& y) const
{
x = m_xCenter;
y = m_yCenter;
}
void
COSXScreen::postMouseEvent(const CGPoint & pos) const
{
// synthesize event. CGPostMouseEvent is a particularly good
// example of a bad API. we have to shadow the mouse state to
// use this API and if we want to support more buttons we have
// to recompile.
//
// the order of buttons on the mac is:
// 1 - Left
// 2 - Right
// 3 - Middle
// Whatever the USB device defined.
//
// It is a bit weird that the behaviour of buttons over 3 are dependent
// on currently plugged in USB devices.
CGPostMouseEvent(pos, true, sizeof(m_buttons) / sizeof(m_buttons[0]),
m_buttons[0],
m_buttons[2],
m_buttons[1],
m_buttons[3],
m_buttons[4]);
}
void
COSXScreen::fakeMouseButton(ButtonID id, bool press) const
{
// get button index
UInt32 index = id - kButtonLeft;
if (index >= sizeof(m_buttons) / sizeof(m_buttons[0])) {
return;
}
// update state
m_buttons[index] = press;
CGPoint pos;
if (!m_cursorPosValid) {
SInt32 x, y;
getCursorPos(x, y);
}
pos.x = m_xCursor;
pos.y = m_yCursor;
postMouseEvent(pos);
}
void
COSXScreen::fakeMouseMove(SInt32 x, SInt32 y) const
{
// synthesize event
CGPoint pos;
pos.x = x;
pos.y = y;
postMouseEvent(pos);
// save new cursor position
m_xCursor = x;
m_yCursor = y;
m_cursorPosValid = true;
}
void
COSXScreen::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const
{
// OS X does not appear to have a fake relative mouse move function.
// simulate it by getting the current mouse position and adding to
// that. this can yield the wrong answer but there's not much else
// we can do.
// get current position
Point oldPos;
GetGlobalMouse(&oldPos);
// synthesize event
CGPoint pos;
pos.x = oldPos.h + dx;
pos.y = oldPos.v + dy;
postMouseEvent(pos);
// we now assume we don't know the current cursor position
m_cursorPosValid = false;
}
void
COSXScreen::fakeMouseWheel(SInt32 delta) const
{
// synergy uses a wheel step size of 120. the mac uses a step size of 1.
delta /= 120;
if (delta == 0) {
return;
}
CFPropertyListRef pref = ::CFPreferencesCopyValue(
CFSTR("com.apple.scrollwheel.scaling") ,
kCFPreferencesAnyApplication,
kCFPreferencesCurrentUser,
kCFPreferencesAnyHost);
int32_t wheelIncr = 1;
if (pref != NULL) {
CFTypeID id = CFGetTypeID(pref);
if (id == CFNumberGetTypeID()) {
CFNumberRef value = static_cast<CFNumberRef>(pref);
double scaling;
if (CFNumberGetValue(value, kCFNumberDoubleType, &scaling)) {
wheelIncr = (int32_t)(8 * scaling);
if (wheelIncr == 0) {
wheelIncr = 1;
}
}
}
CFRelease(pref);
}
// note that we ignore the magnitude of the delta. i think this is to
// avoid local wheel acceleration.
if (delta < 0) {
wheelIncr = -wheelIncr;
}
CGPostScrollWheelEvent(1, wheelIncr);
}
void
COSXScreen::enable()
{
// FIXME -- install clipboard snooper (if we need one)
if (m_isPrimary) {
// FIXME -- start watching jump zones
}
else {
// FIXME -- prevent system from entering power save mode
// hide cursor
if (!m_cursorHidden) {
CGDisplayHideCursor(m_displayID);
m_cursorHidden = true;
}
// warp the mouse to the cursor center
fakeMouseMove(m_xCenter, m_yCenter);
// FIXME -- prepare to show cursor if it moves
}
updateKeys();
}
void
COSXScreen::disable()
{
if (m_isPrimary) {
// FIXME -- stop watching jump zones, stop capturing input
}
else {
// show cursor
if (m_cursorHidden) {
CGDisplayShowCursor(m_displayID);
m_cursorHidden = false;
}
// FIXME -- allow system to enter power saving mode
}
// FIXME -- uninstall clipboard snooper (if we needed one)
m_isOnScreen = m_isPrimary;
}
void
COSXScreen::enter()
{
if (m_isPrimary) {
// stop capturing input, watch jump zones
HideWindow( m_userInputWindow );
ShowWindow( m_hiddenWindow );
SetMouseCoalescingEnabled(true, NULL);
CGSetLocalEventsSuppressionInterval(HUGE_VAL);
}
else {
// show cursor
if (m_cursorHidden) {
CGDisplayShowCursor(m_displayID);
m_cursorHidden = false;
}
// reset buttons
for (UInt32 i = 0; i < sizeof(m_buttons) / sizeof(m_buttons[0]); ++i) {
m_buttons[i] = false;
}
}
// now on screen
m_isOnScreen = true;
}
bool
COSXScreen::leave()
{
// FIXME -- choose keyboard layout if per-process and activate it here
if (m_isPrimary) {
// update key and button state
updateKeys();
// warp to center
warpCursor(m_xCenter, m_yCenter);
// capture events
HideWindow(m_hiddenWindow);
ShowWindow(m_userInputWindow);
RepositionWindow(m_userInputWindow,
m_userInputWindow, kWindowCenterOnMainScreen);
SetUserFocusWindow(m_userInputWindow);
// The OS will coalesce some events if they are similar enough in a
// short period of time this is bad for us since we need every event
// to send it over to other machines. So disable it.
SetMouseCoalescingEnabled(false, NULL);
CGSetLocalEventsSuppressionInterval(0.0001);
}
else {
// hide cursor
if (!m_cursorHidden) {
CGDisplayHideCursor(m_displayID);
m_cursorHidden = true;
}
// warp the mouse to the cursor center
fakeMouseMove(m_xCenter, m_yCenter);
// FIXME -- prepare to show cursor if it moves
// take keyboard focus
// FIXME
}
// now off screen
m_isOnScreen = false;
return true;
}
bool
COSXScreen::setClipboard(ClipboardID, const IClipboard* src)
{
COSXClipboard dst;
m_ownClipboard = true;
if (src != NULL) {
// save clipboard data
return CClipboard::copy(&dst, src);
}
else {
// assert clipboard ownership
if (!dst.open(0)) {
return false;
}
dst.empty();
dst.close();
return true;
}
}
void
COSXScreen::checkClipboards()
{
if (m_ownClipboard && !COSXClipboard::isOwnedBySynergy()) {
static ScrapRef sScrapbook = NULL;
ScrapRef currentScrap;
GetCurrentScrap(&currentScrap);
if (sScrapbook != currentScrap) {
m_ownClipboard = false;
sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardClipboard);
sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardSelection);
sScrapbook = currentScrap;
}
}
}
void
COSXScreen::openScreensaver(bool notify)
{
m_screensaverNotify = notify;
if (!m_screensaverNotify) {
m_screensaver->disable();
}
}
void
COSXScreen::closeScreensaver()
{
if (!m_screensaverNotify) {
m_screensaver->enable();
}
}
void
COSXScreen::screensaver(bool activate)
{
if (activate) {
m_screensaver->activate();
}
else {
m_screensaver->deactivate();
}
}
void
COSXScreen::resetOptions()
{
// no options
}
void
COSXScreen::setOptions(const COptionsList&)
{
// no options
}
void
COSXScreen::setSequenceNumber(UInt32 seqNum)
{
m_sequenceNumber = seqNum;
}
bool
COSXScreen::isPrimary() const
{
return m_isPrimary;
}
void
COSXScreen::sendEvent(CEvent::Type type, void* data) const
{
EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), data));
}
void
COSXScreen::sendClipboardEvent(CEvent::Type type, ClipboardID id) const
{
CClipboardInfo* info = (CClipboardInfo*)malloc(sizeof(CClipboardInfo));
info->m_id = id;
info->m_sequenceNumber = m_sequenceNumber;
sendEvent(type, info);
}
void
COSXScreen::handleSystemEvent(const CEvent& event, void*)
{
EventRef* carbonEvent = reinterpret_cast<EventRef*>(event.getData());
assert(carbonEvent != NULL);
UInt32 eventClass = GetEventClass(*carbonEvent);
switch (eventClass) {
case kEventClassMouse:
switch (GetEventKind(*carbonEvent)) {
case kEventMouseDown:
{
UInt16 myButton;
GetEventParameter(*carbonEvent,
kEventParamMouseButton,
typeMouseButton,
NULL,
sizeof(myButton),
NULL,
&myButton);
onMouseButton(true, myButton);
break;
}
case kEventMouseUp:
{
UInt16 myButton;
GetEventParameter(*carbonEvent,
kEventParamMouseButton,
typeMouseButton,
NULL,
sizeof(myButton),
NULL,
&myButton);
onMouseButton(false, myButton);
break;
}
case kEventMouseDragged:
case kEventMouseMoved:
{
HIPoint point;
GetEventParameter(*carbonEvent,
kEventParamMouseLocation,
typeHIPoint,
NULL,
sizeof(point),
NULL,
&point);
onMouseMove((SInt32)point.x, (SInt32)point.y);
break;
}
case kEventMouseWheelMoved:
{
EventMouseWheelAxis axis;
SInt32 delta;
GetEventParameter(*carbonEvent,
kEventParamMouseWheelAxis,
typeMouseWheelAxis,
NULL,
sizeof(axis),
NULL,
&axis);
if (axis == kEventMouseWheelAxisY) {
GetEventParameter(*carbonEvent,
kEventParamMouseWheelDelta,
typeLongInteger,
NULL,
sizeof(delta),
NULL,
&delta);
onMouseWheel(120 * (SInt32)delta);
}
break;
}
}
break;
case kEventClassKeyboard:
switch (GetEventKind(*carbonEvent)) {
case kEventRawKeyUp:
case kEventRawKeyDown:
case kEventRawKeyRepeat:
case kEventRawKeyModifiersChanged:
// case kEventHotKeyPressed:
// case kEventHotKeyReleased:
onKey(*carbonEvent);
break;
}
break;
case kEventClassWindow:
SendEventToWindow(*carbonEvent, m_userInputWindow);
switch (GetEventKind(*carbonEvent)) {
case kEventWindowActivated:
LOG((CLOG_DEBUG1 "window activated"));
break;
case kEventWindowDeactivated:
LOG((CLOG_DEBUG1 "window deactivated"));
break;
case kEventWindowFocusAcquired:
LOG((CLOG_DEBUG1 "focus acquired"));
break;
case kEventWindowFocusRelinquish:
LOG((CLOG_DEBUG1 "focus released"));
break;
}
break;
default:
break;
}
}
bool
COSXScreen::onMouseMove(SInt32 mx, SInt32 my)
{
LOG((CLOG_DEBUG2 "mouse move %+d,%+d", mx, my));
SInt32 x = mx - m_xCursor;
SInt32 y = my - m_yCursor;
if ((x == 0 && y == 0) || (mx == m_xCenter && mx == m_yCenter)) {
return true;
}
// save position to compute delta of next motion
m_xCursor = mx;
m_yCursor = my;
if (m_isOnScreen) {
// motion on primary screen
sendEvent(getMotionOnPrimaryEvent(),
CMotionInfo::alloc(m_xCursor, m_yCursor));
}
else {
// motion on secondary screen. warp mouse back to
// center.
warpCursor(m_xCenter, m_yCenter);
// examine the motion. if it's about the distance
// from the center of the screen to an edge then
// it's probably a bogus motion that we want to
// ignore (see warpCursorNoFlush() for a further
// description).
static SInt32 bogusZoneSize = 10;
if (-x + bogusZoneSize > m_xCenter - m_x ||
x + bogusZoneSize > m_x + m_w - m_xCenter ||
-y + bogusZoneSize > m_yCenter - m_y ||
y + bogusZoneSize > m_y + m_h - m_yCenter) {
LOG((CLOG_DEBUG "dropped bogus motion %+d,%+d", x, y));
}
else {
// send motion
sendEvent(getMotionOnSecondaryEvent(), CMotionInfo::alloc(x, y));
}
}
return true;
}
bool
COSXScreen::onMouseButton(bool pressed, UInt16 macButton) const
{
// Buttons 2 and 3 are inverted on the mac
ButtonID button = mapMacButtonToSynergy(macButton);
if (pressed) {
LOG((CLOG_DEBUG1 "event: button press button=%d", button));
if (button != kButtonNone) {
sendEvent(getButtonDownEvent(), CButtonInfo::alloc(button));
}
}
else {
LOG((CLOG_DEBUG1 "event: button release button=%d", button));
if (button != kButtonNone) {
sendEvent(getButtonUpEvent(), CButtonInfo::alloc(button));
}
}
return true;
}
bool
COSXScreen::onMouseWheel(SInt32 delta) const
{
LOG((CLOG_DEBUG1 "event: button wheel delta=%d", delta));
sendEvent(getWheelEvent(), CWheelInfo::alloc(delta));
return true;
}
pascal void
COSXScreen::displayManagerCallback(void* inUserData, SInt16 inMessage, void*)
{
COSXScreen* screen = (COSXScreen*)inUserData;
if (inMessage == kDMNotifyEvent) {
screen->onDisplayChange();
}
}
bool
COSXScreen::onDisplayChange()
{
// screen resolution may have changed. save old shape.
SInt32 xOld = m_x, yOld = m_y, wOld = m_w, hOld = m_h;
// update shape
updateScreenShape();
// do nothing if resolution hasn't changed
if (xOld != m_x || yOld != m_y || wOld != m_w || hOld != m_h) {
if (m_isPrimary) {
// warp mouse to center if off screen
if (!m_isOnScreen) {
warpCursor(m_xCenter, m_yCenter);
}
}
// send new screen info
sendEvent(getShapeChangedEvent());
}
return true;
}
bool
COSXScreen::onKey(EventRef event) const
{
UInt32 eventKind = GetEventKind(event);
// get the key
UInt32 virtualKey;
GetEventParameter(event, kEventParamKeyCode, typeUInt32,
NULL, sizeof(virtualKey), NULL, &virtualKey);
LOG((CLOG_DEBUG1 "event: Key event kind: %d, keycode=%d", eventKind, virtualKey));
// sadly, OS X doesn't report the virtualKey for modifier keys.
// virtualKey will be zero for modifier keys. since that's not good
// enough we'll have to figure out what the key was.
if (virtualKey == 0 && eventKind == kEventRawKeyModifiersChanged) {
// get old and new modifier state
KeyModifierMask oldMask = getActiveModifiers();
KeyModifierMask newMask = mapMacModifiersToSynergy(event);
m_keyState->handleModifierKeys(getEventTarget(), oldMask, newMask);
return true;
}
// decode event type
bool down = (eventKind == kEventRawKeyDown);
bool up = (eventKind == kEventRawKeyUp);
bool isRepeat = (eventKind == kEventRawKeyRepeat);
// map event to keys
KeyModifierMask mask;
COSXKeyState::CKeyIDs keys;
KeyButton button = m_keyState->mapKeyFromEvent(keys, &mask, event);
if (button == 0) {
return false;
}
// update button state
if (down) {
m_keyState->setKeyDown(button, true);
}
else if (up) {
if (!isKeyDown(button)) {
// up event for a dead key. throw it away.
return false;
}
m_keyState->setKeyDown(button, false);
}
// send key events
for (COSXKeyState::CKeyIDs::const_iterator i = keys.begin();
i != keys.end(); ++i) {
m_keyState->sendKeyEvent(getEventTarget(), down, isRepeat,
*i, mask, 1, button);
}
return true;
}
ButtonID
COSXScreen::mapMacButtonToSynergy(UInt16 macButton) const
{
switch (macButton) {
case 1:
return kButtonLeft;
case 2:
return kButtonRight;
case 3:
return kButtonMiddle;
}
return kButtonNone;
}
KeyModifierMask
COSXScreen::mapMacModifiersToSynergy(EventRef event) const
{
// get native bit mask
UInt32 macMask;
GetEventParameter(event, kEventParamKeyModifiers, typeUInt32,
NULL, sizeof(macMask), NULL, &macMask);
// convert
KeyModifierMask outMask = 0;
if ((macMask & shiftKey) != 0) {
outMask |= KeyModifierShift;
}
if ((macMask & rightShiftKey) != 0) {
outMask |= KeyModifierShift;
}
if ((macMask & controlKey) != 0) {
outMask |= KeyModifierControl;
}
if ((macMask & rightControlKey) != 0) {
outMask |= KeyModifierControl;
}
if ((macMask & cmdKey) != 0) {
outMask |= KeyModifierAlt;
}
if ((macMask & optionKey) != 0) {
outMask |= KeyModifierSuper;
}
if ((macMask & rightOptionKey) != 0) {
outMask |= KeyModifierSuper;
}
if ((macMask & alphaLock) != 0) {
outMask |= KeyModifierCapsLock;
}
return outMask;
}
void
COSXScreen::updateButtons()
{
// FIXME -- get current button state into m_buttons[]
}
IKeyState*
COSXScreen::getKeyState() const
{
return m_keyState;
}
void
COSXScreen::updateScreenShape()
{
// get info for each display
CGDisplayCount displayCount = 0;
if (CGGetActiveDisplayList(0, NULL, &displayCount) != CGDisplayNoErr) {
return;
}
if (displayCount == 0) {
return;
}
CGDirectDisplayID* displays = new CGDirectDisplayID[displayCount];
if (displays == NULL) {
return;
}
if (CGGetActiveDisplayList(displayCount,
displays, &displayCount) != CGDisplayNoErr) {
delete[] displays;
return;
}
// get smallest rect enclosing all display rects
CGRect totalBounds = CGRectZero;
for (CGDisplayCount i = 0; i < displayCount; ++i) {
CGRect bounds = CGDisplayBounds(displays[i]);
totalBounds = CGRectUnion(totalBounds, bounds);
}
// get shape of default screen
m_x = (SInt32)totalBounds.origin.x;
m_y = (SInt32)totalBounds.origin.y;
m_w = (SInt32)totalBounds.size.width;
m_h = (SInt32)totalBounds.size.height;
// get center of default screen
GDHandle mainScreen = GetMainDevice();
if (mainScreen != NULL) {
const Rect& rect = (*mainScreen)->gdRect;
m_xCenter = (rect.left + rect.right) / 2;
m_yCenter = (rect.top + rect.bottom) / 2;
}
else {
m_xCenter = m_x + (m_w >> 1);
m_yCenter = m_y + (m_h >> 1);
}
delete[] displays;
LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d on %u %s", m_x, m_y, m_w, m_h, displayCount, (displayCount == 1) ? "display" : "displays"));
}