1/*
2    Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3
4    This file is part of CopyQ.
5
6    CopyQ is free software: you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation, either version 3 of the License, or
9    (at your option) any later version.
10
11    CopyQ is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15
16    You should have received a copy of the GNU General Public License
17    along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18*/
19
20#include "macplatform.h"
21
22#include "app/applicationexceptionhandler.h"
23#include "common/log.h"
24#include "copyqpasteboardmime.h"
25#include "foregroundbackgroundfilter.h"
26#include "macplatformwindow.h"
27#include "platform/mac/macactivity.h"
28#include "urlpasteboardmime.h"
29#include "macclipboard.h"
30
31#include <QApplication>
32#include <QCoreApplication>
33#include <QDir>
34#include <QGuiApplication>
35#include <QScopedPointer>
36#include <QStringList>
37
38#include <Cocoa/Cocoa.h>
39#include <Carbon/Carbon.h>
40
41namespace {
42    class ClipboardApplication : public QApplication
43    {
44    public:
45        ClipboardApplication(int &argc, char **argv)
46            : QApplication(argc, argv)
47            , m_pasteboardMime()
48            , m_pasteboardMimeUrl(QLatin1String("public.url"))
49            , m_pasteboardMimeFileUrl(QLatin1String("public.file-url"))
50        {
51        }
52
53    private:
54        CopyQPasteboardMime m_pasteboardMime;
55        UrlPasteboardMime m_pasteboardMimeUrl;
56        UrlPasteboardMime m_pasteboardMimeFileUrl;
57    };
58
59    template<typename T> inline T* objc_cast(id from)
60    {
61        if (from && [from isKindOfClass:[T class]]) {
62            return static_cast<T*>(from);
63        }
64        return nil;
65    }
66
67    bool isApplicationInItemList(LSSharedFileListRef list) {
68        bool flag = false;
69        UInt32 seed;
70        CFArrayRef items = LSSharedFileListCopySnapshot(list, &seed);
71        if (items) {
72            CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
73            if (url) {
74                for (id item in(__bridge NSArray *) items) {
75                    LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)item;
76                    if (LSSharedFileListItemResolve(itemRef, 0, &url, NULL) == noErr) {
77                        if ([[(__bridge NSURL *) url path] hasPrefix:[[NSBundle mainBundle] bundlePath]]) {
78                            flag = true;
79                            break;
80                        }
81                    }
82                }
83            }
84            CFRelease(items);
85        }
86        return flag;
87    }
88
89    void addToLoginItems()
90    {
91        LSSharedFileListRef list = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, NULL);
92        if (list) {
93            if (!isApplicationInItemList(list)) {
94                CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
95                if (url) {
96                    // Don't "Hide on Launch", as we don't have a window to show anyway
97                    NSDictionary *properties = [NSDictionary
98                        dictionaryWithObject: [NSNumber numberWithBool:NO]
99                        forKey: @"com.apple.loginitem.HideOnLaunch"];
100                    LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(list, kLSSharedFileListItemLast, NULL, NULL, url, (__bridge CFDictionaryRef)properties, NULL);
101                    if (item)
102                        CFRelease(item);
103                } else {
104                    ::log("Unable to find url for bundle, can't auto-load app", LogWarning);
105                }
106            }
107            CFRelease(list);
108        } else {
109            ::log("Unable to access shared file list, can't auto-load app", LogWarning);
110        }
111    }
112
113    void removeFromLoginItems()
114    {
115        LSSharedFileListRef list = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, NULL);
116        if (list) {
117            if (isApplicationInItemList(list)) {
118                CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
119                if (url) {
120                    UInt32 seed;
121                    CFArrayRef items = LSSharedFileListCopySnapshot(list, &seed);
122                    if (items) {
123                        for (id item in(__bridge NSArray *) items) {
124                            LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)item;
125                            if (LSSharedFileListItemResolve(itemRef, 0, &url, NULL) == noErr)
126                                if ([[(__bridge NSURL *) url path] hasPrefix:[[NSBundle mainBundle] bundlePath]])
127                                    LSSharedFileListItemRemove(list, itemRef);
128                        }
129                        CFRelease(items);
130                    } else {
131                        ::log("No items in list of auto-loaded apps, can't stop auto-load of app", LogWarning);
132                    }
133                } else {
134                    ::log("Unable to find url for bundle, can't stop auto-load of app", LogWarning);
135                }
136            }
137            CFRelease(list);
138        } else {
139            ::log("Unable to access shared file list, can't stop auto-load of app", LogWarning);
140        }
141    }
142
143    QString absoluteResourcesePath(const QString &path)
144    {
145        return QCoreApplication::applicationDirPath() + "/../Resources/" + path;
146    }
147
148    template <typename QtApplication>
149    class Activity
150        : public MacActivity
151        , public ApplicationExceptionHandler<QtApplication>
152    {
153    public:
154        Activity(int &argc, char **argv, const QString &reason)
155            : MacActivity(reason)
156            , ApplicationExceptionHandler<QtApplication>(argc, argv)
157        {
158            [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
159        }
160    };
161
162} // namespace
163
164PlatformNativeInterface *platformNativeInterface()
165{
166    static MacPlatform platform;
167    return &platform;
168}
169
170MacPlatform::MacPlatform()
171{
172}
173
174QCoreApplication *MacPlatform::createConsoleApplication(int &argc, char **argv)
175{
176    return new ApplicationExceptionHandler<QCoreApplication>(argc, argv);
177}
178
179QApplication *MacPlatform::createServerApplication(int &argc, char **argv)
180{
181    QApplication *app = new Activity<ClipboardApplication>(argc, argv, "CopyQ Server");
182
183    // Switch the app to foreground when in foreground
184    ForegroundBackgroundFilter::installFilter(app);
185
186    return app;
187}
188
189QGuiApplication *MacPlatform::createMonitorApplication(int &argc, char **argv)
190{
191    return new Activity<ClipboardApplication>(argc, argv, "CopyQ clipboard monitor");
192}
193
194QGuiApplication *MacPlatform::createClipboardProviderApplication(int &argc, char **argv)
195{
196    return new Activity<ClipboardApplication>(argc, argv, "CopyQ clipboard provider");
197}
198
199QCoreApplication *MacPlatform::createClientApplication(int &argc, char **argv)
200{
201    return new Activity<QCoreApplication>(argc, argv, "CopyQ Client");
202}
203
204QGuiApplication *MacPlatform::createTestApplication(int &argc, char **argv)
205{
206    return new Activity<QGuiApplication>(argc, argv, "CopyQ Tests");
207}
208
209PlatformClipboardPtr MacPlatform::clipboard()
210{
211    return PlatformClipboardPtr(new MacClipboard());
212}
213
214QStringList MacPlatform::getCommandLineArguments(int argc, char **argv)
215{
216    QStringList arguments;
217
218    for (int i = 1; i < argc; ++i)
219        arguments.append( QString::fromUtf8(argv[i]) );
220
221    return arguments;
222}
223
224bool MacPlatform::findPluginDir(QDir *pluginsDir)
225{
226    pluginsDir->setPath( qApp->applicationDirPath() );
227    if (pluginsDir->dirName() != "MacOS") {
228        if ( pluginsDir->cd("plugins")) {
229            COPYQ_LOG("Found plugins in build tree");
230            return true;
231        }
232        return false;
233    }
234
235    if ( pluginsDir->cdUp() // Contents
236            && pluginsDir->cd("PlugIns")
237            && pluginsDir->cd("copyq"))
238    {
239        // OK, found it in the bundle
240        COPYQ_LOG("Found plugins in application bundle");
241        return true;
242    }
243
244    pluginsDir->setPath( qApp->applicationDirPath() );
245
246    if ( pluginsDir->cdUp() // Contents
247            && pluginsDir->cdUp() // copyq.app
248            && pluginsDir->cdUp() // repo root
249            && pluginsDir->cd("plugins")) {
250        COPYQ_LOG("Found plugins in build tree");
251        return true;
252    }
253
254    return false;
255}
256
257QString MacPlatform::defaultEditorCommand()
258{
259    return "open -t -W -n %1";
260}
261
262QString MacPlatform::translationPrefix()
263{
264    return absoluteResourcesePath("translations");
265}
266
267QString MacPlatform::themePrefix()
268{
269    return absoluteResourcesePath("themes");
270}
271
272PlatformWindowPtr MacPlatform::getCurrentWindow()
273{
274    // FIXME: frontmostApplication doesn't seem to work well for own windows (at least in tests).
275    auto window = QApplication::activeWindow();
276    if (window == nullptr)
277        window = QApplication::activeModalWidget();
278    if (window != nullptr)
279        return PlatformWindowPtr(new MacPlatformWindow(window->winId()));
280
281    NSRunningApplication *runningApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
282    return PlatformWindowPtr(new MacPlatformWindow(runningApp));
283}
284
285PlatformWindowPtr MacPlatform::getWindow(WId winId) {
286    return PlatformWindowPtr(new MacPlatformWindow(winId));
287}
288
289bool MacPlatform::isAutostartEnabled()
290{
291    // Note that this will need to be done differently if CopyQ goes into
292    // the App Store.
293    // http://rhult.github.io/articles/sandboxed-launch-on-login/
294    bool isInList = false;
295    LSSharedFileListRef list = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, NULL);
296    if (list) {
297        isInList = isApplicationInItemList(list);
298        CFRelease(list);
299    }
300    return isInList;
301}
302
303void MacPlatform::setAutostartEnabled(bool shouldEnable)
304{
305    if (shouldEnable != isAutostartEnabled()) {
306        if (shouldEnable) {
307            addToLoginItems();
308        } else {
309            removeFromLoginItems();
310        }
311    }
312}
313