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