/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2008 Blender Foundation. All rights reserved. */ /** \file * \ingroup spfile */ #include "BLI_utildefines.h" #include "BLI_blenlib.h" #include "BLI_linklist.h" #include "BLI_math.h" #include "BLO_readfile.h" #include "BKE_appdir.h" #include "BKE_context.h" #include "BKE_global.h" #include "BKE_main.h" #include "BKE_report.h" #include "BKE_screen.h" #ifdef WIN32 # include "BLI_winstuff.h" #endif #include "ED_asset.h" #include "ED_fileselect.h" #include "ED_screen.h" #include "ED_select_utils.h" #include "UI_interface.h" #include "UI_interface_icons.h" #include "UI_resources.h" #include "MEM_guardedalloc.h" #include "RNA_access.h" #include "RNA_define.h" #include "UI_view2d.h" #include "WM_api.h" #include "WM_types.h" #include "file_intern.h" #include "filelist.h" #include "fsmenu.h" #include #include #include #include #include /* -------------------------------------------------------------------- */ /** \name File Selection Utilities * \{ */ static FileSelection find_file_mouse_rect(SpaceFile *sfile, ARegion *region, const rcti *rect_region) { FileSelection sel; View2D *v2d = ®ion->v2d; rcti rect_view; rctf rect_view_fl; rctf rect_region_fl; BLI_rctf_rcti_copy(&rect_region_fl, rect_region); /* Okay, manipulating v2d rects here is hacky... */ v2d->mask.ymax -= sfile->layout->offset_top; v2d->cur.ymax -= sfile->layout->offset_top; UI_view2d_region_to_view_rctf(v2d, &rect_region_fl, &rect_view_fl); v2d->mask.ymax += sfile->layout->offset_top; v2d->cur.ymax += sfile->layout->offset_top; BLI_rcti_init(&rect_view, (int)(v2d->tot.xmin + rect_view_fl.xmin), (int)(v2d->tot.xmin + rect_view_fl.xmax), (int)(v2d->tot.ymax - rect_view_fl.ymin), (int)(v2d->tot.ymax - rect_view_fl.ymax)); sel = ED_fileselect_layout_offset_rect(sfile->layout, &rect_view); return sel; } typedef enum FileSelect { FILE_SELECT_NOTHING = 0, FILE_SELECT_DIR = 1, FILE_SELECT_FILE = 2, } FileSelect; static void clamp_to_filelist(int numfiles, FileSelection *sel) { /* box select before the first file */ if ((sel->first < 0) && (sel->last >= 0)) { sel->first = 0; } /* don't select if everything is outside filelist */ if ((sel->first >= numfiles) && ((sel->last < 0) || (sel->last >= numfiles))) { sel->first = -1; sel->last = -1; } /* fix if last file invalid */ if ((sel->first > 0) && (sel->last < 0)) { sel->last = numfiles - 1; } /* clamp */ if (sel->first >= numfiles) { sel->first = numfiles - 1; } if (sel->last >= numfiles) { sel->last = numfiles - 1; } } static FileSelection file_selection_get(bContext *C, const rcti *rect, bool fill) { ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); int numfiles = filelist_files_ensure(sfile->files); FileSelection sel; sel = find_file_mouse_rect(sfile, region, rect); if (!((sel.first == -1) && (sel.last == -1))) { clamp_to_filelist(numfiles, &sel); } /* if desired, fill the selection up from the last selected file to the current one */ if (fill && (sel.last >= 0) && (sel.last < numfiles)) { int f; /* Try to find a smaller-index selected item. */ for (f = sel.last; f >= 0; f--) { if (filelist_entry_select_index_get(sfile->files, f, CHECK_ALL)) { break; } } if (f >= 0) { sel.first = f + 1; } /* If none found, try to find a higher-index selected item. */ else { for (f = sel.first; f < numfiles; f++) { if (filelist_entry_select_index_get(sfile->files, f, CHECK_ALL)) { break; } } if (f < numfiles) { sel.last = f - 1; } } } return sel; } static FileSelect file_select_do(bContext *C, int selected_idx, bool do_diropen) { Main *bmain = CTX_data_main(C); FileSelect retval = FILE_SELECT_NOTHING; SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); int numfiles = filelist_files_ensure(sfile->files); const FileDirEntry *file; /* make the selected file active */ if ((selected_idx >= 0) && (selected_idx < numfiles) && (file = filelist_file(sfile->files, selected_idx))) { params->highlight_file = selected_idx; params->active_file = selected_idx; if (file->typeflag & FILE_TYPE_DIR) { const bool is_parent_dir = FILENAME_IS_PARENT(file->relpath); if (do_diropen == false) { retval = FILE_SELECT_DIR; } /* the path is too long and we are not going up! */ else if (!is_parent_dir && strlen(params->dir) + strlen(file->relpath) >= FILE_MAX) { // XXX error("Path too long, cannot enter this directory"); } else { if (is_parent_dir) { /* avoids /../../ */ BLI_path_parent_dir(params->dir); if (params->recursion_level > 1) { /* Disable 'dirtree' recursion when going up in tree. */ params->recursion_level = 0; filelist_setrecursion(sfile->files, params->recursion_level); } } else if (file->redirection_path) { BLI_strncpy(params->dir, file->redirection_path, sizeof(params->dir)); BLI_path_normalize_dir(BKE_main_blendfile_path(bmain), params->dir, sizeof(params->dir)); BLI_path_slash_ensure(params->dir, sizeof(params->dir)); } else { BLI_path_normalize_dir(BKE_main_blendfile_path(bmain), params->dir, sizeof(params->dir)); BLI_path_append_dir(params->dir, sizeof(params->dir), file->relpath); } ED_file_change_dir(C); retval = FILE_SELECT_DIR; } } else { retval = FILE_SELECT_FILE; } fileselect_file_set(C, sfile, selected_idx); } return retval; } /** * \warning Loops over all files so better use cautiously. */ static bool file_is_any_selected(struct FileList *files) { const int numfiles = filelist_files_ensure(files); int i; /* Is any file selected ? */ for (i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(files, i, CHECK_ALL)) { return true; } } return false; } static FileSelection file_current_selection_range_get(struct FileList *files) { const int numfiles = filelist_files_ensure(files); FileSelection selection = {-1, -1}; /* Iterate over the files once but in two loops, one to find the first selected file, and the * other to find the last. */ int file_index; for (file_index = 0; file_index < numfiles; file_index++) { if (filelist_entry_is_selected(files, file_index)) { /* First selected entry found. */ selection.first = file_index; break; } } for (; file_index < numfiles; file_index++) { if (filelist_entry_is_selected(files, file_index)) { selection.last = file_index; /* Keep looping, we may find more selected files. */ } } return selection; } /** * If \a file is outside viewbounds, this adjusts view to make sure it's inside */ static void file_ensure_inside_viewbounds(ARegion *region, SpaceFile *sfile, const int file) { FileLayout *layout = ED_fileselect_get_layout(sfile, region); rctf *cur = ®ion->v2d.cur; rcti rect; bool changed = true; file_tile_boundbox(region, layout, file, &rect); /* down - also use if tile is higher than viewbounds so view is aligned to file name */ if (cur->ymin > rect.ymin || layout->tile_h > region->winy) { cur->ymin = rect.ymin - (2 * layout->tile_border_y); cur->ymax = cur->ymin + region->winy; } /* up */ else if ((cur->ymax - layout->offset_top) < rect.ymax) { cur->ymax = rect.ymax + layout->tile_border_y + layout->offset_top; cur->ymin = cur->ymax - region->winy; } /* left - also use if tile is wider than viewbounds so view is aligned to file name */ else if (cur->xmin > rect.xmin || layout->tile_w > region->winx) { cur->xmin = rect.xmin - layout->tile_border_x; cur->xmax = cur->xmin + region->winx; } /* right */ else if (cur->xmax < rect.xmax) { cur->xmax = rect.xmax + (2 * layout->tile_border_x); cur->xmin = cur->xmax - region->winx; } else { BLI_assert(cur->xmin <= rect.xmin && cur->xmax >= rect.xmax && cur->ymin <= rect.ymin && (cur->ymax - layout->offset_top) >= rect.ymax); changed = false; } if (changed) { UI_view2d_curRect_validate(®ion->v2d); } } static void file_ensure_selection_inside_viewbounds(ARegion *region, SpaceFile *sfile, FileSelection *sel) { const FileLayout *layout = ED_fileselect_get_layout(sfile, region); if (((layout->flag & FILE_LAYOUT_HOR) && region->winx <= (1.2f * layout->tile_w)) && ((layout->flag & FILE_LAYOUT_VER) && region->winy <= (2.0f * layout->tile_h))) { return; } /* Adjust view to display selection. Doing iterations for first and last * selected item makes view showing as much of the selection possible. * Not really useful if tiles are (almost) bigger than viewbounds though. */ file_ensure_inside_viewbounds(region, sfile, sel->last); file_ensure_inside_viewbounds(region, sfile, sel->first); } static FileSelect file_select( bContext *C, const rcti *rect, FileSelType select, bool fill, bool do_diropen) { SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); FileSelect retval = FILE_SELECT_NOTHING; FileSelection sel = file_selection_get(C, rect, fill); /* get the selection */ const FileCheckType check_type = (params->flag & FILE_DIRSEL_ONLY) ? CHECK_DIRS : CHECK_ALL; /* flag the files as selected in the filelist */ filelist_entries_select_index_range_set( sfile->files, &sel, select, FILE_SEL_SELECTED, check_type); /* Don't act on multiple selected files */ if (sel.first != sel.last) { select = 0; } /* Do we have a valid selection and are we actually selecting */ if ((sel.last >= 0) && (select != FILE_SEL_REMOVE)) { /* Check last selection, if selected, act on the file or dir */ if (filelist_entry_select_index_get(sfile->files, sel.last, check_type)) { retval = file_select_do(C, sel.last, do_diropen); } } if (select != FILE_SEL_ADD && !file_is_any_selected(sfile->files)) { params->active_file = -1; } else if (sel.last >= 0) { ARegion *region = CTX_wm_region(C); file_ensure_selection_inside_viewbounds(region, sfile, &sel); } /* update operator for name change event */ file_draw_check(C); return retval; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Bookmark Utilities * \{ */ /** * Local utility to write #BLENDER_BOOKMARK_FILE, reporting an error on failure. */ static bool fsmenu_write_file_and_refresh_or_report_error(struct FSMenu *fsmenu, ScrArea *area, ReportList *reports) { /* NOTE: use warning instead of error here, because the bookmark operation may be part of * other actions which should not cause the operator to fail entirely. */ const char *cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, NULL); if (UNLIKELY(!cfgdir)) { BKE_report(reports, RPT_ERROR, "Unable to create configuration directory to write bookmarks"); return false; } char filepath[FILE_MAX]; BLI_path_join(filepath, sizeof(filepath), cfgdir, BLENDER_BOOKMARK_FILE); if (UNLIKELY(!fsmenu_write_file(fsmenu, filepath))) { BKE_reportf(reports, RPT_ERROR, "Unable to open or write bookmark file \"%s\"", filepath); return false; } ED_area_tag_refresh(area); ED_area_tag_redraw(area); return true; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Box Select Operator * \{ */ static int file_box_select_find_last_selected(SpaceFile *sfile, ARegion *region, const FileSelection *sel, const int mouse_xy[2]) { FileLayout *layout = ED_fileselect_get_layout(sfile, region); rcti bounds_first, bounds_last; int dist_first, dist_last; float mouseco_view[2]; UI_view2d_region_to_view(®ion->v2d, UNPACK2(mouse_xy), &mouseco_view[0], &mouseco_view[1]); file_tile_boundbox(region, layout, sel->first, &bounds_first); file_tile_boundbox(region, layout, sel->last, &bounds_last); /* are first and last in the same column (horizontal layout)/row (vertical layout)? */ if ((layout->flag & FILE_LAYOUT_HOR && bounds_first.xmin == bounds_last.xmin) || (layout->flag & FILE_LAYOUT_VER && bounds_first.ymin != bounds_last.ymin)) { /* use vertical distance */ const int my_loc = (int)mouseco_view[1]; dist_first = BLI_rcti_length_y(&bounds_first, my_loc); dist_last = BLI_rcti_length_y(&bounds_last, my_loc); } else { /* use horizontal distance */ const int mx_loc = (int)mouseco_view[0]; dist_first = BLI_rcti_length_x(&bounds_first, mx_loc); dist_last = BLI_rcti_length_x(&bounds_last, mx_loc); } return (dist_first < dist_last) ? sel->first : sel->last; } static int file_box_select_modal(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); FileSelection sel; rcti rect; int result; result = WM_gesture_box_modal(C, op, event); if (result == OPERATOR_RUNNING_MODAL) { WM_operator_properties_border_to_rcti(op, &rect); ED_fileselect_layout_isect_rect(sfile->layout, ®ion->v2d, &rect, &rect); sel = file_selection_get(C, &rect, 0); if ((sel.first != params->sel_first) || (sel.last != params->sel_last)) { int idx; file_select_deselect_all(sfile, FILE_SEL_HIGHLIGHTED); filelist_entries_select_index_range_set( sfile->files, &sel, FILE_SEL_ADD, FILE_SEL_HIGHLIGHTED, CHECK_ALL); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); for (idx = sel.last; idx >= 0; idx--) { const FileDirEntry *file = filelist_file(sfile->files, idx); /* Don't highlight read-only file (".." or ".") on box select. */ if (FILENAME_IS_CURRPAR(file->relpath)) { filelist_entry_select_set( sfile->files, file, FILE_SEL_REMOVE, FILE_SEL_HIGHLIGHTED, CHECK_ALL); } /* make sure highlight_file is no readonly file */ if (sel.last == idx) { params->highlight_file = idx; } } } params->sel_first = sel.first; params->sel_last = sel.last; params->active_file = file_box_select_find_last_selected(sfile, region, &sel, event->mval); } else { params->highlight_file = -1; params->sel_first = params->sel_last = -1; fileselect_file_set(C, sfile, params->active_file); file_select_deselect_all(sfile, FILE_SEL_HIGHLIGHTED); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); } return result; } static int file_box_select_exec(bContext *C, wmOperator *op) { ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); rcti rect; FileSelect ret; WM_operator_properties_border_to_rcti(op, &rect); const eSelectOp sel_op = RNA_enum_get(op->ptr, "mode"); const bool select = (sel_op != SEL_OP_SUB); if (SEL_OP_USE_PRE_DESELECT(sel_op)) { file_select_deselect_all(sfile, FILE_SEL_SELECTED); } ED_fileselect_layout_isect_rect(sfile->layout, ®ion->v2d, &rect, &rect); ret = file_select(C, &rect, select ? FILE_SEL_ADD : FILE_SEL_REMOVE, false, false); /* unselect '..' parent entry - it's not supposed to be selected if more than * one file is selected */ filelist_entry_parent_select_set(sfile->files, FILE_SEL_REMOVE, FILE_SEL_SELECTED, CHECK_ALL); if (FILE_SELECT_DIR == ret) { WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); } else if (FILE_SELECT_FILE == ret) { WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); } return OPERATOR_FINISHED; } void FILE_OT_select_box(wmOperatorType *ot) { /* identifiers */ ot->name = "Box Select"; ot->description = "Activate/select the file(s) contained in the border"; ot->idname = "FILE_OT_select_box"; /* api callbacks */ ot->invoke = WM_gesture_box_invoke; ot->exec = file_box_select_exec; ot->modal = file_box_select_modal; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; ot->cancel = WM_gesture_box_cancel; /* properties */ WM_operator_properties_gesture_box(ot); WM_operator_properties_select_operation_simple(ot); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Select Pick Operator * \{ */ static rcti file_select_mval_to_select_rect(const int mval[2]) { rcti rect; rect.xmin = rect.xmax = mval[0]; rect.ymin = rect.ymax = mval[1]; return rect; } static int file_select_exec(bContext *C, wmOperator *op) { ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelect ret; rcti rect; const bool extend = RNA_boolean_get(op->ptr, "extend"); const bool fill = RNA_boolean_get(op->ptr, "fill"); const bool do_diropen = RNA_boolean_get(op->ptr, "open"); const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all"); const bool only_activate_if_selected = RNA_boolean_get(op->ptr, "only_activate_if_selected"); /* Used so right mouse clicks can do both, activate and spawn the context menu. */ const bool pass_through = RNA_boolean_get(op->ptr, "pass_through"); bool wait_to_deselect_others = RNA_boolean_get(op->ptr, "wait_to_deselect_others"); if (region->regiontype != RGN_TYPE_WINDOW) { return OPERATOR_CANCELLED; } int mval[2]; mval[0] = RNA_int_get(op->ptr, "mouse_x"); mval[1] = RNA_int_get(op->ptr, "mouse_y"); rect = file_select_mval_to_select_rect(mval); if (!ED_fileselect_layout_is_inside_pt(sfile->layout, ®ion->v2d, rect.xmin, rect.ymin)) { return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; } if (extend || fill) { wait_to_deselect_others = false; } int ret_val = OPERATOR_FINISHED; const FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (sfile && params) { int idx = params->highlight_file; int numfiles = filelist_files_ensure(sfile->files); if ((idx >= 0) && (idx < numfiles)) { const bool is_selected = filelist_entry_select_index_get(sfile->files, idx, CHECK_ALL) & FILE_SEL_SELECTED; if (only_activate_if_selected && is_selected) { /* Don't deselect other items. */ } else if (wait_to_deselect_others && is_selected) { ret_val = OPERATOR_RUNNING_MODAL; } /* single select, deselect all selected first */ else if (!extend) { file_select_deselect_all(sfile, FILE_SEL_SELECTED); } } } ret = file_select(C, &rect, extend ? FILE_SEL_TOGGLE : FILE_SEL_ADD, fill, do_diropen); if (extend) { /* unselect '..' parent entry - it's not supposed to be selected if more * than one file is selected */ filelist_entry_parent_select_set(sfile->files, FILE_SEL_REMOVE, FILE_SEL_SELECTED, CHECK_ALL); } if (ret == FILE_SELECT_NOTHING) { if (deselect_all) { file_select_deselect_all(sfile, FILE_SEL_SELECTED); } } else if (ret == FILE_SELECT_DIR) { WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); } else if (ret == FILE_SELECT_FILE) { WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); } WM_event_add_mousemove(CTX_wm_window(C)); /* for directory changes */ WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); if ((ret_val == OPERATOR_FINISHED) && pass_through) { ret_val |= OPERATOR_PASS_THROUGH; } return ret_val; } void FILE_OT_select(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Select"; ot->idname = "FILE_OT_select"; ot->description = "Handle mouse clicks to select and activate items"; /* api callbacks */ ot->invoke = WM_generic_select_invoke; ot->exec = file_select_exec; ot->modal = WM_generic_select_modal; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; /* properties */ WM_operator_properties_generic_select(ot); prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend selection instead of deselecting everything first"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean( ot->srna, "fill", false, "Fill", "Select everything beginning with the last selection"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "open", true, "Open", "Open a directory when selecting it"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "deselect_all", false, "Deselect On Nothing", "Deselect all when nothing under the cursor"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "only_activate_if_selected", false, "Only Activate if Selected", "Do not change selection if the item under the cursor is already " "selected, only activate it"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "pass_through", false, "Pass Through", "Even on successful execution, pass the event on so other operators can " "execute on it as well"); RNA_def_property_flag(prop, PROP_SKIP_SAVE | PROP_HIDDEN); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Select Walk Operator * \{ */ /** * \returns true if selection has changed */ static bool file_walk_select_selection_set(struct bContext *C, wmWindow *win, ARegion *region, SpaceFile *sfile, const int direction, const int numfiles, const int active_old, const int active_new, const int other_site, const bool has_selection, const bool extend, const bool fill) { FileSelectParams *params = ED_fileselect_get_active_params(sfile); struct FileList *files = sfile->files; const int last_sel = params->active_file; /* store old value */ int active = active_old; /* could use active_old instead, just for readability */ bool deselect = false; BLI_assert(params); if (numfiles == 0) { /* No files visible, nothing to do. */ return false; } if (has_selection) { if (extend && filelist_entry_select_index_get(files, active_old, CHECK_ALL) && filelist_entry_select_index_get(files, active_new, CHECK_ALL)) { /* conditions for deselecting: initial file is selected, new file is * selected and either other_side isn't selected/found or we use fill */ deselect = (fill || other_site == -1 || !filelist_entry_select_index_get(files, other_site, CHECK_ALL)); /* don't change highlight_file here since we either want to deselect active or we want * to walk through a block of selected files without selecting/deselecting anything */ params->active_file = active_new; /* but we want to change active if we use fill * (needed to get correct selection bounds) */ if (deselect && fill) { active = active_new; } } else { /* regular selection change */ params->active_file = active = active_new; } } else { /* select last file */ if (ELEM(direction, UI_SELECT_WALK_UP, UI_SELECT_WALK_LEFT)) { params->active_file = active = numfiles - 1; } /* select first file */ else if (ELEM(direction, UI_SELECT_WALK_DOWN, UI_SELECT_WALK_RIGHT)) { params->active_file = active = 0; } else { BLI_assert(0); } } if (active < 0) { return false; } if (extend) { /* highlight the active walker file for extended selection for better visual feedback */ params->highlight_file = params->active_file; /* unselect '..' parent entry - it's not supposed to be selected if more * than one file is selected */ filelist_entry_parent_select_set(files, FILE_SEL_REMOVE, FILE_SEL_SELECTED, CHECK_ALL); } else { /* deselect all first */ file_select_deselect_all(sfile, FILE_SEL_SELECTED); /* highlight file under mouse pos */ params->highlight_file = -1; WM_event_add_mousemove(win); } /* do the actual selection */ if (fill) { FileSelection sel = {MIN2(active, last_sel), MAX2(active, last_sel)}; /* fill selection between last and first selected file */ filelist_entries_select_index_range_set( files, &sel, deselect ? FILE_SEL_REMOVE : FILE_SEL_ADD, FILE_SEL_SELECTED, CHECK_ALL); /* entire sel is cleared here, so select active again */ if (deselect) { filelist_entry_select_index_set(files, active, FILE_SEL_ADD, FILE_SEL_SELECTED, CHECK_ALL); } /* unselect '..' parent entry - it's not supposed to be selected if more * than one file is selected */ if ((sel.last - sel.first) > 1) { filelist_entry_parent_select_set(files, FILE_SEL_REMOVE, FILE_SEL_SELECTED, CHECK_ALL); } } else { filelist_entry_select_index_set( files, active, deselect ? FILE_SEL_REMOVE : FILE_SEL_ADD, FILE_SEL_SELECTED, CHECK_ALL); } BLI_assert(IN_RANGE(active, -1, numfiles)); fileselect_file_set(C, sfile, params->active_file); /* ensure newly selected file is inside viewbounds */ file_ensure_inside_viewbounds(region, sfile, params->active_file); /* selection changed */ return true; } /** * \returns true if selection has changed */ static bool file_walk_select_do(bContext *C, SpaceFile *sfile, FileSelectParams *params, const int direction, const bool extend, const bool fill) { wmWindow *win = CTX_wm_window(C); ARegion *region = CTX_wm_region(C); struct FileList *files = sfile->files; const int numfiles = filelist_files_ensure(files); const bool has_selection = file_is_any_selected(files); const int active_old = params->active_file; int active_new = -1; int other_site = -1; /* file on the other site of active_old */ /* *** get all needed files for handling selection *** */ if (numfiles == 0) { /* No files visible, nothing to do. */ return false; } if (has_selection) { FileLayout *layout = ED_fileselect_get_layout(sfile, region); const int idx_shift = (layout->flag & FILE_LAYOUT_HOR) ? layout->rows : layout->flow_columns; if ((layout->flag & FILE_LAYOUT_HOR && direction == UI_SELECT_WALK_UP) || (layout->flag & FILE_LAYOUT_VER && direction == UI_SELECT_WALK_LEFT)) { active_new = active_old - 1; other_site = active_old + 1; } else if ((layout->flag & FILE_LAYOUT_HOR && direction == UI_SELECT_WALK_DOWN) || (layout->flag & FILE_LAYOUT_VER && direction == UI_SELECT_WALK_RIGHT)) { active_new = active_old + 1; other_site = active_old - 1; } else if ((layout->flag & FILE_LAYOUT_HOR && direction == UI_SELECT_WALK_LEFT) || (layout->flag & FILE_LAYOUT_VER && direction == UI_SELECT_WALK_UP)) { active_new = active_old - idx_shift; other_site = active_old + idx_shift; } else if ((layout->flag & FILE_LAYOUT_HOR && direction == UI_SELECT_WALK_RIGHT) || (layout->flag & FILE_LAYOUT_VER && direction == UI_SELECT_WALK_DOWN)) { active_new = active_old + idx_shift; other_site = active_old - idx_shift; } else { BLI_assert(0); } if (!IN_RANGE(active_new, -1, numfiles)) { if (extend) { /* extend to invalid file -> abort */ return false; } /* if we don't extend, selecting '..' (index == 0) is allowed so * using key selection to go to parent directory is possible */ if (active_new != 0) { /* select initial file */ active_new = active_old; } } if (!IN_RANGE(other_site, 0, numfiles)) { other_site = -1; } } return file_walk_select_selection_set(C, win, region, sfile, direction, numfiles, active_old, active_new, other_site, has_selection, extend, fill); } static int file_walk_select_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) { SpaceFile *sfile = (SpaceFile *)CTX_wm_space_data(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); const int direction = RNA_enum_get(op->ptr, "direction"); const bool extend = RNA_boolean_get(op->ptr, "extend"); const bool fill = RNA_boolean_get(op->ptr, "fill"); if (file_walk_select_do(C, sfile, params, direction, extend, fill)) { WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); return OPERATOR_FINISHED; } return OPERATOR_CANCELLED; } void FILE_OT_select_walk(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Walk Select/Deselect File"; ot->description = "Select/Deselect files by walking through them"; ot->idname = "FILE_OT_select_walk"; /* api callbacks */ ot->invoke = file_walk_select_invoke; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; /* properties */ WM_operator_properties_select_walk_direction(ot); prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend selection instead of deselecting everything first"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean( ot->srna, "fill", false, "Fill", "Select everything beginning with the last selection"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Select All Operator * \{ */ static int file_select_all_exec(bContext *C, wmOperator *op) { ScrArea *area = CTX_wm_area(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); FileSelection sel; const int numfiles = filelist_files_ensure(sfile->files); int action = RNA_enum_get(op->ptr, "action"); if (action == SEL_TOGGLE) { action = file_is_any_selected(sfile->files) ? SEL_DESELECT : SEL_SELECT; } sel.first = 0; sel.last = numfiles - 1; FileCheckType check_type; FileSelType filesel_type; switch (action) { case SEL_SELECT: case SEL_INVERT: { check_type = (params->flag & FILE_DIRSEL_ONLY) ? CHECK_DIRS : CHECK_FILES; filesel_type = (action == SEL_INVERT) ? FILE_SEL_TOGGLE : FILE_SEL_ADD; break; } case SEL_DESELECT: { check_type = CHECK_ALL; filesel_type = FILE_SEL_REMOVE; break; } default: { BLI_assert(0); return OPERATOR_CANCELLED; } } filelist_entries_select_index_range_set( sfile->files, &sel, filesel_type, FILE_SEL_SELECTED, check_type); params->active_file = -1; if (action != SEL_DESELECT) { for (int i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(sfile->files, i, check_type)) { params->active_file = i; break; } } } file_draw_check(C); WM_event_add_mousemove(CTX_wm_window(C)); ED_area_tag_redraw(area); return OPERATOR_FINISHED; } void FILE_OT_select_all(wmOperatorType *ot) { /* identifiers */ ot->name = "(De)select All Files"; ot->description = "Select or deselect all files"; ot->idname = "FILE_OT_select_all"; /* api callbacks */ ot->exec = file_select_all_exec; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; /* properties */ WM_operator_properties_select_all(ot); } /** \} */ /* -------------------------------------------------------------------- */ /** \name View Selected Operator * \{ */ static int file_view_selected_exec(bContext *C, wmOperator *UNUSED(op)) { SpaceFile *sfile = CTX_wm_space_file(C); FileSelection sel = file_current_selection_range_get(sfile->files); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (sel.first == -1 && sel.last == -1 && params->active_file == -1) { /* Nothing was selected. */ return OPERATOR_CANCELLED; } /* Extend the selection area with the active file, as it may not be selected but still is * important to have in view. */ if (sel.first == -1 || params->active_file < sel.first) { sel.first = params->active_file; } if (sel.last == -1 || params->active_file > sel.last) { sel.last = params->active_file; } ScrArea *area = CTX_wm_area(C); ARegion *region = CTX_wm_region(C); file_ensure_selection_inside_viewbounds(region, sfile, &sel); file_draw_check(C); WM_event_add_mousemove(CTX_wm_window(C)); ED_area_tag_redraw(area); return OPERATOR_FINISHED; } void FILE_OT_view_selected(wmOperatorType *ot) { /* identifiers */ ot->name = "Frame Selected"; ot->description = "Scroll the selected files into view"; ot->idname = "FILE_OT_view_selected"; /* api callbacks */ ot->exec = file_view_selected_exec; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Select Bookmark Operator * \{ */ /* Note we could get rid of this one, but it's used by some addon so... * Does not hurt keeping it around for now. */ static int bookmark_select_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); SpaceFile *sfile = CTX_wm_space_file(C); PropertyRNA *prop = RNA_struct_find_property(op->ptr, "dir"); FileSelectParams *params = ED_fileselect_get_active_params(sfile); char entry[256]; RNA_property_string_get(op->ptr, prop, entry); BLI_strncpy(params->dir, entry, sizeof(params->dir)); BLI_path_normalize_dir(BKE_main_blendfile_path(bmain), params->dir, sizeof(params->dir)); ED_file_change_dir(C); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); return OPERATOR_FINISHED; } void FILE_OT_select_bookmark(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Select Directory"; ot->description = "Select a bookmarked directory"; ot->idname = "FILE_OT_select_bookmark"; /* api callbacks */ ot->exec = bookmark_select_exec; /* Bookmarks are for file browsing only (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* properties */ prop = RNA_def_string(ot->srna, "dir", NULL, FILE_MAXDIR, "Directory", ""); RNA_def_property_flag(prop, PROP_SKIP_SAVE); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Add Bookmark Operator * \{ */ static int bookmark_add_exec(bContext *C, wmOperator *op) { ScrArea *area = CTX_wm_area(C); SpaceFile *sfile = CTX_wm_space_file(C); struct FSMenu *fsmenu = ED_fsmenu_get(); struct FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params->dir[0] != '\0') { fsmenu_insert_entry( fsmenu, FS_CATEGORY_BOOKMARKS, params->dir, NULL, ICON_FILE_FOLDER, FS_INSERT_SAVE); fsmenu_write_file_and_refresh_or_report_error(fsmenu, area, op->reports); } return OPERATOR_FINISHED; } void FILE_OT_bookmark_add(wmOperatorType *ot) { /* identifiers */ ot->name = "Add Bookmark"; ot->description = "Add a bookmark for the selected/active directory"; ot->idname = "FILE_OT_bookmark_add"; /* api callbacks */ ot->exec = bookmark_add_exec; /* Bookmarks are for file browsing only (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Delete Bookmark Operator * \{ */ static int bookmark_delete_exec(bContext *C, wmOperator *op) { ScrArea *area = CTX_wm_area(C); SpaceFile *sfile = CTX_wm_space_file(C); struct FSMenu *fsmenu = ED_fsmenu_get(); int nentries = ED_fsmenu_get_nentries(fsmenu, FS_CATEGORY_BOOKMARKS); PropertyRNA *prop = RNA_struct_find_property(op->ptr, "index"); const int index = RNA_property_is_set(op->ptr, prop) ? RNA_property_int_get(op->ptr, prop) : sfile->bookmarknr; if ((index > -1) && (index < nentries)) { fsmenu_remove_entry(fsmenu, FS_CATEGORY_BOOKMARKS, index); fsmenu_write_file_and_refresh_or_report_error(fsmenu, area, op->reports); } return OPERATOR_FINISHED; } void FILE_OT_bookmark_delete(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Delete Bookmark"; ot->description = "Delete selected bookmark"; ot->idname = "FILE_OT_bookmark_delete"; /* api callbacks */ ot->exec = bookmark_delete_exec; /* Bookmarks are for file browsing only (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* properties */ prop = RNA_def_int(ot->srna, "index", -1, -1, 20000, "Index", "", -1, 20000); RNA_def_property_flag(prop, PROP_SKIP_SAVE); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Cleanup Bookmark Operator * \{ */ static int bookmark_cleanup_exec(bContext *C, wmOperator *op) { ScrArea *area = CTX_wm_area(C); struct FSMenu *fsmenu = ED_fsmenu_get(); struct FSMenuEntry *fsme_next, *fsme = ED_fsmenu_get_category(fsmenu, FS_CATEGORY_BOOKMARKS); int index; bool changed = false; for (index = 0; fsme; fsme = fsme_next) { fsme_next = fsme->next; if (!BLI_is_dir(fsme->path)) { fsmenu_remove_entry(fsmenu, FS_CATEGORY_BOOKMARKS, index); changed = true; } else { index++; } } if (changed) { fsmenu_write_file_and_refresh_or_report_error(fsmenu, area, op->reports); fsmenu_refresh_bookmarks_status(CTX_wm_manager(C), fsmenu); } return OPERATOR_FINISHED; } void FILE_OT_bookmark_cleanup(wmOperatorType *ot) { /* identifiers */ ot->name = "Cleanup Bookmarks"; ot->description = "Delete all invalid bookmarks"; ot->idname = "FILE_OT_bookmark_cleanup"; /* api callbacks */ ot->exec = bookmark_cleanup_exec; /* Bookmarks are for file browsing only (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* properties */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Reorder Bookmark Operator * \{ */ enum { FILE_BOOKMARK_MOVE_TOP = -2, FILE_BOOKMARK_MOVE_UP = -1, FILE_BOOKMARK_MOVE_DOWN = 1, FILE_BOOKMARK_MOVE_BOTTOM = 2, }; static int bookmark_move_exec(bContext *C, wmOperator *op) { ScrArea *area = CTX_wm_area(C); SpaceFile *sfile = CTX_wm_space_file(C); struct FSMenu *fsmenu = ED_fsmenu_get(); struct FSMenuEntry *fsmentry = ED_fsmenu_get_category(fsmenu, FS_CATEGORY_BOOKMARKS); const struct FSMenuEntry *fsmentry_org = fsmentry; const int direction = RNA_enum_get(op->ptr, "direction"); const int totitems = ED_fsmenu_get_nentries(fsmenu, FS_CATEGORY_BOOKMARKS); const int act_index = sfile->bookmarknr; int new_index; if (totitems < 2) { return OPERATOR_CANCELLED; } switch (direction) { case FILE_BOOKMARK_MOVE_TOP: new_index = 0; break; case FILE_BOOKMARK_MOVE_BOTTOM: new_index = totitems - 1; break; case FILE_BOOKMARK_MOVE_UP: case FILE_BOOKMARK_MOVE_DOWN: default: new_index = (totitems + act_index + direction) % totitems; break; } if (new_index == act_index) { return OPERATOR_CANCELLED; } BLI_linklist_move_item((LinkNode **)&fsmentry, act_index, new_index); if (fsmentry != fsmentry_org) { ED_fsmenu_set_category(fsmenu, FS_CATEGORY_BOOKMARKS, fsmentry); } /* Need to update active bookmark number. */ sfile->bookmarknr = new_index; fsmenu_write_file_and_refresh_or_report_error(fsmenu, area, op->reports); return OPERATOR_FINISHED; } static bool file_bookmark_move_poll(bContext *C) { SpaceFile *sfile = CTX_wm_space_file(C); /* Bookmarks are for file browsing only (not asset browsing). */ if (!ED_operator_file_browsing_active(C)) { return false; } return sfile->bookmarknr != -1; } void FILE_OT_bookmark_move(wmOperatorType *ot) { static const EnumPropertyItem slot_move[] = { {FILE_BOOKMARK_MOVE_TOP, "TOP", 0, "Top", "Top of the list"}, {FILE_BOOKMARK_MOVE_UP, "UP", 0, "Up", ""}, {FILE_BOOKMARK_MOVE_DOWN, "DOWN", 0, "Down", ""}, {FILE_BOOKMARK_MOVE_BOTTOM, "BOTTOM", 0, "Bottom", "Bottom of the list"}, {0, NULL, 0, NULL, NULL}}; /* identifiers */ ot->name = "Move Bookmark"; ot->idname = "FILE_OT_bookmark_move"; ot->description = "Move the active bookmark up/down in the list"; /* api callbacks */ ot->exec = bookmark_move_exec; ot->poll = file_bookmark_move_poll; /* flags */ ot->flag = OPTYPE_REGISTER; /* No undo! */ RNA_def_enum(ot->srna, "direction", slot_move, 0, "Direction", "Direction to move the active bookmark towards"); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Reset Recent Blend Files Operator * \{ */ static int reset_recent_exec(bContext *C, wmOperator *op) { ScrArea *area = CTX_wm_area(C); struct FSMenu *fsmenu = ED_fsmenu_get(); while (ED_fsmenu_get_entry(fsmenu, FS_CATEGORY_RECENT, 0) != NULL) { fsmenu_remove_entry(fsmenu, FS_CATEGORY_RECENT, 0); } fsmenu_write_file_and_refresh_or_report_error(fsmenu, area, op->reports); return OPERATOR_FINISHED; } void FILE_OT_reset_recent(wmOperatorType *ot) { /* identifiers */ ot->name = "Reset Recent"; ot->description = "Reset recent files"; ot->idname = "FILE_OT_reset_recent"; /* api callbacks */ ot->exec = reset_recent_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Highlight File Operator * \{ */ int file_highlight_set(SpaceFile *sfile, ARegion *region, int mx, int my) { View2D *v2d = ®ion->v2d; FileSelectParams *params; int numfiles, origfile; /* In case blender starts where the mouse is over a File browser, * this operator can be invoked when the `sfile` or `sfile->layout` isn't initialized yet. */ if (sfile == NULL || sfile->files == NULL || sfile->layout == NULL) { return 0; } params = ED_fileselect_get_active_params(sfile); /* In case #SpaceFile.browse_mode just changed, the area may be pending a refresh still, which is * what creates the params for the current browse mode. See T93508. */ if (!params) { return false; } numfiles = filelist_files_ensure(sfile->files); origfile = params->highlight_file; mx -= region->winrct.xmin; my -= region->winrct.ymin; if (ED_fileselect_layout_is_inside_pt(sfile->layout, v2d, mx, my)) { float fx, fy; int highlight_file; UI_view2d_region_to_view(v2d, mx, my, &fx, &fy); highlight_file = ED_fileselect_layout_offset( sfile->layout, (int)(v2d->tot.xmin + fx), (int)(v2d->tot.ymax - fy)); if ((highlight_file >= 0) && (highlight_file < numfiles)) { params->highlight_file = highlight_file; } else { params->highlight_file = -1; } } else { params->highlight_file = -1; } return (params->highlight_file != origfile); } static int file_highlight_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); if (!file_highlight_set(sfile, region, event->xy[0], event->xy[1])) { return OPERATOR_PASS_THROUGH; } ED_area_tag_redraw(CTX_wm_area(C)); return OPERATOR_PASS_THROUGH; } void FILE_OT_highlight(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Highlight File"; ot->description = "Highlight selected file(s)"; ot->idname = "FILE_OT_highlight"; /* api callbacks */ ot->invoke = file_highlight_invoke; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Sort from Column Operator * \{ */ static int file_column_sort_ui_context_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event) { const ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); if (file_attribute_column_header_is_inside( ®ion->v2d, sfile->layout, event->mval[0], event->mval[1])) { FileSelectParams *params = ED_fileselect_get_active_params(sfile); const FileAttributeColumnType column_type = file_attribute_column_type_find_isect( ®ion->v2d, params, sfile->layout, event->mval[0]); if (column_type != COLUMN_NONE) { const FileAttributeColumn *column = &sfile->layout->attribute_columns[column_type]; BLI_assert(column->sort_type != FILE_SORT_DEFAULT); if (params->sort == column->sort_type) { /* Already sorting by selected column -> toggle sort invert (three state logic). */ params->flag ^= FILE_SORT_INVERT; } else { params->sort = column->sort_type; params->flag &= ~FILE_SORT_INVERT; } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); } } return OPERATOR_PASS_THROUGH; } void FILE_OT_sort_column_ui_context(wmOperatorType *ot) { /* identifiers */ ot->name = "Sort from Column"; ot->description = "Change sorting to use column under cursor"; ot->idname = "FILE_OT_sort_column_ui_context"; /* api callbacks */ ot->invoke = file_column_sort_ui_context_invoke; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; ot->flag = OPTYPE_INTERNAL; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Cancel File Selector Operator * \{ */ static bool file_operator_poll(bContext *C) { bool poll = ED_operator_file_browsing_active(C); SpaceFile *sfile = CTX_wm_space_file(C); if (!sfile || !sfile->op) { poll = 0; } return poll; } static int file_cancel_exec(bContext *C, wmOperator *UNUSED(unused)) { wmWindowManager *wm = CTX_wm_manager(C); SpaceFile *sfile = CTX_wm_space_file(C); wmOperator *op = sfile->op; sfile->op = NULL; WM_event_fileselect_event(wm, op, EVT_FILESELECT_CANCEL); return OPERATOR_FINISHED; } void FILE_OT_cancel(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Cancel File Load"; ot->description = "Cancel loading of selected file"; ot->idname = "FILE_OT_cancel"; /* api callbacks */ ot->exec = file_cancel_exec; ot->poll = file_operator_poll; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Operator Utilities * \{ */ void file_sfile_to_operator_ex( bContext *C, Main *bmain, wmOperator *op, SpaceFile *sfile, char *filepath) { FileSelectParams *params = ED_fileselect_get_active_params(sfile); PropertyRNA *prop; /* XXX, not real length */ if (params->file[0]) { BLI_path_join(filepath, FILE_MAX, params->dir, params->file); } else { BLI_strncpy(filepath, params->dir, FILE_MAX); BLI_path_slash_ensure(filepath, FILE_MAX); } if ((prop = RNA_struct_find_property(op->ptr, "relative_path"))) { if (RNA_property_boolean_get(op->ptr, prop)) { BLI_path_rel(filepath, BKE_main_blendfile_path(bmain)); } } char value[FILE_MAX]; if ((prop = RNA_struct_find_property(op->ptr, "filename"))) { RNA_property_string_get(op->ptr, prop, value); RNA_property_string_set(op->ptr, prop, params->file); if (RNA_property_update_check(prop) && !STREQ(params->file, value)) { RNA_property_update(C, op->ptr, prop); } } if ((prop = RNA_struct_find_property(op->ptr, "directory"))) { RNA_property_string_get(op->ptr, prop, value); RNA_property_string_set(op->ptr, prop, params->dir); if (RNA_property_update_check(prop) && !STREQ(params->dir, value)) { RNA_property_update(C, op->ptr, prop); } } if ((prop = RNA_struct_find_property(op->ptr, "filepath"))) { RNA_property_string_get(op->ptr, prop, value); RNA_property_string_set(op->ptr, prop, filepath); if (RNA_property_update_check(prop) && !STREQ(filepath, value)) { RNA_property_update(C, op->ptr, prop); } } /* some ops have multiple files to select */ /* this is called on operators check() so clear collections first since * they may be already set. */ { int i, numfiles = filelist_files_ensure(sfile->files); if ((prop = RNA_struct_find_property(op->ptr, "files"))) { PointerRNA itemptr; int num_files = 0; RNA_property_collection_clear(op->ptr, prop); for (i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(sfile->files, i, CHECK_FILES)) { FileDirEntry *file = filelist_file(sfile->files, i); /* Cannot (currently) mix regular items and alias/shortcuts in multiple selection. */ if (!file->redirection_path) { RNA_property_collection_add(op->ptr, prop, &itemptr); RNA_string_set(&itemptr, "name", file->relpath); num_files++; } } } /* make sure the file specified in the filename button is added even if no * files selected */ if (0 == num_files) { RNA_property_collection_add(op->ptr, prop, &itemptr); RNA_string_set(&itemptr, "name", params->file); } } if ((prop = RNA_struct_find_property(op->ptr, "dirs"))) { PointerRNA itemptr; int num_dirs = 0; RNA_property_collection_clear(op->ptr, prop); for (i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(sfile->files, i, CHECK_DIRS)) { FileDirEntry *file = filelist_file(sfile->files, i); RNA_property_collection_add(op->ptr, prop, &itemptr); RNA_string_set(&itemptr, "name", file->relpath); num_dirs++; } } /* make sure the directory specified in the button is added even if no * directory selected */ if (0 == num_dirs) { RNA_property_collection_add(op->ptr, prop, &itemptr); RNA_string_set(&itemptr, "name", params->dir); } } } } void file_sfile_to_operator(bContext *C, Main *bmain, wmOperator *op, SpaceFile *sfile) { char filepath_dummy[FILE_MAX]; file_sfile_to_operator_ex(C, bmain, op, sfile, filepath_dummy); } void file_operator_to_sfile(Main *bmain, SpaceFile *sfile, wmOperator *op) { FileSelectParams *params = ED_fileselect_get_active_params(sfile); PropertyRNA *prop; /* If neither of the above are set, split the filepath back */ if ((prop = RNA_struct_find_property(op->ptr, "filepath"))) { char filepath[FILE_MAX]; RNA_property_string_get(op->ptr, prop, filepath); BLI_split_dirfile( filepath, params->dir, params->file, sizeof(params->dir), sizeof(params->file)); } else { if ((prop = RNA_struct_find_property(op->ptr, "filename"))) { RNA_property_string_get(op->ptr, prop, params->file); } if ((prop = RNA_struct_find_property(op->ptr, "directory"))) { RNA_property_string_get(op->ptr, prop, params->dir); } } /* we could check for relative_path property which is used when converting * in the other direction but doesn't hurt to do this every time */ BLI_path_abs(params->dir, BKE_main_blendfile_path(bmain)); /* XXX, files and dirs updates missing, not really so important though */ } void file_sfile_filepath_set(SpaceFile *sfile, const char *filepath) { FileSelectParams *params = ED_fileselect_get_active_params(sfile); BLI_assert(BLI_exists(filepath)); if (BLI_is_dir(filepath)) { BLI_strncpy(params->dir, filepath, sizeof(params->dir)); } else { if ((params->flag & FILE_DIRSEL_ONLY) == 0) { BLI_split_dirfile( filepath, params->dir, params->file, sizeof(params->dir), sizeof(params->file)); } else { BLI_split_dir_part(filepath, params->dir, sizeof(params->dir)); } } } void file_draw_check_ex(bContext *C, ScrArea *area) { /* May happen when manipulating non-active spaces. */ if (UNLIKELY(area->spacetype != SPACE_FILE)) { return; } SpaceFile *sfile = area->spacedata.first; wmOperator *op = sfile->op; if (op) { /* fail on reload */ if (op->type->check) { Main *bmain = CTX_data_main(C); file_sfile_to_operator(C, bmain, op, sfile); /* redraw */ if (op->type->check(C, op)) { file_operator_to_sfile(bmain, sfile, op); /* redraw, else the changed settings won't get updated */ ED_area_tag_redraw(area); } } } } void file_draw_check(bContext *C) { ScrArea *area = CTX_wm_area(C); file_draw_check_ex(C, area); } void file_draw_check_cb(bContext *C, void *UNUSED(arg1), void *UNUSED(arg2)) { file_draw_check(C); } bool file_draw_check_exists(SpaceFile *sfile) { if (sfile->op) { /* fails on reload */ const FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params && (params->flag & FILE_CHECK_EXISTING)) { char filepath[FILE_MAX]; BLI_path_join(filepath, sizeof(filepath), params->dir, params->file); if (BLI_is_file(filepath)) { return true; } } } return false; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Execute File Window Operator * \{ */ /** * Execute the active file, as set in the file select params. */ static bool file_execute(bContext *C, SpaceFile *sfile) { Main *bmain = CTX_data_main(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); FileDirEntry *file = filelist_file(sfile->files, params->active_file); if (file && file->redirection_path) { /* redirection_path is an absolute path that takes precedence * over using params->dir + params->file. */ BLI_split_dirfile(file->redirection_path, params->dir, params->file, sizeof(params->dir), sizeof(params->file)); /* Update relpath with redirected filename as well so that the alternative * combination of params->dir + relpath remains valid as well. */ MEM_freeN(file->relpath); file->relpath = BLI_strdup(params->file); } /* directory change */ if (file && (file->typeflag & FILE_TYPE_DIR)) { if (!file->relpath) { return false; } if (FILENAME_IS_PARENT(file->relpath)) { BLI_path_parent_dir(params->dir); } else { BLI_path_normalize(BKE_main_blendfile_path(bmain), params->dir); BLI_path_append_dir(params->dir, sizeof(params->dir), file->relpath); } ED_file_change_dir(C); } /* Opening file, sends events now, so things get handled on window-queue level. */ else if (sfile->op) { ScrArea *area = CTX_wm_area(C); struct FSMenu *fsmenu = ED_fsmenu_get(); wmOperator *op = sfile->op; char filepath[FILE_MAX]; sfile->op = NULL; file_sfile_to_operator_ex(C, bmain, op, sfile, filepath); if (BLI_exists(params->dir)) { fsmenu_insert_entry(fsmenu, FS_CATEGORY_RECENT, params->dir, NULL, ICON_FILE_FOLDER, FS_INSERT_SAVE | FS_INSERT_FIRST); } fsmenu_write_file_and_refresh_or_report_error(fsmenu, area, op->reports); WM_event_fileselect_event(CTX_wm_manager(C), op, EVT_FILESELECT_EXEC); } return true; } static int file_exec(bContext *C, wmOperator *UNUSED(op)) { SpaceFile *sfile = CTX_wm_space_file(C); if (!file_execute(C, sfile)) { return OPERATOR_CANCELLED; } return OPERATOR_FINISHED; } void FILE_OT_execute(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Execute File Window"; ot->description = "Execute selected file"; ot->idname = "FILE_OT_execute"; /* api callbacks */ ot->exec = file_exec; /* Important since handler is on window level. * * Avoid using #file_operator_poll since this is also used for entering directories * which is used even when the file manager doesn't have an operator. */ ot->poll = ED_operator_file_browsing_active; } /** * \returns false if the mouse doesn't hover a selectable item. */ static bool file_ensure_hovered_is_active(bContext *C, const wmEvent *event) { rcti rect = file_select_mval_to_select_rect(event->mval); if (file_select(C, &rect, FILE_SEL_ADD, false, false) == FILE_SELECT_NOTHING) { return false; } return true; } static int file_execute_mouse_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceFile *sfile = CTX_wm_space_file(C); if (!ED_fileselect_layout_is_inside_pt( sfile->layout, ®ion->v2d, event->mval[0], event->mval[1])) { return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; } /* Note that this isn't needed practically, because the keymap already activates the hovered item * on mouse-press. This execute operator is called afterwards on the double-click event then. * However relying on this would be fragile and could break with keymap changes, so better to * have this mouse-execute operator that makes sure once more that the hovered file is active. */ if (!file_ensure_hovered_is_active(C, event)) { return OPERATOR_CANCELLED; } if (!file_execute(C, sfile)) { return OPERATOR_CANCELLED; } return OPERATOR_FINISHED; } void FILE_OT_mouse_execute(wmOperatorType *ot) { /* identifiers */ ot->name = "Execute File"; ot->description = "Perform the current execute action for the file under the cursor (e.g. open the file)"; ot->idname = "FILE_OT_mouse_execute"; /* api callbacks */ ot->invoke = file_execute_mouse_invoke; ot->poll = ED_operator_file_browsing_active; ot->flag = OPTYPE_INTERNAL; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Refresh File List Operator * \{ */ static int file_refresh_exec(bContext *C, wmOperator *UNUSED(unused)) { wmWindowManager *wm = CTX_wm_manager(C); SpaceFile *sfile = CTX_wm_space_file(C); struct FSMenu *fsmenu = ED_fsmenu_get(); ED_fileselect_clear(wm, sfile); /* refresh system directory menu */ fsmenu_refresh_system_category(fsmenu); /* Update bookmarks 'valid' state. */ fsmenu_refresh_bookmarks_status(wm, fsmenu); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); return OPERATOR_FINISHED; } void FILE_OT_refresh(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Refresh File List"; ot->description = "Refresh the file list"; ot->idname = "FILE_OT_refresh"; /* api callbacks */ ot->exec = file_refresh_exec; ot->poll = ED_operator_file_browsing_active; /* <- important, handler is on window level */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Navigate Parent Operator * \{ */ static int file_parent_exec(bContext *C, wmOperator *UNUSED(unused)) { Main *bmain = CTX_data_main(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { if (BLI_path_parent_dir(params->dir)) { BLI_path_normalize_dir(BKE_main_blendfile_path(bmain), params->dir, sizeof(params->dir)); ED_file_change_dir(C); if (params->recursion_level > 1) { /* Disable 'dirtree' recursion when going up in tree. */ params->recursion_level = 0; filelist_setrecursion(sfile->files, params->recursion_level); } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); } } return OPERATOR_FINISHED; } void FILE_OT_parent(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Parent File"; ot->description = "Move to parent directory"; ot->idname = "FILE_OT_parent"; /* api callbacks */ ot->exec = file_parent_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* <- important, handler is on window level */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Navigate Previous Operator * \{ */ static int file_previous_exec(bContext *C, wmOperator *UNUSED(op)) { SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { folderlist_pushdir(sfile->folders_next, params->dir); folderlist_popdir(sfile->folders_prev, params->dir); folderlist_pushdir(sfile->folders_next, params->dir); ED_file_change_dir(C); } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); return OPERATOR_FINISHED; } void FILE_OT_previous(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Previous Folder"; ot->description = "Move to previous folder"; ot->idname = "FILE_OT_previous"; /* api callbacks */ ot->exec = file_previous_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* <- important, handler is on window level */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Navigate Next Operator * \{ */ static int file_next_exec(bContext *C, wmOperator *UNUSED(unused)) { SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { folderlist_pushdir(sfile->folders_prev, params->dir); folderlist_popdir(sfile->folders_next, params->dir); /* update folders_prev so we can check for it in #folderlist_clear_next() */ folderlist_pushdir(sfile->folders_prev, params->dir); ED_file_change_dir(C); } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); return OPERATOR_FINISHED; } void FILE_OT_next(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Next Folder"; ot->description = "Move to next folder"; ot->idname = "FILE_OT_next"; /* api callbacks */ ot->exec = file_next_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* <- important, handler is on window level */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Smooth Scroll Operator * \{ */ /* only meant for timer usage */ static int file_smoothscroll_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event) { ScrArea *area = CTX_wm_area(C); SpaceFile *sfile = CTX_wm_space_file(C); ARegion *region, *region_ctx = CTX_wm_region(C); const bool is_horizontal = (sfile->layout->flag & FILE_LAYOUT_HOR) != 0; int i; /* escape if not our timer */ if (sfile->smoothscroll_timer == NULL || sfile->smoothscroll_timer != event->customdata) { return OPERATOR_PASS_THROUGH; } const int numfiles = filelist_files_ensure(sfile->files); /* Due to async nature of file listing, we may execute this code before `file_refresh()` * editing entry is available in our listing, * so we also have to handle switching to rename mode here. */ FileSelectParams *params = ED_fileselect_get_active_params(sfile); if ((params->rename_flag & (FILE_PARAMS_RENAME_PENDING | FILE_PARAMS_RENAME_POSTSCROLL_PENDING)) != 0) { file_params_renamefile_activate(sfile, params); } /* check if we are editing a name */ int edit_idx = -1; for (i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(sfile->files, i, CHECK_ALL) & (FILE_SEL_EDITING | FILE_SEL_HIGHLIGHTED)) { edit_idx = i; break; } } wmWindowManager *wm = CTX_wm_manager(C); wmWindow *win = CTX_wm_window(C); /* if we are not editing, we are done */ if (edit_idx == -1) { /* Do not invalidate timer if filerename is still pending, * we might still be building the filelist and yet have to find edited entry. */ if (params->rename_flag == 0) { file_params_smoothscroll_timer_clear(wm, win, sfile); } return OPERATOR_PASS_THROUGH; } /* we need the correct area for scrolling */ region = BKE_area_find_region_type(area, RGN_TYPE_WINDOW); if (!region || region->regiontype != RGN_TYPE_WINDOW) { file_params_smoothscroll_timer_clear(wm, win, sfile); return OPERATOR_PASS_THROUGH; } /* Number of items in a block (i.e. lines in a column in horizontal layout, or columns in a line * in vertical layout). */ const int items_block_size = is_horizontal ? sfile->layout->rows : sfile->layout->flow_columns; /* Scroll offset is the first file in the row/column we are editing in. */ if (sfile->scroll_offset == 0) { sfile->scroll_offset = (edit_idx / items_block_size) * items_block_size; } const int numfiles_layout = ED_fileselect_layout_numfiles(sfile->layout, region); const int first_visible_item = ED_fileselect_layout_offset( sfile->layout, (int)region->v2d.cur.xmin, (int)-region->v2d.cur.ymax); const int last_visible_item = first_visible_item + numfiles_layout + 1; /* NOTE: the special case for vertical layout is because filename is at the bottom of items then, * so we artificially move current row back one step, to ensure we show bottom of * active item rather than its top (important in case visible height is low). */ const int middle_offset = max_ii( 0, (first_visible_item + last_visible_item) / 2 - (is_horizontal ? 0 : items_block_size)); const int min_middle_offset = numfiles_layout / 2; const int max_middle_offset = ((numfiles / items_block_size) * items_block_size + ((numfiles % items_block_size) != 0 ? items_block_size : 0)) - (numfiles_layout / 2); /* Actual (physical) scrolling info, in pixels, used to detect whether we are fully at the * beginning/end of the view. */ /* Note that there is a weird glitch, that sometimes tot rctf is smaller than cur rctf... * that is why we still need to keep the min/max_middle_offset checks too. :( */ const float min_tot_scroll = is_horizontal ? region->v2d.tot.xmin : -region->v2d.tot.ymax; const float max_tot_scroll = is_horizontal ? region->v2d.tot.xmax : -region->v2d.tot.ymin; const float min_curr_scroll = is_horizontal ? region->v2d.cur.xmin : -region->v2d.cur.ymax; const float max_curr_scroll = is_horizontal ? region->v2d.cur.xmax : -region->v2d.cur.ymin; /* Check if we have reached our final scroll position. */ /* Filelist has to be ready, otherwise it makes no sense to stop scrolling yet. */ const bool is_ready = filelist_is_ready(sfile->files); /* Edited item must be in the 'middle' of shown area (kind of approximated). * Note that we have to do the check in 'block space', not in 'item space' here. */ const bool is_centered = (abs(middle_offset / items_block_size - sfile->scroll_offset / items_block_size) == 0); /* OR edited item must be towards the beginning, and we are scrolled fully to the start. */ const bool is_full_start = ((sfile->scroll_offset < min_middle_offset) && (min_curr_scroll - min_tot_scroll < 1.0f) && (middle_offset - min_middle_offset < items_block_size)); /* OR edited item must be towards the end, and we are scrolled fully to the end. * This one is crucial (unlike the one for the beginning), because without it we won't scroll * fully to the end, and last column or row will end up only partially drawn. */ const bool is_full_end = ((sfile->scroll_offset > max_middle_offset) && (max_tot_scroll - max_curr_scroll < 1.0f) && (max_middle_offset - middle_offset < items_block_size)); if (is_ready && (is_centered || is_full_start || is_full_end)) { file_params_smoothscroll_timer_clear(wm, win, sfile); /* Post-scroll (after rename has been validated by user) is done, * rename process is totally finished, cleanup. */ if ((params->rename_flag & FILE_PARAMS_RENAME_POSTSCROLL_ACTIVE) != 0) { file_params_renamefile_clear(params); } return OPERATOR_FINISHED; } /* Temporarily set context to the main window region, * so that the pan operator works. */ CTX_wm_region_set(C, region); /* scroll one step in the desired direction */ PointerRNA op_ptr; int deltax = 0; int deltay = 0; /* We adjust speed of scrolling to avoid tens of seconds of it in e.g. directories with tens of * thousands of folders... See T65782. */ /* This will slow down scrolling when approaching final goal, also avoids going too far and * having to bounce back... */ /* Number of blocks (columns in horizontal layout, rows otherwise) between current middle of * screen, and final goal position. */ const int diff_offset = sfile->scroll_offset / items_block_size - middle_offset / items_block_size; /* convert diff_offset into pixels. */ const int diff_offset_delta = abs(diff_offset) * (is_horizontal ? sfile->layout->tile_w + 2 * sfile->layout->tile_border_x : sfile->layout->tile_h + 2 * sfile->layout->tile_border_y); const int scroll_delta = max_ii(2, diff_offset_delta / 15); if (diff_offset < 0) { if (is_horizontal) { deltax = -scroll_delta; } else { deltay = scroll_delta; } } else { if (is_horizontal) { deltax = scroll_delta; } else { deltay = -scroll_delta; } } WM_operator_properties_create(&op_ptr, "VIEW2D_OT_pan"); RNA_int_set(&op_ptr, "deltax", deltax); RNA_int_set(&op_ptr, "deltay", deltay); WM_operator_name_call(C, "VIEW2D_OT_pan", WM_OP_EXEC_DEFAULT, &op_ptr, event); WM_operator_properties_free(&op_ptr); ED_region_tag_redraw(region); /* and restore context */ CTX_wm_region_set(C, region_ctx); return OPERATOR_FINISHED; } void FILE_OT_smoothscroll(wmOperatorType *ot) { /* identifiers */ ot->name = "Smooth Scroll"; ot->idname = "FILE_OT_smoothscroll"; ot->description = "Smooth scroll to make editable file visible"; /* api callbacks */ ot->invoke = file_smoothscroll_invoke; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name File Selector Drop Operator * \{ */ static int filepath_drop_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); SpaceFile *sfile = CTX_wm_space_file(C); if (sfile) { char filepath[FILE_MAX]; RNA_string_get(op->ptr, "filepath", filepath); if (!BLI_exists(filepath)) { BKE_report(op->reports, RPT_ERROR, "File does not exist"); return OPERATOR_CANCELLED; } file_sfile_filepath_set(sfile, filepath); if (sfile->op) { file_sfile_to_operator(C, bmain, sfile->op, sfile); file_draw_check(C); } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); return OPERATOR_FINISHED; } return OPERATOR_CANCELLED; } void FILE_OT_filepath_drop(wmOperatorType *ot) { ot->name = "File Selector Drop"; ot->idname = "FILE_OT_filepath_drop"; ot->exec = filepath_drop_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; RNA_def_string_file_path(ot->srna, "filepath", "Path", FILE_MAX, "", ""); } /** \} */ /* -------------------------------------------------------------------- */ /** \name New Directory Operator * \{ */ /** * Create a new, non-existing folder name, returns true if successful, * false if name couldn't be created. * The actual name is returned in 'name', 'folder' contains the complete path, * including the new folder name. */ static bool new_folder_path(const char *parent, char folder[FILE_MAX], char name[FILE_MAXFILE]) { int i = 1; int len = 0; BLI_strncpy(name, "New Folder", FILE_MAXFILE); BLI_path_join(folder, FILE_MAX, parent, name); /* check whether folder with the name already exists, in this case * add number to the name. Check length of generated name to avoid * crazy case of huge number of folders each named 'New Folder (x)' */ while (BLI_exists(folder) && (len < FILE_MAXFILE)) { len = BLI_snprintf(name, FILE_MAXFILE, "New Folder(%d)", i); BLI_path_join(folder, FILE_MAX, parent, name); i++; } return (len < FILE_MAXFILE); } static int file_directory_new_exec(bContext *C, wmOperator *op) { char name[FILE_MAXFILE]; char path[FILE_MAX]; bool generate_name = true; wmWindowManager *wm = CTX_wm_manager(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); const bool do_diropen = RNA_boolean_get(op->ptr, "open"); if (!params) { BKE_report(op->reports, RPT_WARNING, "No parent directory given"); return OPERATOR_CANCELLED; } path[0] = '\0'; { PropertyRNA *prop = RNA_struct_find_property(op->ptr, "directory"); RNA_property_string_get(op->ptr, prop, path); if (path[0] != '\0') { generate_name = false; } } if (generate_name) { /* create a new, non-existing folder name */ if (!new_folder_path(params->dir, path, name)) { BKE_report(op->reports, RPT_ERROR, "Could not create new folder name"); return OPERATOR_CANCELLED; } } else { /* We assume we are able to generate a valid name! */ char org_path[FILE_MAX]; BLI_strncpy(org_path, path, sizeof(org_path)); if (BLI_path_make_safe(path)) { BKE_reportf(op->reports, RPT_WARNING, "'%s' given path is OS-invalid, creating '%s' path instead", org_path, path); } } /* create the file */ errno = 0; if (!BLI_dir_create_recursive(path) || /* Should no more be needed, * now that BLI_dir_create_recursive returns a success state - but kept just in case. */ !BLI_exists(path)) { BKE_reportf(op->reports, RPT_ERROR, "Could not create new folder: %s", errno ? strerror(errno) : "unknown error"); return OPERATOR_CANCELLED; } eFileSel_Params_RenameFlag rename_flag = params->rename_flag; /* If we don't enter the directory directly, remember file to jump into editing. */ if (do_diropen == false) { BLI_assert_msg(params->rename_id == NULL, "File rename handling should immediately clear rename_id when done, " "because otherwise it will keep taking precedence over renamefile."); BLI_strncpy(params->renamefile, name, FILE_MAXFILE); rename_flag = FILE_PARAMS_RENAME_PENDING; } file_params_invoke_rename_postscroll(wm, CTX_wm_window(C), sfile); params->rename_flag = rename_flag; /* reload dir to make sure we're seeing what's in the directory */ ED_fileselect_clear(wm, sfile); if (do_diropen) { BLI_strncpy(params->dir, path, sizeof(params->dir)); ED_file_change_dir(C); } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); return OPERATOR_FINISHED; } void FILE_OT_directory_new(struct wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Create New Directory"; ot->description = "Create a new directory"; ot->idname = "FILE_OT_directory_new"; /* api callbacks */ ot->invoke = WM_operator_confirm_or_exec; ot->exec = file_directory_new_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* <- important, handler is on window level */ prop = RNA_def_string_dir_path( ot->srna, "directory", NULL, FILE_MAX, "Directory", "Name of new directory"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "open", false, "Open", "Open new directory"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); WM_operator_properties_confirm_or_exec(ot); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Refresh File List Operator * \{ */ /* TODO: This should go to BLI_path_utils. */ static void file_expand_directory(bContext *C) { Main *bmain = CTX_data_main(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { if (BLI_path_is_rel(params->dir)) { /* Use of 'default' folder here is just to avoid an error message on '//' prefix. */ const char *blendfile_path = BKE_main_blendfile_path(bmain); BLI_path_abs(params->dir, (blendfile_path[0] != '\0') ? blendfile_path : BKE_appdir_folder_default_or_root()); } else if (params->dir[0] == '~') { char tmpstr[sizeof(params->dir) - 1]; BLI_strncpy(tmpstr, params->dir + 1, sizeof(tmpstr)); BLI_path_join(params->dir, sizeof(params->dir), BKE_appdir_folder_default_or_root(), tmpstr); } else if (params->dir[0] == '\0') #ifndef WIN32 { params->dir[0] = '/'; params->dir[1] = '\0'; } #else { BLI_windows_get_default_root_dir(params->dir); } /* change "C:" --> "C:\", T28102. */ else if ((isalpha(params->dir[0]) && (params->dir[1] == ':')) && (params->dir[2] == '\0')) { params->dir[2] = '\\'; params->dir[3] = '\0'; } else if (BLI_path_is_unc(params->dir)) { BLI_path_normalize_unc(params->dir, FILE_MAX_LIBEXTRA); } #endif } } /* TODO: check we still need this, it's annoying to have OS-specific code here... :/. */ #if defined(WIN32) static bool can_create_dir(const char *dir) { /* for UNC paths we need to check whether the parent of the new * directory is a proper directory itself and not a share or the * UNC root (server name) itself. Calling BLI_is_dir does this */ if (BLI_path_is_unc(dir)) { char parent[PATH_MAX]; BLI_strncpy(parent, dir, PATH_MAX); BLI_path_parent_dir(parent); return BLI_is_dir(parent); } return true; } #endif void file_directory_enter_handle(bContext *C, void *UNUSED(arg_unused), void *UNUSED(arg_but)) { Main *bmain = CTX_data_main(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { char old_dir[sizeof(params->dir)]; BLI_strncpy(old_dir, params->dir, sizeof(old_dir)); file_expand_directory(C); /* special case, user may have pasted a filepath into the directory */ if (!filelist_is_dir(sfile->files, params->dir)) { char tdir[FILE_MAX_LIBEXTRA]; char *group, *name; if (BLI_is_file(params->dir)) { char path[sizeof(params->dir)]; BLI_strncpy(path, params->dir, sizeof(path)); BLI_split_dirfile( path, params->dir, params->file, sizeof(params->dir), sizeof(params->file)); } else if (BLO_library_path_explode(params->dir, tdir, &group, &name)) { if (group) { BLI_path_append(tdir, sizeof(tdir), group); } BLI_strncpy(params->dir, tdir, sizeof(params->dir)); if (name) { BLI_strncpy(params->file, name, sizeof(params->file)); } else { params->file[0] = '\0'; } } } BLI_path_normalize_dir(BKE_main_blendfile_path(bmain), params->dir, sizeof(params->dir)); if (filelist_is_dir(sfile->files, params->dir)) { if (!STREQ(params->dir, old_dir)) { /* Avoids flickering when nothing's changed. */ /* if directory exists, enter it immediately */ ED_file_change_dir(C); } /* don't do for now because it selects entire text instead of * placing cursor at the end */ // UI_textbutton_activate_but(C, but); } #if defined(WIN32) else if (!can_create_dir(params->dir)) { const char *lastdir = folderlist_peeklastdir(sfile->folders_prev); if (lastdir) { BLI_strncpy(params->dir, lastdir, sizeof(params->dir)); } } #endif else { const char *lastdir = folderlist_peeklastdir(sfile->folders_prev); char tdir[FILE_MAX_LIBEXTRA]; /* If we are 'inside' a blend library, we cannot do anything... */ if (lastdir && BLO_library_path_explode(lastdir, tdir, NULL, NULL)) { BLI_strncpy(params->dir, lastdir, sizeof(params->dir)); } else { /* if not, ask to create it and enter if confirmed */ wmOperatorType *ot = WM_operatortype_find("FILE_OT_directory_new", false); PointerRNA ptr; WM_operator_properties_create_ptr(&ptr, ot); RNA_string_set(&ptr, "directory", params->dir); RNA_boolean_set(&ptr, "open", true); /* Enable confirmation prompt, else it's too easy * to accidentally create new directories. */ RNA_boolean_set(&ptr, "confirm", true); if (lastdir) { BLI_strncpy(params->dir, lastdir, sizeof(params->dir)); } WM_operator_name_call_ptr(C, ot, WM_OP_INVOKE_DEFAULT, &ptr, NULL); WM_operator_properties_free(&ptr); } } WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); } } void file_filename_enter_handle(bContext *C, void *UNUSED(arg_unused), void *arg_but) { Main *bmain = CTX_data_main(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); uiBut *but = arg_but; char matched_file[FILE_MAX]; if (params) { char filepath[sizeof(params->dir)]; int matches; matched_file[0] = '\0'; filepath[0] = '\0'; file_expand_directory(C); matches = file_select_match(sfile, params->file, matched_file); /* *After* file_select_match! */ const bool allow_tokens = (params->flag & FILE_PATH_TOKENS_ALLOW) != 0; BLI_filename_make_safe_ex(params->file, allow_tokens); if (matches) { /* replace the pattern (or filename that the user typed in, * with the first selected file of the match */ BLI_strncpy(params->file, matched_file, sizeof(params->file)); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); } if (matches == 1) { BLI_path_join(filepath, sizeof(params->dir), params->dir, params->file); /* if directory, open it and empty filename field */ if (filelist_is_dir(sfile->files, filepath)) { BLI_path_normalize_dir(BKE_main_blendfile_path(bmain), filepath, sizeof(filepath)); BLI_strncpy(params->dir, filepath, sizeof(params->dir)); params->file[0] = '\0'; ED_file_change_dir(C); UI_textbutton_activate_but(C, but); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL); } } else if (matches > 1) { file_draw_check(C); } } } /** \} */ /* -------------------------------------------------------------------- */ /** \name Toggle Show Hidden Files Operator * \{ */ static int file_hidedot_exec(bContext *C, wmOperator *UNUSED(unused)) { wmWindowManager *wm = CTX_wm_manager(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { params->flag ^= FILE_HIDE_DOT; ED_fileselect_clear(wm, sfile); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); } return OPERATOR_FINISHED; } void FILE_OT_hidedot(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Toggle Hide Dot Files"; ot->description = "Toggle hide hidden dot files"; ot->idname = "FILE_OT_hidedot"; /* api callbacks */ ot->exec = file_hidedot_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; /* <- important, handler is on window level */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Increment Filename Operator * \{ */ static bool file_filenum_poll(bContext *C) { SpaceFile *sfile = CTX_wm_space_file(C); /* File browsing only operator (not asset browsing). */ if (!ED_operator_file_browsing_active(C)) { return false; } FileSelectParams *params = ED_fileselect_get_active_params(sfile); return params && (params->flag & FILE_CHECK_EXISTING); } /** * Looks for a string of digits within name (using BLI_path_sequence_decode) and adjusts it by add. */ static void filenum_newname(char *name, size_t name_size, int add) { char head[FILE_MAXFILE], tail[FILE_MAXFILE]; char name_temp[FILE_MAXFILE]; int pic; ushort digits; pic = BLI_path_sequence_decode(name, head, tail, &digits); /* are we going from 100 -> 99 or from 10 -> 9 */ if (add < 0 && digits > 0) { int i, exp; exp = 1; for (i = digits; i > 1; i--) { exp *= 10; } if (pic >= exp && (pic + add) < exp) { digits--; } } pic += add; if (pic < 0) { pic = 0; } BLI_path_sequence_encode(name_temp, head, tail, digits, pic); BLI_strncpy(name, name_temp, name_size); } static int file_filenum_exec(bContext *C, wmOperator *op) { SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); ScrArea *area = CTX_wm_area(C); int inc = RNA_int_get(op->ptr, "increment"); if (params && (inc != 0)) { filenum_newname(params->file, sizeof(params->file), inc); ED_area_tag_redraw(area); file_draw_check(C); // WM_event_add_notifier(C, NC_WINDOW, NULL); } return OPERATOR_FINISHED; } void FILE_OT_filenum(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Increment Number in Filename"; ot->description = "Increment number in filename"; ot->idname = "FILE_OT_filenum"; /* api callbacks */ ot->exec = file_filenum_exec; ot->poll = file_filenum_poll; /* props */ RNA_def_int(ot->srna, "increment", 1, -100, 100, "Increment", "", -100, 100); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Rename File/Directory Operator * \{ */ static void file_rename_state_activate(SpaceFile *sfile, int file_idx, bool require_selected) { const int numfiles = filelist_files_ensure(sfile->files); if ((file_idx >= 0) && (file_idx < numfiles)) { FileDirEntry *file = filelist_file(sfile->files, file_idx); if ((require_selected == false) || (filelist_entry_select_get(sfile->files, file, CHECK_ALL) & FILE_SEL_SELECTED)) { FileSelectParams *params = ED_fileselect_get_active_params(sfile); filelist_entry_select_index_set( sfile->files, file_idx, FILE_SEL_ADD, FILE_SEL_EDITING, CHECK_ALL); BLI_strncpy(params->renamefile, file->relpath, FILE_MAXFILE); /* We can skip the pending state, * as we can directly set FILE_SEL_EDITING on the expected entry here. */ params->rename_flag = FILE_PARAMS_RENAME_ACTIVE; } } } static int file_rename_exec(bContext *C, wmOperator *UNUSED(op)) { ScrArea *area = CTX_wm_area(C); SpaceFile *sfile = (SpaceFile *)CTX_wm_space_data(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (params) { file_rename_state_activate(sfile, params->active_file, false); ED_area_tag_redraw(area); } return OPERATOR_FINISHED; } void FILE_OT_rename(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Rename File or Directory"; ot->description = "Rename file or file directory"; ot->idname = "FILE_OT_rename"; /* api callbacks */ ot->exec = file_rename_exec; /* File browsing only operator (not asset browsing). */ ot->poll = ED_operator_file_browsing_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Delete File Operator * \{ */ static bool file_delete_poll(bContext *C) { if (!ED_operator_file_browsing_active(C)) { return false; } SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); if (!sfile || !params) { return false; } char dir[FILE_MAX_LIBEXTRA]; if (filelist_islibrary(sfile->files, dir, NULL)) { return false; } int numfiles = filelist_files_ensure(sfile->files); for (int i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(sfile->files, i, CHECK_ALL)) { /* Has a selected file -> the operator can run. */ return true; } } return false; } static bool file_delete_single(const FileSelectParams *params, FileDirEntry *file, const char **r_error_message) { char str[FILE_MAX]; BLI_path_join(str, sizeof(str), params->dir, file->relpath); if (BLI_delete_soft(str, r_error_message) != 0 || BLI_exists(str)) { return false; } return true; } static int file_delete_exec(bContext *C, wmOperator *op) { wmWindowManager *wm = CTX_wm_manager(C); SpaceFile *sfile = CTX_wm_space_file(C); FileSelectParams *params = ED_fileselect_get_active_params(sfile); int numfiles = filelist_files_ensure(sfile->files); const char *error_message = NULL; bool report_error = false; errno = 0; for (int i = 0; i < numfiles; i++) { if (filelist_entry_select_index_get(sfile->files, i, CHECK_ALL)) { FileDirEntry *file = filelist_file(sfile->files, i); if (!file_delete_single(params, file, &error_message)) { report_error = true; } } } if (report_error) { if (error_message != NULL) { BKE_reportf(op->reports, RPT_ERROR, "Could not delete file or directory: %s", error_message); } else { BKE_reportf(op->reports, RPT_ERROR, "Could not delete file or directory: %s", errno ? strerror(errno) : "unknown error"); } } ED_fileselect_clear(wm, sfile); WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, NULL); return OPERATOR_FINISHED; } void FILE_OT_delete(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Delete Selected Files"; ot->description = "Move selected files to the trash or recycle bin"; ot->idname = "FILE_OT_delete"; /* api callbacks */ ot->invoke = WM_operator_confirm; ot->exec = file_delete_exec; ot->poll = file_delete_poll; /* <- important, handler is on window level */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Enter Filter Text Operator * \{ */ static int file_start_filter_exec(bContext *C, wmOperator *UNUSED(op)) { const ScrArea *area = CTX_wm_area(C); const SpaceFile *sfile = CTX_wm_space_file(C); const FileSelectParams *params = ED_fileselect_get_active_params(sfile); ARegion *region_ctx = CTX_wm_region(C); if (area) { LISTBASE_FOREACH (ARegion *, region, &area->regionbase) { CTX_wm_region_set(C, region); if (UI_textbutton_activate_rna(C, region, params, "filter_search")) { break; } } } CTX_wm_region_set(C, region_ctx); return OPERATOR_FINISHED; } void FILE_OT_start_filter(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Filter"; ot->description = "Start entering filter text"; ot->idname = "FILE_OT_start_filter"; /* api callbacks */ ot->exec = file_start_filter_exec; /* Operator works for file or asset browsing */ ot->poll = ED_operator_file_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Edit Directory Path Operator * \{ */ static int file_edit_directory_path_exec(bContext *C, wmOperator *UNUSED(op)) { const ScrArea *area = CTX_wm_area(C); const SpaceFile *sfile = CTX_wm_space_file(C); const FileSelectParams *params = ED_fileselect_get_active_params(sfile); ARegion *region_ctx = CTX_wm_region(C); if (area) { LISTBASE_FOREACH (ARegion *, region, &area->regionbase) { CTX_wm_region_set(C, region); if (UI_textbutton_activate_rna(C, region, params, "directory")) { break; } } } CTX_wm_region_set(C, region_ctx); return OPERATOR_FINISHED; } void FILE_OT_edit_directory_path(struct wmOperatorType *ot) { /* identifiers */ ot->name = "Edit Directory Path"; ot->description = "Start editing directory field"; ot->idname = "FILE_OT_edit_directory_path"; /* api callbacks */ ot->exec = file_edit_directory_path_exec; ot->poll = ED_operator_file_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Macro Operators * \{ */ void ED_operatormacros_file(void) { // wmOperatorType *ot; // wmOperatorTypeMacro *otmacro; /* future macros */ } /** \} */