/* Copyright (c) 2014, Lukas Holecek This file is part of CopyQ. CopyQ is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. CopyQ 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. You should have received a copy of the GNU General Public License along with CopyQ. If not, see . */ #include "macplatformwindow.h" #include "common/log.h" #include #include #include #include #include #include namespace { template inline T* objc_cast(id from) { if ([from isKindOfClass:[T class]]) { return static_cast(from); } return nil; } void sendShortcut(int modifier, int key) { CGEventSourceRef sourceRef = CGEventSourceCreate( kCGEventSourceStateCombinedSessionState); CGEventRef commandDown = CGEventCreateKeyboardEvent(sourceRef, modifier, YES); CGEventRef VDown = CGEventCreateKeyboardEvent(sourceRef, key, YES); CGEventRef VUp = CGEventCreateKeyboardEvent(sourceRef, key, NO); CGEventRef commandUp = CGEventCreateKeyboardEvent(sourceRef, modifier, NO); // 0x000008 is a hack to fix pasting in Emacs? // https://github.com/TermiT/Flycut/pull/18 CGEventSetFlags(VDown,CGEventFlags(kCGEventFlagMaskCommand|0x000008)); CGEventSetFlags(VUp,CGEventFlags(kCGEventFlagMaskCommand|0x000008)); CGEventPost(kCGHIDEventTap, commandDown); CGEventPost(kCGHIDEventTap, VDown); CGEventPost(kCGHIDEventTap, VUp); CGEventPost(kCGHIDEventTap, commandUp); CFRelease(commandDown); CFRelease(VDown); CFRelease(VUp); CFRelease(commandUp); CFRelease(sourceRef); } /** * Delays a sending Command+key operation for delayInMS, and retries up to 'tries' times. * * This function is necessary in order to check that the intended window has come * to the foreground. The "isActive" property only changes when the app gets back * to the "run loop", so we can't block and check it. */ void delayedSendShortcut(int modifier, int key, int64_t delayInMS, uint tries, NSWindow *window) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayInMS * NSEC_PER_MSEC), dispatch_get_main_queue(), ^(void){ if (window && ![window isKeyWindow]) { if (tries > 0) { delayedSendShortcut(modifier, key, delayInMS, tries - 1, window); } else { log("Failed to raise application, will not paste.", LogWarning); } } else { sendShortcut(modifier, key); } }); } pid_t getPidForWid(WId find_wid) { // Build a set of "normal" windows. This is necessary as "NSWindowList" gets things like the // menubar (which can be "owned" by various apps). NSArray *array = (__bridge NSArray*) CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, kCGNullWindowID); for (NSDictionary* dict in array) { long int pid = [(NSNumber*)[dict objectForKey:@"kCGWindowOwnerPID"] longValue]; unsigned long int wid = (unsigned long) [(NSNumber*)[dict objectForKey:@"kCGWindowNumber"] longValue]; if (wid == find_wid) { return pid; } } return 0; } long int getTopWindow(pid_t process_pid) { // Build a set of "normal" windows. This is necessary as "NSWindowList" gets things like the // menubar (which can be "owned" by various apps). QSet widsForProcess; NSArray *array = (__bridge NSArray*) CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, kCGNullWindowID); for (NSDictionary* dict in array) { long int pid = [(NSNumber*)[dict objectForKey:@"kCGWindowOwnerPID"] longValue]; long int wid = [(NSNumber*)[dict objectForKey:@"kCGWindowNumber"] longValue]; long int layer = [(NSNumber*)[dict objectForKey:@"kCGWindowLayer"] longValue]; if (pid == process_pid && layer == 0) { widsForProcess.insert(wid); } } // Now look through the windows in NSWindowList (which are ordered from front to back) // the first window in this list which is also in widsForProcess is our frontmost "normal" window long int wid = -1; for (NSNumber *window : [NSWindow windowNumbersWithOptions:0]) { const long int wid2 = [window longValue]; if (widsForProcess.contains(wid2)) { wid = wid2; break; } } return wid; } QString getTitleFromWid(long int wid) { QString title; if (wid < 0) { return title; } uint32_t windowid[1] = {static_cast(wid)}; CFArrayRef windowArray = CFArrayCreate ( NULL, (const void **)windowid, 1 ,NULL); NSArray *array = (__bridge NSArray*) CGWindowListCreateDescriptionFromArray(windowArray); // Should only be one for (NSDictionary* dict in array) { title = QString::fromNSString([dict objectForKey:@"kCGWindowName"]); } CFRelease(windowArray); return title; } } // namespace MacPlatformWindow::MacPlatformWindow(NSRunningApplication *runningApp): m_windowNumber(-1) , m_window(0) , m_runningApplication(0) { if (runningApp) { m_runningApplication = runningApp; [runningApp retain]; m_windowNumber = getTopWindow(runningApp.processIdentifier); COPYQ_LOG("Created platform window for non-copyq"); } else { log("Failed to convert runningApplication to application", LogWarning); } } MacPlatformWindow::MacPlatformWindow(WId wid): m_windowNumber(-1) , m_window(0) , m_runningApplication(0) { // Try using wid as an actual window ID pid_t pid = getPidForWid(wid); if (pid != 0) { m_runningApplication = [NSRunningApplication runningApplicationWithProcessIdentifier:pid]; m_windowNumber = wid; // This will return 'nil' unless this process owns the window m_window = [NSApp windowWithWindowNumber: wid]; } else if (NSView *view = objc_cast((id)wid)) { // If given a view, its ours m_runningApplication = [NSRunningApplication currentApplication]; m_window = [view window]; [m_runningApplication retain]; [m_window retain]; m_windowNumber = [m_window windowNumber]; COPYQ_LOG("Created platform window for copyq"); } else { log("Failed to convert WId to window", LogWarning); } } MacPlatformWindow::MacPlatformWindow(): m_windowNumber(-1) , m_window(0) , m_runningApplication(0) { } MacPlatformWindow::~MacPlatformWindow() { // Releasing '0' or 'nil' is fine [m_runningApplication release]; [m_window release]; } QString MacPlatformWindow::getTitle() { QString appTitle; if (m_runningApplication) appTitle = QString::fromNSString([m_runningApplication localizedName]); QString windowTitle; if (m_window) windowTitle = QString::fromNSString([m_window title]); if (windowTitle.isEmpty() && m_windowNumber >= 0) windowTitle = getTitleFromWid(m_windowNumber); // We have two separate titles, the application title (shown at the top // left in the menu bar and the window title (shown in the window bar). // Some apps put the app name into the window title as well, some don't. We // want to be able to match on the app name (e.g. Firefox, Safari, or // Terminal) when writing commands, so we'll ensure that the app name is at // the beginning of the returned window title. QString result; if (windowTitle.isEmpty()) { result = appTitle; } else if (appTitle.isEmpty() || windowTitle.startsWith(appTitle)) { result = windowTitle; } else { result = QString("%1 - %2").arg(appTitle, windowTitle); } return result; } void MacPlatformWindow::raise() { if (m_window && m_runningApplication && [m_runningApplication isEqual:[NSRunningApplication currentApplication]]) { COPYQ_LOG(QString("Raise CopyQ")); [NSApp activateIgnoringOtherApps:YES]; [m_window makeKeyAndOrderFront:nil]; } else if (m_runningApplication) { // Shouldn't need to unhide since we should have just been in this // application.. //[m_runningApplication unhide]; COPYQ_LOG(QString("Raise running app.")); [m_runningApplication activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } else { ::log(QString("Tried to raise unknown window."), LogWarning); } } void MacPlatformWindow::pasteClipboard() { if (!m_runningApplication) { log("Failed to paste to unknown window", LogWarning); return; } // Window MUST be raised, otherwise we can't send events to it raise(); // Paste after after 100ms, try 5 times delayedSendShortcut(kVK_Command, kVK_ANSI_V, 100, 5, m_window); } void MacPlatformWindow::copy() { if (!m_runningApplication) { log("Failed to copy from unknown window", LogWarning); return; } // Window MUST be raised, otherwise we can't send events to it raise(); // Copy after after 100ms, try 5 times delayedSendShortcut(kVK_Command, kVK_ANSI_C, 100, 5, m_window); }