1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2019 Chris Warren-Smith.
4//
5// This program is distributed under the terms of the GPL v2.0 or later
6// Download the GNU Public License (GPL) from www.gnu.org
7//
8
9#include <config.h>
10#include <dirent.h>
11#include <unistd.h>
12#include <sys/stat.h>
13#include <FL/fl_ask.H>
14#include "platform/fltk/MainWindow.h"
15#include "platform/fltk/HelpWidget.h"
16#include "platform/fltk/FileWidget.h"
17#include "ui/strlib.h"
18#include "common/device.h"
19
20FileWidget *fileWidget;
21String click;
22enum SORT_BY { e_name, e_size, e_time } sortBy;
23bool sortDesc;
24
25const char CMD_SET_DIR = '+';
26const char CMD_CHG_DIR = '!';
27const char CMD_ENTER_PATH = '@';
28const char CMD_SAVE_AS = '~';
29const char CMD_SORT_DATE = '#';
30const char CMD_SORT_SIZE = '^';
31const char CMD_SORT_NAME = '$';
32
33struct FileNode {
34 FileNode(const char *label, const char *name, time_t m_time, off_t size, bool isdir) :
35 _label(label, strlen(label)),
36 _name(name, strlen(name)),
37 _m_time(m_time),
38 _size(size),
39 _isdir(isdir) {
40 }
41
42 String _label;
43 String _name;
44 time_t _m_time;
45 off_t _size;
46 bool _isdir;
47};
48
49int stringCompare(const void *a, const void *b) {
50 String *s1 = ((String **) a)[0];
51 String *s2 = ((String **) b)[0];
52 return strcasecmp(s1->c_str(), s2->c_str());
53}
54
55int fileNodeCompare(const void *a, const void *b) {
56 FileNode *n1 = ((FileNode **) a)[0];
57 FileNode *n2 = ((FileNode **) b)[0];
58 int result = 0;
59 switch (sortBy) {
60 case e_name:
61 if (n1->_isdir && !n2->_isdir) {
62 result = -1;
63 } else if (!n1->_isdir && n2->_isdir) {
64 result = 1;
65 } else {
66 result = strcasecmp(n1->_name.c_str(), n2->_name.c_str());
67 }
68 break;
69 case e_size:
70 result = n1->_size < n2->_size ? -1 : n1->_size > n2->_size ? 1 : 0;
71 break;
72 case e_time:
73 result = n1->_m_time < n2->_m_time ? -1 : n1->_m_time > n2->_m_time ? 1 : 0;
74 break;
75 }
76 if (sortDesc) {
77 result = -result;
78 }
79 return result;
80}
81
82void updateSortBy(SORT_BY newSort) {
83 if (sortBy == newSort) {
84 sortDesc = !sortDesc;
85 } else {
86 sortBy = newSort;
87 sortDesc = false;
88 }
89}
90
91static void anchorClick_event(void *) {
92 Fl::remove_check(anchorClick_event);
93 fileWidget->anchorClick();
94}
95
96static void anchorClick_cb(Fl_Widget *w, void *v) {
97 if (fileWidget) {
98 click.clear();
99 click.append((char *)v);
100 Fl::add_check(anchorClick_event); // post message
101 }
102}
103
104FileWidget::FileWidget(Fl_Widget *rect, int fontSize) :
105 HelpWidget(rect, fontSize),
106 _saveEditorAs(0),
107 _recentPaths(NULL) {
108 callback(anchorClick_cb);
109 fileWidget = this;
110 sortDesc = false;
111 sortBy = e_name;
112}
113
114FileWidget::~FileWidget() {
115 fileWidget = NULL;
116 delete _recentPaths;
117}
118
119//
120// convert slash chars in filename to forward slashes
121//
122const char *FileWidget::forwardSlash(char *filename) {
123 const char *result = 0;
124 int len = filename ? strlen(filename) : 0;
125 for (int i = 0; i < len; i++) {
126 if (filename[i] == '\\') {
127 filename[i] = '/';
128 result = &filename[i];
129 }
130 }
131 return result;
132}
133
134/**
135 * return the name component of the full file path
136 */
137const char *FileWidget::splitPath(const char *filename, char *path) {
138 const char *result = strrchr(filename, '/');
139 if (!result) {
140 result = strrchr(filename, '\\');
141 }
142
143 if (!result) {
144 result = filename;
145 } else {
146 // skip slash
147 result++;
148 }
149
150 if (path) {
151 // return the path component
152 int len = result - filename - 1;
153 if (len > 0) {
154 strncpy(path, filename, len);
155 // snip any double slashes
156 while (len > 0 && path[len - 1] == '/') {
157 len--;
158 }
159 path[len] = 0;
160 } else {
161 path[0] = 0;
162 }
163 }
164
165 return result;
166}
167
168//
169// removes CRLF line endings
170//
171const char *FileWidget::trimEOL(char *buffer) {
172 int index = strlen(buffer) - 1;
173 const char *result = buffer;
174 while (index > 0 && (buffer[index] == '\r' || buffer[index] == '\n')) {
175 buffer[index] = 0;
176 index--;
177 }
178 return result;
179}
180
181//
182// anchor link clicked
183//
184void FileWidget::anchorClick() {
185 const char *target = click.c_str();
186
187 switch (target[0]) {
188 case CMD_CHG_DIR:
189 changeDir(target + 1);
190 return;
191
192 case CMD_SET_DIR:
193 setDir(target + 1);
194 return;
195
196 case CMD_SAVE_AS:
197 saveAs();
198 return;
199
200 case CMD_ENTER_PATH:
201 enterPath();
202 return;
203
204 case CMD_SORT_NAME:
205 updateSortBy(e_name);
206 displayPath();
207 return;
208
209 case CMD_SORT_SIZE:
210 updateSortBy(e_size);
211 displayPath();
212 return;
213
214 case CMD_SORT_DATE:
215 updateSortBy(e_time);
216 displayPath();
217 return;
218 }
219
220 String docHome;
221 if (target[0] == '/') {
222 const char *base = getDocHome();
223 if (base && base[0]) {
224 // remove any overlapping string segments between the docHome
225 // of the index page, eg c:/home/cache/smh/handheld/ and the
226 // anchored sub-page, eg, "/handheld/articles/2006/... "
227 // ie, remove the URL component from the file name
228 int len = strlen(base);
229 const char *p = strchr(base + 1, '/');
230 while (p && *p && *(p + 1)) {
231 if (strncmp(p, target, len - (p - base)) == 0) {
232 len = p - base;
233 break;
234 }
235 p = strchr(p + 1, '/');
236 }
237 docHome.append(base, len);
238 }
239 } else {
240 docHome.append(_path);
241 }
242
243 if (_saveEditorAs) {
244 Fl_Input *input = (Fl_Input *)getInput("saveas");
245 input->value(target);
246 } else {
247 setDocHome(docHome);
248 String fullPath;
249 fullPath.append(docHome.c_str());
250 fullPath.append("/");
251 fullPath.append(target[0] == '/' ? target + 1 : target);
252 wnd->editFile(fullPath.c_str());
253 }
254}
255
256//
257// open file
258//
259void FileWidget::fileOpen(EditorWidget *_saveEditorAs) {
260 this->_saveEditorAs = _saveEditorAs;
261 displayPath();
262}
263
264//
265// display the given path
266//
267void FileWidget::openPath(const char *newPath, StringList *recentPaths) {
268 if (newPath && access(newPath, R_OK) == 0) {
269 strlcpy(_path, newPath, PATH_MAX);
270 } else {
271 getcwd(_path, sizeof(_path));
272 }
273
274 delete _recentPaths;
275 _recentPaths = recentPaths;
276
277 forwardSlash(_path);
278 displayPath();
279 redraw();
280}
281
282//
283// change to the given dir
284//
285void FileWidget::changeDir(const char *target) {
286 char newPath[PATH_MAX + 1];
287
288 strlcpy(newPath, _path, PATH_MAX);
289
290 // file browser window
291 if (strcmp(target, "..") == 0) {
292 // go up a level c:/src/foo or /src/foo
293 char *p = strrchr(newPath, '/');
294 if (strchr(newPath, '/') != p) {
295 *p = 0; // last item not first
296 } else {
297 *(p + 1) = 0; // found root
298 }
299 } else {
300 // go down a level
301 if (newPath[strlen(newPath) - 1] != '/') {
302 strlcat(newPath, "/", PATH_MAX);
303 }
304 strlcat(newPath, target, PATH_MAX);
305 }
306 setDir(newPath);
307}
308
309//
310// set to the given dir
311//
312void FileWidget::setDir(const char *target) {
313 if (chdir(target) == 0) {
314 strlcpy(_path, target, PATH_MAX);
315 displayPath();
316 } else {
317 fl_message("Invalid path '%s'", target);
318 }
319}
320
321//
322// display the path
323//
324void FileWidget::displayPath() {
325 dirent *entry;
326 struct stat stbuf;
327 strlib::List<FileNode *> files;
328 char modifedTime[100];
329 String html;
330
331 if (chdir(_path) != 0) {
332 return;
333 }
334
335 DIR *dp = opendir(_path);
336 if (dp == 0) {
337 return;
338 }
339
340 while ((entry = readdir(dp)) != 0) {
341 char *name = entry->d_name;
342 int len = strlen(name);
343 if (strcmp(name, ".") == 0) {
344 continue;
345 }
346
347 if (strcmp(name, "..") == 0) {
348 if (strcmp(_path, "/") != 0 && strcmp(_path + 1, ":/") != 0) {
349 // not "/" or "C:/"
350 files.add(new FileNode("Go up", "..", stbuf.st_mtime, stbuf.st_size, true));
351 }
352 } else if (stat(name, &stbuf) != -1 && stbuf.st_mode & S_IFDIR) {
353 files.add(new FileNode(name, name, stbuf.st_mtime, stbuf.st_size, true));
354 } else if (strncasecmp(name + len - 4, ".htm", 4) == 0 ||
355 strncasecmp(name + len - 5, ".html", 5) == 0 ||
356 strncasecmp(name + len - 4, ".bas", 4) == 0 ||
357 strncasecmp(name + len - 4, ".txt", 4) == 0) {
358 files.add(new FileNode(name, name, stbuf.st_mtime, stbuf.st_size, false));
359 }
360 }
361 closedir(dp);
362
363 files.sort(fileNodeCompare);
364
365 if (_saveEditorAs) {
366 const char *path = _saveEditorAs->getFilename();
367 const char *slash = strrchr(path, '/');
368 html.append("<b>Save ").append(slash ? slash + 1 : path).append(" as:<font size=3><br>")
369 .append("<input size=220 type=text value='").append(slash ? slash + 1 : path)
370 .append("' name=saveas>&nbsp;<input type=button onclick='")
371 .append(CMD_SAVE_AS).append("' value='Save As'><br><font size=1>");
372 }
373
374 _recentPaths->sort(stringCompare);
375 html.append("<b>Recent places:</b><br>");
376 List_each(String*, it, *_recentPaths) {
377 String *nextPath = (*it);
378 if (!nextPath->equals(_path)) {
379 html.append("<a href='")
380 .append(CMD_SET_DIR)
381 .append(nextPath)
382 .append("'> [ ")
383 .append(nextPath)
384 .append(" ]</a><br>");
385 }
386 }
387
388 html.append("<br><b>Files in: <a href=")
389 .append(CMD_ENTER_PATH).append(">").append(_path)
390 .append("</a></b>");
391
392 html.append("<table><tr bgcolor=#e1e1e1>")
393 .append("<td><a href=").append(CMD_SORT_NAME).append("><b><u>Name</u></b></a></td>")
394 .append("<td><a href=").append(CMD_SORT_SIZE).append("><b><u>Size</u></b></a></td>")
395 .append("<td><a href=").append(CMD_SORT_DATE).append("><b><u>Date</u></b></a></td></tr>");
396
397 List_each(FileNode*, it, files) {
398 FileNode *fileNode = (*it);
399 html.append("<tr bgcolor=#f1f1f1>").append("<td><a href='");
400 if (fileNode->_isdir) {
401 html.append(CMD_CHG_DIR);
402 }
403 html.append(fileNode->_name).append("'>");
404 if (fileNode->_isdir) {
405 html.append("[ ");
406 }
407 html.append(fileNode->_label);
408 if (fileNode->_isdir) {
409 html.append(" ]");
410 }
411 html.append("</a></td>");
412 html.append("<td>");
413 if (fileNode->_isdir) {
414 html.append(0);
415 } else {
416 html.append(fileNode->_isdir ? 0 : (int)fileNode->_size);
417 }
418 html.append("</td>");
419 if ((int)fileNode->_m_time > 0) {
420 strftime(modifedTime, sizeof(modifedTime), "%Y-%m-%d %I:%M %p",
421 localtime(&fileNode->_m_time));
422 html.append("<td>").append(modifedTime).append("</td></tr>");
423 } else {
424 html.append("<td>?</td></tr>");
425 }
426 }
427
428 html.append("</table>");
429 loadBuffer(html);
430 take_focus();
431}
432
433//
434// open the path
435//
436void FileWidget::enterPath() {
437 const char *newPath = fl_input("Enter path:", _path);
438 if (newPath != 0) {
439 if (chdir(newPath) == 0) {
440 strlcpy(_path, newPath, PATH_MAX);
441 displayPath();
442 } else {
443 fl_message("Invalid path '%s'", newPath);
444 }
445 }
446}
447
448//
449// event handler
450//
451int FileWidget::handle(int e) {
452 static char buffer[PATH_MAX];
453 static int dnd_active = 0;
454
455 switch (e) {
456 case FL_SHOW:
457 if (_saveEditorAs) {
458 _saveEditorAs = 0;
459 displayPath();
460 }
461 break;
462
463 case FL_DND_LEAVE:
464 dnd_active = 0;
465 return 1;
466
467 case FL_DND_DRAG:
468 case FL_DND_RELEASE:
469 case FL_DND_ENTER:
470 dnd_active = 1;
471 return 1;
472
473 case FL_MOVE:
474 if (dnd_active) {
475 return 1; // return 1 to become drop-target
476 }
477 break;
478
479 case FL_PASTE:
480 strncpy(buffer, Fl::event_text(), Fl::event_length());
481 buffer[Fl::event_length()] = 0;
482 forwardSlash(buffer);
483 wnd->editFile(buffer);
484 dnd_active = 0;
485 return 1;
486 }
487
488 return HelpWidget::handle(e);
489}
490
491//
492// save the buffer with a new name
493//
494void FileWidget::saveAs() {
495 if (_saveEditorAs) {
496 const char *enteredPath = getInputValue(getInput("saveas"));
497 if (enteredPath && enteredPath[0]) {
498 // a path has been entered
499 char savepath[PATH_MAX + 1];
500 if (enteredPath[0] == '~') {
501 // substitute ~ for $HOME contents
502 const char *home = dev_getenv("HOME");
503 if (home) {
504 strlcpy(savepath, home, PATH_MAX);
505 } else {
506 savepath[0] = 0;
507 }
508 strlcat(savepath, enteredPath + 1, PATH_MAX);
509 } else if (enteredPath[0] == '/' || enteredPath[1] == ':') {
510 // absolute path given
511 strlcpy(savepath, enteredPath, PATH_MAX);
512 } else {
513 strlcpy(savepath, _path, PATH_MAX);
514 strlcat(savepath, "/", PATH_MAX);
515 strlcat(savepath, enteredPath, PATH_MAX);
516 }
517 if (strcasecmp(savepath + strlen(savepath) - 4, ".bas") != 0) {
518 strlcat(savepath, ".bas", PATH_MAX);
519 }
520 const char *msg = "%s\n\nFile already exists.\nDo you want to replace it?";
521 if (access(savepath, 0) != 0 || fl_choice(msg, "Yes", "No", 0, savepath) == 0) {
522 _saveEditorAs->doSaveFile(savepath, true);
523 }
524 }
525 }
526}
527
528