1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 #if JUCE_MODAL_LOOPS_PERMITTED
exeIsAvailable(String executable)30 static bool exeIsAvailable (String executable)
31 {
32     ChildProcess child;
33 
34     if (child.start ("which " + executable))
35     {
36         child.waitForProcessToFinish (60 * 1000);
37         return (child.getExitCode() == 0);
38     }
39 
40     return false;
41 }
42 
43 class FileChooser::Native    : public FileChooser::Pimpl,
44                                private Timer
45 {
46 public:
Native(FileChooser & fileChooser,int flags)47     Native (FileChooser& fileChooser, int flags)
48         : owner (fileChooser),
49           isDirectory         ((flags & FileBrowserComponent::canSelectDirectories)   != 0),
50           isSave              ((flags & FileBrowserComponent::saveMode)               != 0),
51           selectMultipleFiles ((flags & FileBrowserComponent::canSelectMultipleItems) != 0),
52           warnAboutOverwrite  ((flags & FileBrowserComponent::warnAboutOverwriting)   != 0)
53     {
54         const File previousWorkingDirectory (File::getCurrentWorkingDirectory());
55 
56         // use kdialog for KDE sessions or if zenity is missing
57         if (exeIsAvailable ("kdialog") && (isKdeFullSession() || ! exeIsAvailable ("zenity")))
58             addKDialogArgs();
59         else
60             addZenityArgs();
61     }
62 
~Native()63     ~Native() override
64     {
65         finish (true);
66     }
67 
runModally()68     void runModally() override
69     {
70         child.start (args, ChildProcess::wantStdOut);
71 
72         while (child.isRunning())
73             if (! MessageManager::getInstance()->runDispatchLoopUntil (20))
74                 break;
75 
76         finish (false);
77     }
78 
launch()79     void launch() override
80     {
81         child.start (args, ChildProcess::wantStdOut);
82         startTimer (100);
83     }
84 
85 private:
86     FileChooser& owner;
87     bool isDirectory, isSave, selectMultipleFiles, warnAboutOverwrite;
88 
89     ChildProcess child;
90     StringArray args;
91     String separator;
92 
timerCallback()93     void timerCallback() override
94     {
95         if (! child.isRunning())
96         {
97             stopTimer();
98             finish (false);
99         }
100     }
101 
finish(bool shouldKill)102     void finish (bool shouldKill)
103     {
104         String result;
105         Array<URL> selection;
106 
107         if (shouldKill)
108             child.kill();
109         else
110             result = child.readAllProcessOutput().trim();
111 
112         if (result.isNotEmpty())
113         {
114             StringArray tokens;
115 
116             if (selectMultipleFiles)
117                 tokens.addTokens (result, separator, "\"");
118             else
119                 tokens.add (result);
120 
121             for (auto& token : tokens)
122                 selection.add (URL (File::getCurrentWorkingDirectory().getChildFile (token)));
123         }
124 
125         if (! shouldKill)
126         {
127             child.waitForProcessToFinish (60 * 1000);
128             owner.finished (selection);
129         }
130     }
131 
getTopWindowID()132     static uint64 getTopWindowID() noexcept
133     {
134         if (TopLevelWindow* top = TopLevelWindow::getActiveTopLevelWindow())
135             return (uint64) (pointer_sized_uint) top->getWindowHandle();
136 
137         return 0;
138     }
139 
isKdeFullSession()140     static bool isKdeFullSession()
141     {
142         return SystemStats::getEnvironmentVariable ("KDE_FULL_SESSION", String())
143                      .equalsIgnoreCase ("true");
144     }
145 
addKDialogArgs()146     void addKDialogArgs()
147     {
148         args.add ("kdialog");
149 
150         if (owner.title.isNotEmpty())
151             args.add ("--title=" + owner.title);
152 
153         if (uint64 topWindowID = getTopWindowID())
154         {
155             args.add ("--attach");
156             args.add (String (topWindowID));
157         }
158 
159         if (selectMultipleFiles)
160         {
161             separator = "\n";
162             args.add ("--multiple");
163             args.add ("--separate-output");
164             args.add ("--getopenfilename");
165         }
166         else
167         {
168             if (isSave)             args.add ("--getsavefilename");
169             else if (isDirectory)   args.add ("--getexistingdirectory");
170             else                    args.add ("--getopenfilename");
171         }
172 
173         File startPath;
174 
175         if (owner.startingFile.exists())
176         {
177             startPath = owner.startingFile;
178         }
179         else if (owner.startingFile.getParentDirectory().exists())
180         {
181             startPath = owner.startingFile.getParentDirectory();
182         }
183         else
184         {
185             startPath = File::getSpecialLocation (File::userHomeDirectory);
186 
187             if (isSave)
188                 startPath = startPath.getChildFile (owner.startingFile.getFileName());
189         }
190 
191         args.add (startPath.getFullPathName());
192         args.add ("(" + owner.filters.replaceCharacter (';', ' ') + ")");
193     }
194 
addZenityArgs()195     void addZenityArgs()
196     {
197         args.add ("zenity");
198         args.add ("--file-selection");
199 
200         if (warnAboutOverwrite)
201             args.add("--confirm-overwrite");
202 
203         if (owner.title.isNotEmpty())
204             args.add ("--title=" + owner.title);
205 
206         if (selectMultipleFiles)
207         {
208             separator = ":";
209             args.add ("--multiple");
210             args.add ("--separator=" + separator);
211         }
212         else
213         {
214             if (isDirectory)  args.add ("--directory");
215             if (isSave)       args.add ("--save");
216         }
217 
218         if (owner.filters.isNotEmpty() && owner.filters != "*" && owner.filters != "*.*")
219         {
220             StringArray tokens;
221             tokens.addTokens (owner.filters, ";,|", "\"");
222 
223             args.add ("--file-filter=" + tokens.joinIntoString (" "));
224         }
225 
226         if (owner.startingFile.isDirectory())
227             owner.startingFile.setAsCurrentWorkingDirectory();
228         else if (owner.startingFile.getParentDirectory().exists())
229             owner.startingFile.getParentDirectory().setAsCurrentWorkingDirectory();
230         else
231             File::getSpecialLocation (File::userHomeDirectory).setAsCurrentWorkingDirectory();
232 
233         auto filename = owner.startingFile.getFileName();
234 
235         if (! filename.isEmpty())
236             args.add ("--filename=" + filename);
237 
238         // supplying the window ID of the topmost window makes sure that Zenity pops up..
239         if (uint64 topWindowID = getTopWindowID())
240             setenv ("WINDOWID", String (topWindowID).toRawUTF8(), true);
241     }
242 
243     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
244 };
245 #endif
246 
isPlatformDialogAvailable()247 bool FileChooser::isPlatformDialogAvailable()
248 {
249    #if JUCE_DISABLE_NATIVE_FILECHOOSERS || ! JUCE_MODAL_LOOPS_PERMITTED
250     return false;
251    #else
252     static bool canUseNativeBox = exeIsAvailable ("zenity") || exeIsAvailable ("kdialog");
253     return canUseNativeBox;
254    #endif
255 }
256 
showPlatformDialog(FileChooser & owner,int flags,FilePreviewComponent *)257 FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent*)
258 {
259 #if JUCE_MODAL_LOOPS_PERMITTED
260     return new Native (owner, flags);
261 #else
262     return nullptr;
263 #endif
264 }
265 
266 } // namespace juce
267