1 //
2 // Copyright (c) 2008-2017 the Urho3D project.
3 //
4 // Permission is hereby granted, free of charge, to any person obtaining a copy
5 // of this software and associated documentation files (the "Software"), to deal
6 // in the Software without restriction, including without limitation the rights
7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 // copies of the Software, and to permit persons to whom the Software is
9 // furnished to do so, subject to the following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included in
12 // all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 // THE SOFTWARE.
21 //
22
23 #include "../Precompiled.h"
24
25 #include "../Core/Context.h"
26 #include "../IO/File.h"
27 #include "../IO/FileSystem.h"
28 #include "../Input/InputEvents.h"
29 #include "../UI/DropDownList.h"
30 #include "../UI/FileSelector.h"
31 #include "../UI/LineEdit.h"
32 #include "../UI/ListView.h"
33 #include "../UI/Text.h"
34 #include "../UI/UI.h"
35 #include "../UI/UIEvents.h"
36 #include "../UI/Window.h"
37
38 #include "../DebugNew.h"
39
40 namespace Urho3D
41 {
42
CompareEntries(const FileSelectorEntry & lhs,const FileSelectorEntry & rhs)43 static bool CompareEntries(const FileSelectorEntry& lhs, const FileSelectorEntry& rhs)
44 {
45 if (lhs.directory_ && !rhs.directory_)
46 return true;
47 if (!lhs.directory_ && rhs.directory_)
48 return false;
49 return lhs.name_.Compare(rhs.name_, false) < 0;
50 }
51
FileSelector(Context * context)52 FileSelector::FileSelector(Context* context) :
53 Object(context),
54 directoryMode_(false),
55 ignoreEvents_(false)
56 {
57 window_ = new Window(context_);
58 window_->SetLayout(LM_VERTICAL);
59
60 titleLayout = new UIElement(context_);
61 titleLayout->SetLayout(LM_HORIZONTAL);
62 window_->AddChild(titleLayout);
63
64 titleText_ = new Text(context_);
65 titleLayout->AddChild(titleText_);
66
67 closeButton_ = new Button(context_);
68 titleLayout->AddChild(closeButton_);
69
70 pathEdit_ = new LineEdit(context_);
71 window_->AddChild(pathEdit_);
72
73 fileList_ = new ListView(context_);
74 window_->AddChild(fileList_);
75
76 fileNameLayout_ = new UIElement(context_);
77 fileNameLayout_->SetLayout(LM_HORIZONTAL);
78
79 fileNameEdit_ = new LineEdit(context_);
80 fileNameLayout_->AddChild(fileNameEdit_);
81
82 filterList_ = new DropDownList(context_);
83 fileNameLayout_->AddChild(filterList_);
84
85 window_->AddChild(fileNameLayout_);
86
87 separatorLayout_ = new UIElement(context_);
88 window_->AddChild(separatorLayout_);
89
90 buttonLayout_ = new UIElement(context_);
91 buttonLayout_->SetLayout(LM_HORIZONTAL);
92
93 buttonLayout_->AddChild(new UIElement(context_)); // Add spacer
94
95 cancelButton_ = new Button(context_);
96 cancelButtonText_ = new Text(context_);
97 cancelButtonText_->SetAlignment(HA_CENTER, VA_CENTER);
98 cancelButton_->AddChild(cancelButtonText_);
99 buttonLayout_->AddChild(cancelButton_);
100
101 okButton_ = new Button(context_);
102 okButtonText_ = new Text(context_);
103 okButtonText_->SetAlignment(HA_CENTER, VA_CENTER);
104 okButton_->AddChild(okButtonText_);
105 buttonLayout_->AddChild(okButton_);
106
107 window_->AddChild(buttonLayout_);
108
109 Vector<String> defaultFilters;
110 defaultFilters.Push("*.*");
111 SetFilters(defaultFilters, 0);
112 FileSystem* fileSystem = GetSubsystem<FileSystem>();
113 SetPath(fileSystem->GetCurrentDir());
114
115 // Focus the fileselector's filelist initially when created, and bring to front
116 UI* ui = GetSubsystem<UI>();
117 ui->GetRoot()->AddChild(window_);
118 ui->SetFocusElement(fileList_);
119 window_->SetModal(true);
120
121 SubscribeToEvent(filterList_, E_ITEMSELECTED, URHO3D_HANDLER(FileSelector, HandleFilterChanged));
122 SubscribeToEvent(pathEdit_, E_TEXTFINISHED, URHO3D_HANDLER(FileSelector, HandlePathChanged));
123 SubscribeToEvent(fileNameEdit_, E_TEXTFINISHED, URHO3D_HANDLER(FileSelector, HandleOKPressed));
124 SubscribeToEvent(fileList_, E_ITEMSELECTED, URHO3D_HANDLER(FileSelector, HandleFileSelected));
125 SubscribeToEvent(fileList_, E_ITEMDOUBLECLICKED, URHO3D_HANDLER(FileSelector, HandleFileDoubleClicked));
126 SubscribeToEvent(fileList_, E_UNHANDLEDKEY, URHO3D_HANDLER(FileSelector, HandleFileListKey));
127 SubscribeToEvent(okButton_, E_RELEASED, URHO3D_HANDLER(FileSelector, HandleOKPressed));
128 SubscribeToEvent(cancelButton_, E_RELEASED, URHO3D_HANDLER(FileSelector, HandleCancelPressed));
129 SubscribeToEvent(closeButton_, E_RELEASED, URHO3D_HANDLER(FileSelector, HandleCancelPressed));
130 SubscribeToEvent(window_, E_MODALCHANGED, URHO3D_HANDLER(FileSelector, HandleCancelPressed));
131 }
132
~FileSelector()133 FileSelector::~FileSelector()
134 {
135 window_->Remove();
136 }
137
RegisterObject(Context * context)138 void FileSelector::RegisterObject(Context* context)
139 {
140 context->RegisterFactory<FileSelector>();
141 }
142
SetDefaultStyle(XMLFile * style)143 void FileSelector::SetDefaultStyle(XMLFile* style)
144 {
145 if (!style)
146 return;
147
148 window_->SetDefaultStyle(style);
149 window_->SetStyle("FileSelector");
150
151 titleText_->SetStyle("FileSelectorTitleText");
152 closeButton_->SetStyle("CloseButton");
153
154 okButtonText_->SetStyle("FileSelectorButtonText");
155 cancelButtonText_->SetStyle("FileSelectorButtonText");
156
157 titleLayout->SetStyle("FileSelectorLayout");
158 fileNameLayout_->SetStyle("FileSelectorLayout");
159 buttonLayout_->SetStyle("FileSelectorLayout");
160 separatorLayout_->SetStyle("EditorSeparator");
161
162 fileList_->SetStyle("FileSelectorListView");
163 fileNameEdit_->SetStyle("FileSelectorLineEdit");
164 pathEdit_->SetStyle("FileSelectorLineEdit");
165
166 filterList_->SetStyle("FileSelectorFilterList");
167
168 okButton_->SetStyle("FileSelectorButton");
169 cancelButton_->SetStyle("FileSelectorButton");
170
171 const Vector<SharedPtr<UIElement> >& filterTexts = filterList_->GetListView()->GetContentElement()->GetChildren();
172 for (unsigned i = 0; i < filterTexts.Size(); ++i)
173 filterTexts[i]->SetStyle("FileSelectorFilterText");
174
175 const Vector<SharedPtr<UIElement> >& listTexts = fileList_->GetContentElement()->GetChildren();
176 for (unsigned i = 0; i < listTexts.Size(); ++i)
177 listTexts[i]->SetStyle("FileSelectorListText");
178
179 UpdateElements();
180 }
181
SetTitle(const String & text)182 void FileSelector::SetTitle(const String& text)
183 {
184 titleText_->SetText(text);
185 }
186
SetButtonTexts(const String & okText,const String & cancelText)187 void FileSelector::SetButtonTexts(const String& okText, const String& cancelText)
188 {
189 okButtonText_->SetText(okText);
190 cancelButtonText_->SetText(cancelText);
191 }
192
SetPath(const String & path)193 void FileSelector::SetPath(const String& path)
194 {
195 FileSystem* fileSystem = GetSubsystem<FileSystem>();
196 if (fileSystem->DirExists(path))
197 {
198 path_ = AddTrailingSlash(path);
199 SetLineEditText(pathEdit_, path_);
200 RefreshFiles();
201 }
202 else
203 {
204 // If path was invalid, restore the old path to the line edit
205 if (pathEdit_->GetText() != path_)
206 SetLineEditText(pathEdit_, path_);
207 }
208 }
209
SetFileName(const String & fileName)210 void FileSelector::SetFileName(const String& fileName)
211 {
212 SetLineEditText(fileNameEdit_, fileName);
213 }
214
SetFilters(const Vector<String> & filters,unsigned defaultIndex)215 void FileSelector::SetFilters(const Vector<String>& filters, unsigned defaultIndex)
216 {
217 if (filters.Empty())
218 return;
219
220 ignoreEvents_ = true;
221
222 filters_ = filters;
223 filterList_->RemoveAllItems();
224 for (unsigned i = 0; i < filters_.Size(); ++i)
225 {
226 Text* filterText = new Text(context_);
227 filterList_->AddItem(filterText);
228 filterText->SetText(filters_[i]);
229 filterText->SetStyle("FileSelectorFilterText");
230 }
231 if (defaultIndex > filters.Size())
232 defaultIndex = 0;
233 filterList_->SetSelection(defaultIndex);
234
235 ignoreEvents_ = false;
236
237 if (GetFilter() != lastUsedFilter_)
238 RefreshFiles();
239 }
240
SetDirectoryMode(bool enable)241 void FileSelector::SetDirectoryMode(bool enable)
242 {
243 directoryMode_ = enable;
244 }
245
UpdateElements()246 void FileSelector::UpdateElements()
247 {
248 buttonLayout_->SetFixedHeight(Max(okButton_->GetHeight(), cancelButton_->GetHeight()));
249 }
250
GetDefaultStyle() const251 XMLFile* FileSelector::GetDefaultStyle() const
252 {
253 return window_->GetDefaultStyle(false);
254 }
255
GetTitle() const256 const String& FileSelector::GetTitle() const
257 {
258 return titleText_->GetText();
259 }
260
GetFileName() const261 const String& FileSelector::GetFileName() const
262 {
263 return fileNameEdit_->GetText();
264 }
265
GetFilter() const266 const String& FileSelector::GetFilter() const
267 {
268 Text* selectedFilter = static_cast<Text*>(filterList_->GetSelectedItem());
269 if (selectedFilter)
270 return selectedFilter->GetText();
271 else
272 return String::EMPTY;
273 }
274
GetFilterIndex() const275 unsigned FileSelector::GetFilterIndex() const
276 {
277 return filterList_->GetSelection();
278 }
279
SetLineEditText(LineEdit * edit,const String & text)280 void FileSelector::SetLineEditText(LineEdit* edit, const String& text)
281 {
282 ignoreEvents_ = true;
283 edit->SetText(text);
284 ignoreEvents_ = false;
285 }
286
RefreshFiles()287 void FileSelector::RefreshFiles()
288 {
289 FileSystem* fileSystem = GetSubsystem<FileSystem>();
290
291 ignoreEvents_ = true;
292
293 fileList_->RemoveAllItems();
294 fileEntries_.Clear();
295
296 Vector<String> directories;
297 Vector<String> files;
298 fileSystem->ScanDir(directories, path_, "*", SCAN_DIRS, false);
299 fileSystem->ScanDir(files, path_, GetFilter(), SCAN_FILES, false);
300
301 fileEntries_.Reserve(directories.Size() + files.Size());
302
303 for (unsigned i = 0; i < directories.Size(); ++i)
304 {
305 FileSelectorEntry newEntry;
306 newEntry.name_ = directories[i];
307 newEntry.directory_ = true;
308 fileEntries_.Push(newEntry);
309 }
310
311 for (unsigned i = 0; i < files.Size(); ++i)
312 {
313 FileSelectorEntry newEntry;
314 newEntry.name_ = files[i];
315 newEntry.directory_ = false;
316 fileEntries_.Push(newEntry);
317 }
318
319 // Sort and add to the list view
320 // While items are being added, disable layout update for performance optimization
321 Sort(fileEntries_.Begin(), fileEntries_.End(), CompareEntries);
322 UIElement* listContent = fileList_->GetContentElement();
323 listContent->DisableLayoutUpdate();
324 for (unsigned i = 0; i < fileEntries_.Size(); ++i)
325 {
326 String displayName;
327 if (fileEntries_[i].directory_)
328 displayName = "<DIR> " + fileEntries_[i].name_;
329 else
330 displayName = fileEntries_[i].name_;
331
332 Text* entryText = new Text(context_);
333 fileList_->AddItem(entryText);
334 entryText->SetText(displayName);
335 entryText->SetStyle("FileSelectorListText");
336 }
337 listContent->EnableLayoutUpdate();
338 listContent->UpdateLayout();
339
340 ignoreEvents_ = false;
341
342 // Clear filename from the previous dir so that there is no confusion
343 SetFileName(String::EMPTY);
344 lastUsedFilter_ = GetFilter();
345 }
346
EnterFile()347 bool FileSelector::EnterFile()
348 {
349 unsigned index = fileList_->GetSelection();
350 if (index >= fileEntries_.Size())
351 return false;
352
353 if (fileEntries_[index].directory_)
354 {
355 // If a directory double clicked, enter it. Recognize . and .. as a special case
356 const String& newPath = fileEntries_[index].name_;
357 if ((newPath != ".") && (newPath != ".."))
358 SetPath(path_ + newPath);
359 else if (newPath == "..")
360 {
361 String parentPath = GetParentPath(path_);
362 SetPath(parentPath);
363 }
364
365 return true;
366 }
367 else
368 {
369 // Double clicking a file is the same as pressing OK
370 if (!directoryMode_)
371 {
372 using namespace FileSelected;
373
374 VariantMap& eventData = GetEventDataMap();
375 eventData[P_FILENAME] = path_ + fileEntries_[index].name_;
376 eventData[P_FILTER] = GetFilter();
377 eventData[P_OK] = true;
378 SendEvent(E_FILESELECTED, eventData);
379 }
380 }
381
382 return false;
383 }
384
HandleFilterChanged(StringHash eventType,VariantMap & eventData)385 void FileSelector::HandleFilterChanged(StringHash eventType, VariantMap& eventData)
386 {
387 if (ignoreEvents_)
388 return;
389
390 if (GetFilter() != lastUsedFilter_)
391 RefreshFiles();
392 }
393
HandlePathChanged(StringHash eventType,VariantMap & eventData)394 void FileSelector::HandlePathChanged(StringHash eventType, VariantMap& eventData)
395 {
396 if (ignoreEvents_)
397 return;
398
399 // Attempt to set path. Restores old if does not exist
400 SetPath(pathEdit_->GetText());
401 }
402
HandleFileSelected(StringHash eventType,VariantMap & eventData)403 void FileSelector::HandleFileSelected(StringHash eventType, VariantMap& eventData)
404 {
405 if (ignoreEvents_)
406 return;
407
408 unsigned index = fileList_->GetSelection();
409 if (index >= fileEntries_.Size())
410 return;
411 // If a file selected, update the filename edit field
412 if (!fileEntries_[index].directory_)
413 SetFileName(fileEntries_[index].name_);
414 }
415
HandleFileDoubleClicked(StringHash eventType,VariantMap & eventData)416 void FileSelector::HandleFileDoubleClicked(StringHash eventType, VariantMap& eventData)
417 {
418 if (ignoreEvents_)
419 return;
420
421 if (eventData[ItemDoubleClicked::P_BUTTON] == MOUSEB_LEFT)
422 EnterFile();
423 }
424
HandleFileListKey(StringHash eventType,VariantMap & eventData)425 void FileSelector::HandleFileListKey(StringHash eventType, VariantMap& eventData)
426 {
427 if (ignoreEvents_)
428 return;
429
430 using namespace UnhandledKey;
431
432 int key = eventData[P_KEY].GetInt();
433 if (key == KEY_RETURN || key == KEY_RETURN2 || key == KEY_KP_ENTER)
434 {
435 bool entered = EnterFile();
436 // When a key is used to enter a directory, select the first file if no selection
437 if (entered && !fileList_->GetSelectedItem())
438 fileList_->SetSelection(0);
439 }
440 }
441
HandleOKPressed(StringHash eventType,VariantMap & eventData)442 void FileSelector::HandleOKPressed(StringHash eventType, VariantMap& eventData)
443 {
444 if (ignoreEvents_)
445 return;
446
447 const String& fileName = GetFileName();
448
449 if (!directoryMode_)
450 {
451 if (!fileName.Empty())
452 {
453 using namespace FileSelected;
454
455 VariantMap& newEventData = GetEventDataMap();
456 newEventData[P_FILENAME] = path_ + GetFileName();
457 newEventData[P_FILTER] = GetFilter();
458 newEventData[P_OK] = true;
459 SendEvent(E_FILESELECTED, newEventData);
460 }
461 }
462 else if (eventType == E_RELEASED && !path_.Empty())
463 {
464 using namespace FileSelected;
465
466 VariantMap& newEventData = GetEventDataMap();
467 newEventData[P_FILENAME] = path_;
468 newEventData[P_FILTER] = GetFilter();
469 newEventData[P_OK] = true;
470 SendEvent(E_FILESELECTED, newEventData);
471 }
472 }
473
HandleCancelPressed(StringHash eventType,VariantMap & eventData)474 void FileSelector::HandleCancelPressed(StringHash eventType, VariantMap& eventData)
475 {
476 if (ignoreEvents_)
477 return;
478
479 if (eventType == E_MODALCHANGED && eventData[ModalChanged::P_MODAL].GetBool())
480 return;
481
482 using namespace FileSelected;
483
484 VariantMap& newEventData = GetEventDataMap();
485 newEventData[P_FILENAME] = String::EMPTY;
486 newEventData[P_FILTER] = GetFilter();
487 newEventData[P_OK] = false;
488 SendEvent(E_FILESELECTED, newEventData);
489 }
490
491 }
492