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