Merge branch 'blender-v2.81-release'
[blender.git] / source / blender / editors / space_file / filesel.c
1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License
4  * as published by the Free Software Foundation; either version 2
5  * of the License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software Foundation,
14  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15  *
16  * The Original Code is Copyright (C) 2008 Blender Foundation.
17  * All rights reserved.
18  */
19
20 /** \file
21  * \ingroup spfile
22  */
23
24 #include <string.h>
25 #include <stdio.h>
26 #include <math.h>
27
28 #include <sys/stat.h>
29 #include <sys/types.h>
30
31 /* path/file handling stuff */
32 #ifdef WIN32
33 #  include <io.h>
34 #  include <direct.h>
35 #  include "BLI_winstuff.h"
36 #else
37 #  include <unistd.h>
38 #  include <sys/times.h>
39 #  include <dirent.h>
40 #endif
41
42 #include "DNA_space_types.h"
43 #include "DNA_screen_types.h"
44 #include "DNA_userdef_types.h"
45
46 #include "MEM_guardedalloc.h"
47
48 #include "BLI_blenlib.h"
49 #include "BLI_utildefines.h"
50 #include "BLI_fnmatch.h"
51
52 #include "BLO_readfile.h"
53
54 #include "BLT_translation.h"
55
56 #include "BKE_appdir.h"
57 #include "BKE_context.h"
58 #include "BKE_main.h"
59
60 #include "BLF_api.h"
61
62 #include "ED_fileselect.h"
63
64 #include "WM_api.h"
65 #include "WM_types.h"
66
67 #include "RNA_access.h"
68
69 #include "UI_interface.h"
70 #include "UI_interface_icons.h"
71 #include "UI_view2d.h"
72
73 #include "file_intern.h"
74 #include "filelist.h"
75
76 #define VERTLIST_MAJORCOLUMN_WIDTH (25 * UI_UNIT_X)
77
78 FileSelectParams *ED_fileselect_get_params(struct SpaceFile *sfile)
79 {
80   if (!sfile->params) {
81     ED_fileselect_set_params(sfile);
82   }
83   return sfile->params;
84 }
85
86 /**
87  * \note RNA_struct_property_is_set_ex is used here because we want
88  *       the previously used settings to be used here rather then overriding them */
89 short ED_fileselect_set_params(SpaceFile *sfile)
90 {
91   FileSelectParams *params;
92   wmOperator *op = sfile->op;
93
94   const char *blendfile_path = BKE_main_blendfile_path_from_global();
95
96   /* create new parameters if necessary */
97   if (!sfile->params) {
98     sfile->params = MEM_callocN(sizeof(FileSelectParams), "fileselparams");
99     /* set path to most recently opened .blend */
100     BLI_split_dirfile(blendfile_path,
101                       sfile->params->dir,
102                       sfile->params->file,
103                       sizeof(sfile->params->dir),
104                       sizeof(sfile->params->file));
105     sfile->params->filter_glob[0] = '\0';
106     /* set the default thumbnails size */
107     sfile->params->thumbnail_size = U_default.file_space_data.thumbnail_size;
108     /* Show size column by default. */
109     sfile->params->details_flags = U_default.file_space_data.details_flags;
110   }
111
112   params = sfile->params;
113
114   /* set the parameters from the operator, if it exists */
115   if (op) {
116     PropertyRNA *prop;
117     const bool is_files = (RNA_struct_find_property(op->ptr, "files") != NULL);
118     const bool is_filepath = (RNA_struct_find_property(op->ptr, "filepath") != NULL);
119     const bool is_filename = (RNA_struct_find_property(op->ptr, "filename") != NULL);
120     const bool is_directory = (RNA_struct_find_property(op->ptr, "directory") != NULL);
121     const bool is_relative_path = (RNA_struct_find_property(op->ptr, "relative_path") != NULL);
122
123     BLI_strncpy_utf8(
124         params->title, WM_operatortype_name(op->type, op->ptr), sizeof(params->title));
125
126     if ((prop = RNA_struct_find_property(op->ptr, "filemode"))) {
127       params->type = RNA_property_int_get(op->ptr, prop);
128     }
129     else {
130       params->type = FILE_SPECIAL;
131     }
132
133     if (is_filepath && RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
134       char name[FILE_MAX];
135       RNA_string_get(op->ptr, "filepath", name);
136       if (params->type == FILE_LOADLIB) {
137         BLI_strncpy(params->dir, name, sizeof(params->dir));
138         sfile->params->file[0] = '\0';
139       }
140       else {
141         BLI_split_dirfile(name,
142                           sfile->params->dir,
143                           sfile->params->file,
144                           sizeof(sfile->params->dir),
145                           sizeof(sfile->params->file));
146       }
147     }
148     else {
149       if (is_directory && RNA_struct_property_is_set_ex(op->ptr, "directory", false)) {
150         RNA_string_get(op->ptr, "directory", params->dir);
151         sfile->params->file[0] = '\0';
152       }
153
154       if (is_filename && RNA_struct_property_is_set_ex(op->ptr, "filename", false)) {
155         RNA_string_get(op->ptr, "filename", params->file);
156       }
157     }
158
159     if (params->dir[0]) {
160       BLI_cleanup_dir(blendfile_path, params->dir);
161       BLI_path_abs(params->dir, blendfile_path);
162     }
163
164     if (is_directory == true && is_filename == false && is_filepath == false &&
165         is_files == false) {
166       params->flag |= FILE_DIRSEL_ONLY;
167     }
168     else {
169       params->flag &= ~FILE_DIRSEL_ONLY;
170     }
171
172     if ((prop = RNA_struct_find_property(op->ptr, "check_existing"))) {
173       params->flag |= RNA_property_boolean_get(op->ptr, prop) ? FILE_CHECK_EXISTING : 0;
174     }
175     if ((prop = RNA_struct_find_property(op->ptr, "hide_props_region"))) {
176       params->flag |= RNA_property_boolean_get(op->ptr, prop) ? FILE_HIDE_TOOL_PROPS : 0;
177     }
178
179     params->filter = 0;
180     if ((prop = RNA_struct_find_property(op->ptr, "filter_blender"))) {
181       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_BLENDER : 0;
182     }
183     if ((prop = RNA_struct_find_property(op->ptr, "filter_blenlib"))) {
184       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_BLENDERLIB : 0;
185     }
186     if ((prop = RNA_struct_find_property(op->ptr, "filter_backup"))) {
187       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_BLENDER_BACKUP : 0;
188     }
189     if ((prop = RNA_struct_find_property(op->ptr, "filter_image"))) {
190       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_IMAGE : 0;
191     }
192     if ((prop = RNA_struct_find_property(op->ptr, "filter_movie"))) {
193       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_MOVIE : 0;
194     }
195     if ((prop = RNA_struct_find_property(op->ptr, "filter_python"))) {
196       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_PYSCRIPT : 0;
197     }
198     if ((prop = RNA_struct_find_property(op->ptr, "filter_font"))) {
199       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_FTFONT : 0;
200     }
201     if ((prop = RNA_struct_find_property(op->ptr, "filter_sound"))) {
202       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_SOUND : 0;
203     }
204     if ((prop = RNA_struct_find_property(op->ptr, "filter_text"))) {
205       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_TEXT : 0;
206     }
207     if ((prop = RNA_struct_find_property(op->ptr, "filter_archive"))) {
208       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_ARCHIVE : 0;
209     }
210     if ((prop = RNA_struct_find_property(op->ptr, "filter_folder"))) {
211       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_FOLDER : 0;
212     }
213     if ((prop = RNA_struct_find_property(op->ptr, "filter_btx"))) {
214       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_BTX : 0;
215     }
216     if ((prop = RNA_struct_find_property(op->ptr, "filter_collada"))) {
217       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_COLLADA : 0;
218     }
219     if ((prop = RNA_struct_find_property(op->ptr, "filter_alembic"))) {
220       params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_ALEMBIC : 0;
221     }
222     if ((prop = RNA_struct_find_property(op->ptr, "filter_glob"))) {
223       /* Protection against pyscripts not setting proper size limit... */
224       char *tmp = RNA_property_string_get_alloc(
225           op->ptr, prop, params->filter_glob, sizeof(params->filter_glob), NULL);
226       if (tmp != params->filter_glob) {
227         BLI_strncpy(params->filter_glob, tmp, sizeof(params->filter_glob));
228         MEM_freeN(tmp);
229
230         /* Fix stupid things that truncating might have generated,
231          * like last group being a 'match everything' wildcard-only one... */
232         BLI_path_extension_glob_validate(params->filter_glob);
233       }
234       params->filter |= (FILE_TYPE_OPERATOR | FILE_TYPE_FOLDER);
235     }
236     else {
237       params->filter_glob[0] = '\0';
238     }
239
240     if (params->filter != 0) {
241       if (U.uiflag & USER_FILTERFILEEXTS) {
242         params->flag |= FILE_FILTER;
243       }
244       else {
245         params->flag &= ~FILE_FILTER;
246       }
247     }
248
249     /* For now, always init filterid to 'all true' */
250     params->filter_id = FILTER_ID_ALL;
251
252     if (U.uiflag & USER_HIDE_DOT) {
253       params->flag |= FILE_HIDE_DOT;
254     }
255     else {
256       params->flag &= ~FILE_HIDE_DOT;
257     }
258
259     if (params->type == FILE_LOADLIB) {
260       params->flag |= RNA_boolean_get(op->ptr, "link") ? FILE_LINK : 0;
261       params->flag |= RNA_boolean_get(op->ptr, "autoselect") ? FILE_AUTOSELECT : 0;
262       params->flag |= RNA_boolean_get(op->ptr, "active_collection") ? FILE_ACTIVE_COLLECTION : 0;
263     }
264
265     if ((prop = RNA_struct_find_property(op->ptr, "display_type"))) {
266       params->display = RNA_property_enum_get(op->ptr, prop);
267     }
268
269     if ((prop = RNA_struct_find_property(op->ptr, "sort_method"))) {
270       params->sort = RNA_property_enum_get(op->ptr, prop);
271     }
272     else {
273       params->sort = U_default.file_space_data.sort_type;
274     }
275
276     if (params->display == FILE_DEFAULTDISPLAY) {
277       params->display = U_default.file_space_data.display_type;
278     }
279
280     if (is_relative_path) {
281       if ((prop = RNA_struct_find_property(op->ptr, "relative_path"))) {
282         if (!RNA_property_is_set_ex(op->ptr, prop, false)) {
283           RNA_property_boolean_set(op->ptr, prop, (U.flag & USER_RELPATHS) != 0);
284         }
285       }
286     }
287   }
288   else {
289     /* default values, if no operator */
290     params->type = FILE_UNIX;
291     params->flag |= U_default.file_space_data.flag;
292     params->flag &= ~FILE_DIRSEL_ONLY;
293     params->display = FILE_VERTICALDISPLAY;
294     params->sort = FILE_SORT_ALPHA;
295     params->filter = 0;
296     params->filter_glob[0] = '\0';
297   }
298
299   /* operator has no setting for this */
300   params->active_file = -1;
301
302   /* initialize the list with previous folders */
303   if (!sfile->folders_prev) {
304     sfile->folders_prev = folderlist_new();
305   }
306
307   if (!sfile->params->dir[0]) {
308     if (blendfile_path[0] != '\0') {
309       BLI_split_dir_part(blendfile_path, sfile->params->dir, sizeof(sfile->params->dir));
310     }
311     else {
312       const char *doc_path = BKE_appdir_folder_default();
313       if (doc_path) {
314         BLI_strncpy(sfile->params->dir, doc_path, sizeof(sfile->params->dir));
315       }
316     }
317   }
318
319   folderlist_pushdir(sfile->folders_prev, sfile->params->dir);
320
321   /* switching thumbnails needs to recalc layout [#28809] */
322   if (sfile->layout) {
323     sfile->layout->dirty = true;
324   }
325
326   return 1;
327 }
328
329 /* The subset of FileSelectParams.flag items we store into preferences. */
330 #define PARAMS_FLAGS_REMEMBERED (FILE_HIDE_DOT | FILE_SORT_INVERT)
331
332 void ED_fileselect_set_params_from_userdef(SpaceFile *sfile)
333 {
334   wmOperator *op = sfile->op;
335   UserDef_FileSpaceData *sfile_udata = &U.file_space_data;
336
337   ED_fileselect_set_params(sfile);
338
339   if (!op) {
340     return;
341   }
342
343   if (!RNA_struct_property_is_set(op->ptr, "display_type")) {
344     sfile->params->display = sfile_udata->display_type;
345   }
346   if (!RNA_struct_property_is_set(op->ptr, "sort_method")) {
347     sfile->params->sort = sfile_udata->sort_type;
348   }
349   sfile->params->thumbnail_size = sfile_udata->thumbnail_size;
350   sfile->params->details_flags = sfile_udata->details_flags;
351
352   /* Combine flags we take from params with the flags we take from userdef. */
353   sfile->params->flag = (sfile->params->flag & ~PARAMS_FLAGS_REMEMBERED) |
354                         (sfile_udata->flag & PARAMS_FLAGS_REMEMBERED);
355 }
356
357 /**
358  * Update the user-preference data for the file space. In fact, this also contains some
359  * non-FileSelectParams data, but we can safely ignore this.
360  *
361  * \param temp_win_size: If the browser was opened in a temporary window,
362  * pass its size here so we can store that in the preferences. Otherwise NULL.
363  */
364 void ED_fileselect_params_to_userdef(SpaceFile *sfile, int temp_win_size[2])
365 {
366   UserDef_FileSpaceData *sfile_udata_new = &U.file_space_data;
367   UserDef_FileSpaceData sfile_udata_old = U.file_space_data;
368
369   sfile_udata_new->display_type = sfile->params->display;
370   sfile_udata_new->thumbnail_size = sfile->params->thumbnail_size;
371   sfile_udata_new->sort_type = sfile->params->sort;
372   sfile_udata_new->details_flags = sfile->params->details_flags;
373   sfile_udata_new->flag = sfile->params->flag & PARAMS_FLAGS_REMEMBERED;
374
375   if (temp_win_size) {
376     sfile_udata_new->temp_win_sizex = temp_win_size[0];
377     sfile_udata_new->temp_win_sizey = temp_win_size[1];
378   }
379
380   /* Tag prefs as dirty if something has changed. */
381   if (memcmp(sfile_udata_new, &sfile_udata_old, sizeof(sfile_udata_old)) != 0) {
382     U.runtime.is_dirty = true;
383   }
384 }
385
386 void ED_fileselect_reset_params(SpaceFile *sfile)
387 {
388   sfile->params->type = FILE_UNIX;
389   sfile->params->flag = 0;
390   sfile->params->title[0] = '\0';
391   sfile->params->active_file = -1;
392 }
393
394 /**
395  * Sets FileSelectParams->file (name of selected file)
396  */
397 void fileselect_file_set(SpaceFile *sfile, const int index)
398 {
399   const struct FileDirEntry *file = filelist_file(sfile->files, index);
400   if (file && file->relpath && file->relpath[0] && !(file->typeflag & FILE_TYPE_DIR)) {
401     BLI_strncpy(sfile->params->file, file->relpath, FILE_MAXFILE);
402   }
403 }
404
405 int ED_fileselect_layout_numfiles(FileLayout *layout, ARegion *ar)
406 {
407   int numfiles;
408
409   /* Values in pixels.
410    *
411    * - *_item: size of each (row|col), (including padding)
412    * - *_view: (x|y) size of the view.
413    * - *_over: extra pixels, to take into account, when the fit isnt exact
414    *   (needed since you may see the end of the previous column and the beginning of the next).
415    *
416    * Could be more clever and take scrolling into account,
417    * but for now don't bother.
418    */
419   if (layout->flag & FILE_LAYOUT_HOR) {
420     const int x_item = layout->tile_w + (2 * layout->tile_border_x);
421     const int x_view = (int)(BLI_rctf_size_x(&ar->v2d.cur));
422     const int x_over = x_item - (x_view % x_item);
423     numfiles = (int)((float)(x_view + x_over) / (float)(x_item));
424     return numfiles * layout->rows;
425   }
426   else {
427     const int y_item = layout->tile_h + (2 * layout->tile_border_y);
428     const int y_view = (int)(BLI_rctf_size_y(&ar->v2d.cur)) - layout->offset_top;
429     const int y_over = y_item - (y_view % y_item);
430     numfiles = (int)((float)(y_view + y_over) / (float)(y_item));
431     return numfiles * layout->flow_columns;
432   }
433 }
434
435 static bool is_inside(int x, int y, int cols, int rows)
436 {
437   return ((x >= 0) && (x < cols) && (y >= 0) && (y < rows));
438 }
439
440 FileSelection ED_fileselect_layout_offset_rect(FileLayout *layout, const rcti *rect)
441 {
442   int colmin, colmax, rowmin, rowmax;
443   FileSelection sel;
444   sel.first = sel.last = -1;
445
446   if (layout == NULL) {
447     return sel;
448   }
449
450   colmin = (rect->xmin) / (layout->tile_w + 2 * layout->tile_border_x);
451   rowmin = (rect->ymin - layout->offset_top) / (layout->tile_h + 2 * layout->tile_border_y);
452   colmax = (rect->xmax) / (layout->tile_w + 2 * layout->tile_border_x);
453   rowmax = (rect->ymax - layout->offset_top) / (layout->tile_h + 2 * layout->tile_border_y);
454
455   if (is_inside(colmin, rowmin, layout->flow_columns, layout->rows) ||
456       is_inside(colmax, rowmax, layout->flow_columns, layout->rows)) {
457     CLAMP(colmin, 0, layout->flow_columns - 1);
458     CLAMP(rowmin, 0, layout->rows - 1);
459     CLAMP(colmax, 0, layout->flow_columns - 1);
460     CLAMP(rowmax, 0, layout->rows - 1);
461   }
462
463   if ((colmin > layout->flow_columns - 1) || (rowmin > layout->rows - 1)) {
464     sel.first = -1;
465   }
466   else {
467     if (layout->flag & FILE_LAYOUT_HOR) {
468       sel.first = layout->rows * colmin + rowmin;
469     }
470     else {
471       sel.first = colmin + layout->flow_columns * rowmin;
472     }
473   }
474   if ((colmax > layout->flow_columns - 1) || (rowmax > layout->rows - 1)) {
475     sel.last = -1;
476   }
477   else {
478     if (layout->flag & FILE_LAYOUT_HOR) {
479       sel.last = layout->rows * colmax + rowmax;
480     }
481     else {
482       sel.last = colmax + layout->flow_columns * rowmax;
483     }
484   }
485
486   return sel;
487 }
488
489 int ED_fileselect_layout_offset(FileLayout *layout, int x, int y)
490 {
491   int offsetx, offsety;
492   int active_file;
493
494   if (layout == NULL) {
495     return -1;
496   }
497
498   offsetx = (x) / (layout->tile_w + 2 * layout->tile_border_x);
499   offsety = (y - layout->offset_top) / (layout->tile_h + 2 * layout->tile_border_y);
500
501   if (offsetx > layout->flow_columns - 1) {
502     return -1;
503   }
504   if (offsety > layout->rows - 1) {
505     return -1;
506   }
507
508   if (layout->flag & FILE_LAYOUT_HOR) {
509     active_file = layout->rows * offsetx + offsety;
510   }
511   else {
512     active_file = offsetx + layout->flow_columns * offsety;
513   }
514   return active_file;
515 }
516
517 /**
518  * Get the currently visible bounds of the layout in screen space. Matches View2D.mask minus the
519  * top column-header row.
520  */
521 void ED_fileselect_layout_maskrect(const FileLayout *layout, const View2D *v2d, rcti *r_rect)
522 {
523   *r_rect = v2d->mask;
524   r_rect->ymax -= layout->offset_top;
525 }
526
527 bool ED_fileselect_layout_is_inside_pt(const FileLayout *layout, const View2D *v2d, int x, int y)
528 {
529   rcti maskrect;
530   ED_fileselect_layout_maskrect(layout, v2d, &maskrect);
531   return BLI_rcti_isect_pt(&maskrect, x, y);
532 }
533
534 bool ED_fileselect_layout_isect_rect(const FileLayout *layout,
535                                      const View2D *v2d,
536                                      const rcti *rect,
537                                      rcti *r_dst)
538 {
539   rcti maskrect;
540   ED_fileselect_layout_maskrect(layout, v2d, &maskrect);
541   return BLI_rcti_isect(&maskrect, rect, r_dst);
542 }
543
544 void ED_fileselect_layout_tilepos(FileLayout *layout, int tile, int *x, int *y)
545 {
546   if (layout->flag == FILE_LAYOUT_HOR) {
547     *x = layout->tile_border_x +
548          (tile / layout->rows) * (layout->tile_w + 2 * layout->tile_border_x);
549     *y = layout->offset_top + layout->tile_border_y +
550          (tile % layout->rows) * (layout->tile_h + 2 * layout->tile_border_y);
551   }
552   else {
553     *x = layout->tile_border_x +
554          ((tile) % layout->flow_columns) * (layout->tile_w + 2 * layout->tile_border_x);
555     *y = layout->offset_top + layout->tile_border_y +
556          ((tile) / layout->flow_columns) * (layout->tile_h + 2 * layout->tile_border_y);
557   }
558 }
559
560 /**
561  * Check if the region coordinate defined by \a x and \a y are inside the column header.
562  */
563 bool file_attribute_column_header_is_inside(const View2D *v2d,
564                                             const FileLayout *layout,
565                                             int x,
566                                             int y)
567 {
568   rcti header_rect = v2d->mask;
569   header_rect.ymin = header_rect.ymax - layout->attribute_column_header_h;
570   return BLI_rcti_isect_pt(&header_rect, x, y);
571 }
572
573 bool file_attribute_column_type_enabled(const FileSelectParams *params,
574                                         FileAttributeColumnType column)
575 {
576   switch (column) {
577     case COLUMN_NAME:
578       /* Always enabled */
579       return true;
580     case COLUMN_DATETIME:
581       return (params->details_flags & FILE_DETAILS_DATETIME) != 0;
582     case COLUMN_SIZE:
583       return (params->details_flags & FILE_DETAILS_SIZE) != 0;
584     default:
585       return false;
586   }
587 }
588
589 /**
590  * Find the column type at region coordinate given by \a x (y doesn't matter for this).
591  */
592 FileAttributeColumnType file_attribute_column_type_find_isect(const View2D *v2d,
593                                                               const FileSelectParams *params,
594                                                               FileLayout *layout,
595                                                               int x)
596 {
597   float mx, my;
598   int offset_tile;
599
600   UI_view2d_region_to_view(v2d, x, v2d->mask.ymax - layout->offset_top - 1, &mx, &my);
601   offset_tile = ED_fileselect_layout_offset(
602       layout, (int)(v2d->tot.xmin + mx), (int)(v2d->tot.ymax - my));
603   if (offset_tile > -1) {
604     int tile_x, tile_y;
605     int pos_x = 0;
606     int rel_x; /* x relative to the hovered tile */
607
608     ED_fileselect_layout_tilepos(layout, offset_tile, &tile_x, &tile_y);
609     /* Column header drawing doesn't use left tile border, so subtract it. */
610     rel_x = mx - (tile_x - layout->tile_border_x);
611
612     for (FileAttributeColumnType column = 0; column < ATTRIBUTE_COLUMN_MAX; column++) {
613       if (!file_attribute_column_type_enabled(params, column)) {
614         continue;
615       }
616       const int width = layout->attribute_columns[column].width;
617
618       if (IN_RANGE(rel_x, pos_x, pos_x + width)) {
619         return column;
620       }
621
622       pos_x += width;
623     }
624   }
625
626   return COLUMN_NONE;
627 }
628
629 float file_string_width(const char *str)
630 {
631   uiStyle *style = UI_style_get();
632   float width;
633
634   UI_fontstyle_set(&style->widget);
635   if (style->widget.kerning == 1) { /* for BLF_width */
636     BLF_enable(style->widget.uifont_id, BLF_KERNING_DEFAULT);
637   }
638
639   width = BLF_width(style->widget.uifont_id, str, BLF_DRAW_STR_DUMMY_MAX);
640
641   if (style->widget.kerning == 1) {
642     BLF_disable(style->widget.uifont_id, BLF_KERNING_DEFAULT);
643   }
644
645   return width;
646 }
647
648 float file_font_pointsize(void)
649 {
650 #if 0
651   float s;
652   char tmp[2] = "X";
653   uiStyle *style = UI_style_get();
654   UI_fontstyle_set(&style->widget);
655   s = BLF_height(style->widget.uifont_id, tmp);
656   return style->widget.points;
657 #else
658   uiStyle *style = UI_style_get();
659   UI_fontstyle_set(&style->widget);
660   return style->widget.points * UI_DPI_FAC;
661 #endif
662 }
663
664 static void file_attribute_columns_widths(const FileSelectParams *params, FileLayout *layout)
665 {
666   FileAttributeColumn *columns = layout->attribute_columns;
667   const bool small_size = SMALL_SIZE_CHECK(params->thumbnail_size);
668   const int pad = small_size ? 0 : ATTRIBUTE_COLUMN_PADDING * 2;
669
670   for (int i = 0; i < ATTRIBUTE_COLUMN_MAX; i++) {
671     layout->attribute_columns[i].width = 0;
672   }
673
674   /* Biggest possible reasonable values... */
675   columns[COLUMN_DATETIME].width = file_string_width(small_size ? "23/08/89" :
676                                                                   "23 Dec 6789, 23:59") +
677                                    pad;
678   columns[COLUMN_SIZE].width = file_string_width(small_size ? "98.7 M" : "098.7 MiB") + pad;
679   if (params->display == FILE_IMGDISPLAY) {
680     columns[COLUMN_NAME].width = ((float)params->thumbnail_size / 8.0f) * UI_UNIT_X;
681   }
682   /* Name column uses remaining width */
683   else {
684     int remwidth = layout->tile_w;
685     for (FileAttributeColumnType column_type = ATTRIBUTE_COLUMN_MAX - 1; column_type >= 0;
686          column_type--) {
687       if ((column_type == COLUMN_NAME) ||
688           !file_attribute_column_type_enabled(params, column_type)) {
689         continue;
690       }
691       remwidth -= columns[column_type].width;
692     }
693     columns[COLUMN_NAME].width = remwidth;
694   }
695 }
696
697 static void file_attribute_columns_init(const FileSelectParams *params, FileLayout *layout)
698 {
699   file_attribute_columns_widths(params, layout);
700
701   layout->attribute_columns[COLUMN_NAME].name = N_("Name");
702   layout->attribute_columns[COLUMN_NAME].sort_type = FILE_SORT_ALPHA;
703   layout->attribute_columns[COLUMN_NAME].text_align = UI_STYLE_TEXT_LEFT;
704   layout->attribute_columns[COLUMN_DATETIME].name = N_("Date Modified");
705   layout->attribute_columns[COLUMN_DATETIME].sort_type = FILE_SORT_TIME;
706   layout->attribute_columns[COLUMN_DATETIME].text_align = UI_STYLE_TEXT_LEFT;
707   layout->attribute_columns[COLUMN_SIZE].name = N_("Size");
708   layout->attribute_columns[COLUMN_SIZE].sort_type = FILE_SORT_SIZE;
709   layout->attribute_columns[COLUMN_SIZE].text_align = UI_STYLE_TEXT_RIGHT;
710 }
711
712 void ED_fileselect_init_layout(struct SpaceFile *sfile, ARegion *ar)
713 {
714   FileSelectParams *params = ED_fileselect_get_params(sfile);
715   FileLayout *layout = NULL;
716   View2D *v2d = &ar->v2d;
717   int numfiles;
718   int textheight;
719
720   if (sfile->layout == NULL) {
721     sfile->layout = MEM_callocN(sizeof(struct FileLayout), "file_layout");
722     sfile->layout->dirty = true;
723   }
724   else if (sfile->layout->dirty == false) {
725     return;
726   }
727
728   numfiles = filelist_files_ensure(sfile->files);
729   textheight = (int)file_font_pointsize();
730   layout = sfile->layout;
731   layout->textheight = textheight;
732
733   if (params->display == FILE_IMGDISPLAY) {
734     layout->prv_w = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_X;
735     layout->prv_h = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_Y;
736     layout->tile_border_x = 0.3f * UI_UNIT_X;
737     layout->tile_border_y = 0.3f * UI_UNIT_X;
738     layout->prv_border_x = 0.3f * UI_UNIT_X;
739     layout->prv_border_y = 0.3f * UI_UNIT_Y;
740     layout->tile_w = layout->prv_w + 2 * layout->prv_border_x;
741     layout->tile_h = layout->prv_h + 2 * layout->prv_border_y + textheight;
742     layout->width = (int)(BLI_rctf_size_x(&v2d->cur) - 2 * layout->tile_border_x);
743     layout->flow_columns = layout->width / (layout->tile_w + 2 * layout->tile_border_x);
744     layout->attribute_column_header_h = 0;
745     layout->offset_top = 0;
746     if (layout->flow_columns > 0) {
747       layout->rows = numfiles / layout->flow_columns + 1;  // XXX dirty, modulo is zero
748     }
749     else {
750       layout->flow_columns = 1;
751       layout->rows = numfiles + 1;  // XXX dirty, modulo is zero
752     }
753     layout->height = sfile->layout->rows * (layout->tile_h + 2 * layout->tile_border_y) +
754                      layout->tile_border_y * 2 - layout->offset_top;
755     layout->flag = FILE_LAYOUT_VER;
756   }
757   else if (params->display == FILE_VERTICALDISPLAY) {
758     int rowcount;
759
760     layout->prv_w = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_X;
761     layout->prv_h = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_Y;
762     layout->tile_border_x = 0.4f * UI_UNIT_X;
763     layout->tile_border_y = 0.1f * UI_UNIT_Y;
764     layout->tile_h = textheight * 3 / 2;
765     layout->width = (int)(BLI_rctf_size_x(&v2d->cur) - 2 * layout->tile_border_x);
766     layout->tile_w = layout->width;
767     layout->flow_columns = 1;
768     layout->attribute_column_header_h = layout->tile_h * 1.2f + 2 * layout->tile_border_y;
769     layout->offset_top = layout->attribute_column_header_h;
770     rowcount = (int)(BLI_rctf_size_y(&v2d->cur) - layout->offset_top - 2 * layout->tile_border_y) /
771                (layout->tile_h + 2 * layout->tile_border_y);
772     file_attribute_columns_init(params, layout);
773
774     layout->rows = MAX2(rowcount, numfiles);
775     BLI_assert(layout->rows != 0);
776     layout->height = sfile->layout->rows * (layout->tile_h + 2 * layout->tile_border_y) +
777                      layout->tile_border_y * 2 + layout->offset_top;
778     layout->flag = FILE_LAYOUT_VER;
779   }
780   else if (params->display == FILE_HORIZONTALDISPLAY) {
781     layout->prv_w = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_X;
782     layout->prv_h = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_Y;
783     layout->tile_border_x = 0.4f * UI_UNIT_X;
784     layout->tile_border_y = 0.1f * UI_UNIT_Y;
785     layout->tile_h = textheight * 3 / 2;
786     layout->attribute_column_header_h = 0;
787     layout->offset_top = layout->attribute_column_header_h;
788     layout->height = (int)(BLI_rctf_size_y(&v2d->cur) - 2 * layout->tile_border_y);
789     /* Padding by full scrollbar H is too much, can overlap tile border Y. */
790     layout->rows = (layout->height - V2D_SCROLL_HEIGHT + layout->tile_border_y) /
791                    (layout->tile_h + 2 * layout->tile_border_y);
792     layout->tile_w = VERTLIST_MAJORCOLUMN_WIDTH;
793     file_attribute_columns_init(params, layout);
794
795     if (layout->rows > 0) {
796       layout->flow_columns = numfiles / layout->rows + 1;  // XXX dirty, modulo is zero
797     }
798     else {
799       layout->rows = 1;
800       layout->flow_columns = numfiles + 1;  // XXX dirty, modulo is zero
801     }
802     layout->width = sfile->layout->flow_columns * (layout->tile_w + 2 * layout->tile_border_x) +
803                     layout->tile_border_x * 2;
804     layout->flag = FILE_LAYOUT_HOR;
805   }
806   layout->dirty = false;
807 }
808
809 FileLayout *ED_fileselect_get_layout(struct SpaceFile *sfile, ARegion *ar)
810 {
811   if (!sfile->layout) {
812     ED_fileselect_init_layout(sfile, ar);
813   }
814   return sfile->layout;
815 }
816
817 void ED_file_change_dir(bContext *C)
818 {
819   wmWindowManager *wm = CTX_wm_manager(C);
820   SpaceFile *sfile = CTX_wm_space_file(C);
821   ScrArea *sa = CTX_wm_area(C);
822
823   if (sfile->params) {
824     ED_fileselect_clear(wm, sa, sfile);
825
826     /* Clear search string, it is very rare to want to keep that filter while changing dir,
827      * and usually very annoying to keep it actually! */
828     sfile->params->filter_search[0] = '\0';
829     sfile->params->active_file = -1;
830
831     if (!filelist_is_dir(sfile->files, sfile->params->dir)) {
832       BLI_strncpy(sfile->params->dir, filelist_dir(sfile->files), sizeof(sfile->params->dir));
833       /* could return but just refresh the current dir */
834     }
835     filelist_setdir(sfile->files, sfile->params->dir);
836
837     if (folderlist_clear_next(sfile)) {
838       folderlist_free(sfile->folders_next);
839     }
840
841     folderlist_pushdir(sfile->folders_prev, sfile->params->dir);
842
843     file_draw_check(C);
844   }
845 }
846
847 int file_select_match(struct SpaceFile *sfile, const char *pattern, char *matched_file)
848 {
849   int match = 0;
850
851   int i;
852   FileDirEntry *file;
853   int n = filelist_files_ensure(sfile->files);
854
855   /* select any file that matches the pattern, this includes exact match
856    * if the user selects a single file by entering the filename
857    */
858   for (i = 0; i < n; i++) {
859     file = filelist_file(sfile->files, i);
860     /* Do not check whether file is a file or dir here! Causes T44243
861      * (we do accept dirs at this stage). */
862     if (fnmatch(pattern, file->relpath, 0) == 0) {
863       filelist_entry_select_set(sfile->files, file, FILE_SEL_ADD, FILE_SEL_SELECTED, CHECK_ALL);
864       if (!match) {
865         BLI_strncpy(matched_file, file->relpath, FILE_MAX);
866       }
867       match++;
868     }
869   }
870
871   return match;
872 }
873
874 int autocomplete_directory(struct bContext *C, char *str, void *UNUSED(arg_v))
875 {
876   SpaceFile *sfile = CTX_wm_space_file(C);
877   int match = AUTOCOMPLETE_NO_MATCH;
878
879   /* search if str matches the beginning of name */
880   if (str[0] && sfile->files) {
881     char dirname[FILE_MAX];
882
883     DIR *dir;
884     struct dirent *de;
885
886     BLI_split_dir_part(str, dirname, sizeof(dirname));
887
888     dir = opendir(dirname);
889
890     if (dir) {
891       AutoComplete *autocpl = UI_autocomplete_begin(str, FILE_MAX);
892
893       while ((de = readdir(dir)) != NULL) {
894         if (FILENAME_IS_CURRPAR(de->d_name)) {
895           /* pass */
896         }
897         else {
898           char path[FILE_MAX];
899           BLI_stat_t status;
900
901           BLI_join_dirfile(path, sizeof(path), dirname, de->d_name);
902
903           if (BLI_stat(path, &status) == 0) {
904             if (S_ISDIR(status.st_mode)) { /* is subdir */
905               UI_autocomplete_update_name(autocpl, path);
906             }
907           }
908         }
909       }
910       closedir(dir);
911
912       match = UI_autocomplete_end(autocpl, str);
913       if (match == AUTOCOMPLETE_FULL_MATCH) {
914         BLI_add_slash(str);
915       }
916     }
917   }
918
919   return match;
920 }
921
922 int autocomplete_file(struct bContext *C, char *str, void *UNUSED(arg_v))
923 {
924   SpaceFile *sfile = CTX_wm_space_file(C);
925   int match = AUTOCOMPLETE_NO_MATCH;
926
927   /* search if str matches the beginning of name */
928   if (str[0] && sfile->files) {
929     AutoComplete *autocpl = UI_autocomplete_begin(str, FILE_MAX);
930     int nentries = filelist_files_ensure(sfile->files);
931     int i;
932
933     for (i = 0; i < nentries; i++) {
934       FileDirEntry *file = filelist_file(sfile->files, i);
935       UI_autocomplete_update_name(autocpl, file->relpath);
936     }
937     match = UI_autocomplete_end(autocpl, str);
938   }
939
940   return match;
941 }
942
943 void ED_fileselect_clear(wmWindowManager *wm, ScrArea *sa, SpaceFile *sfile)
944 {
945   /* only NULL in rare cases - [#29734] */
946   if (sfile->files) {
947     filelist_readjob_stop(wm, sa);
948     filelist_freelib(sfile->files);
949     filelist_clear(sfile->files);
950   }
951
952   sfile->params->highlight_file = -1;
953   WM_main_add_notifier(NC_SPACE | ND_SPACE_FILE_LIST, NULL);
954 }
955
956 void ED_fileselect_exit(wmWindowManager *wm, ScrArea *sa, SpaceFile *sfile)
957 {
958   if (!sfile) {
959     return;
960   }
961   if (sfile->op) {
962     wmWindow *temp_win = WM_window_is_temp_screen(wm->winactive) ? wm->winactive : NULL;
963     int win_size[2];
964
965     if (temp_win) {
966       /* Get DPI/pixelsize independent size to be stored in preferences. */
967       WM_window_set_dpi(temp_win); /* Ensure the DPI is taken from the right window. */
968       win_size[0] = WM_window_pixels_x(temp_win) / UI_DPI_FAC;
969       win_size[1] = WM_window_pixels_y(temp_win) / UI_DPI_FAC;
970     }
971     ED_fileselect_params_to_userdef(sfile, temp_win ? win_size : NULL);
972
973     WM_event_fileselect_event(wm, sfile->op, EVT_FILESELECT_EXTERNAL_CANCEL);
974     sfile->op = NULL;
975   }
976
977   folderlist_free(sfile->folders_prev);
978   folderlist_free(sfile->folders_next);
979
980   if (sfile->files) {
981     ED_fileselect_clear(wm, sa, sfile);
982     filelist_free(sfile->files);
983     MEM_freeN(sfile->files);
984     sfile->files = NULL;
985   }
986 }
987
988 /**
989  * Helper used by both main update code, and smooth-scroll timer,
990  * to try to enable rename editing from #FileSelectParams.renamefile name.
991  */
992 void file_params_renamefile_activate(SpaceFile *sfile, FileSelectParams *params)
993 {
994   BLI_assert(params->rename_flag != 0);
995
996   if ((params->rename_flag & (FILE_PARAMS_RENAME_ACTIVE | FILE_PARAMS_RENAME_POSTSCROLL_ACTIVE)) !=
997       0) {
998     return;
999   }
1000
1001   BLI_assert(params->renamefile[0] != '\0');
1002
1003   const int idx = filelist_file_findpath(sfile->files, params->renamefile);
1004   if (idx >= 0) {
1005     FileDirEntry *file = filelist_file(sfile->files, idx);
1006     BLI_assert(file != NULL);
1007
1008     if ((params->rename_flag & FILE_PARAMS_RENAME_PENDING) != 0) {
1009       filelist_entry_select_set(sfile->files, file, FILE_SEL_ADD, FILE_SEL_EDITING, CHECK_ALL);
1010       params->rename_flag = FILE_PARAMS_RENAME_ACTIVE;
1011     }
1012     else if ((params->rename_flag & FILE_PARAMS_RENAME_POSTSCROLL_PENDING) != 0) {
1013       filelist_entry_select_set(sfile->files, file, FILE_SEL_ADD, FILE_SEL_HIGHLIGHTED, CHECK_ALL);
1014       params->renamefile[0] = '\0';
1015       params->rename_flag = FILE_PARAMS_RENAME_POSTSCROLL_ACTIVE;
1016     }
1017   }
1018   /* File listing is now async, only reset renaming if matching entry is not found
1019    * when file listing is not done. */
1020   else if (filelist_is_ready(sfile->files)) {
1021     params->renamefile[0] = '\0';
1022     params->rename_flag = 0;
1023   }
1024 }