--- hatari/src/gui-sdl/dlgFileSelect.c 2019/04/01 07:11:38 1.1.1.1 +++ hatari/src/gui-sdl/dlgFileSelect.c 2019/04/09 08:59:37 1.1.1.15 @@ -1,35 +1,45 @@ /* Hatari - dlgFileSelect.c - - This file is distributed under the GNU Public License, version 2 or at - your option any later version. Read the file gpl.txt for details. - + + This file is distributed under the GNU General Public License, version 2 + or at your option any later version. Read the file gpl.txt for details. + A file selection dialog for the graphical user interface for Hatari. */ -char DlgFileSelect_rcsid[] = "Hatari $Id: dlgFileSelect.c,v 1.1.1.1 2019/04/01 07:11:38 root Exp $"; +const char DlgFileSelect_fileid[] = "Hatari dlgFileSelect.c : " __DATE__ " " __TIME__; #include #include #include -#include #include "main.h" -#include "screen.h" +#include "scandir.h" #include "sdlgui.h" #include "file.h" +#include "paths.h" #include "zip.h" +#include "log.h" -#define SGFSDLG_FILENAME 5 -#define SGFSDLG_UPDIR 6 -#define SGFSDLG_ROOTDIR 7 -#define SGFSDLG_ENTRY1 10 -#define SGFSDLG_ENTRY16 25 -#define SGFSDLG_UP 26 -#define SGFSDLG_DOWN 27 -#define SGFSDLG_OKAY 28 -#define SGFSDLG_CANCEL 29 +#define SGFS_NUMENTRIES 16 /* How many entries are displayed at once */ +#define SGFSDLG_TITLE 1 +#define SGFSDLG_FILENAME 5 +#define SGFSDLG_UPDIR 6 +#define SGFSDLG_CWD 7 +#define SGFSDLG_HOMEDIR 8 +#define SGFSDLG_ROOTDIR 9 +#define SGFSDLG_ENTRYFIRST 12 +#define SGFSDLG_ENTRYLAST 27 +#define SGFSDLG_SCROLLBAR 28 +#define SGFSDLG_UP 29 +#define SGFSDLG_DOWN 30 +#define SGFSDLG_SHOWHIDDEN 31 +#define SGFSDLG_OKAY 32 +#define SGFSDLG_CANCEL 33 + +#define SCROLLOUT_ABOVE 1 +#define SCROLLOUT_UNDER 2 #define DLGPATH_SIZE 62 static char dlgpath[DLGPATH_SIZE+1]; /* Path name in the dialog */ @@ -38,19 +48,26 @@ static char dlgpath[DLGPATH_SIZE+1]; static char dlgfname[DLGFNAME_SIZE+1]; /* Name of the selected file in the dialog */ #define DLGFILENAMES_SIZE 59 -static char dlgfilenames[16][DLGFILENAMES_SIZE+1]; /* Visible file names in the dialog */ +static char dlgfilenames[SGFS_NUMENTRIES][DLGFILENAMES_SIZE+1]; /* Visible file names in the dialog */ + +#define SCROLLBAR_MIN_HEIGHT 4 /* Min value for yScrollbar_size */ + +#define TITLE_OFFSET 1 +#define TITLE_MAXLEN 40 /* The dialog data: */ static SGOBJ fsdlg[] = { { SGBOX, 0, 0, 0,0, 64,25, NULL }, - { SGTEXT, 0, 0, 25,1, 13,1, "Choose a file" }, + { SGTEXT, 0, 0, 1,1, 13,1, NULL, }, { SGTEXT, 0, 0, 1,2, 7,1, "Folder:" }, { SGTEXT, 0, 0, 1,3, DLGPATH_SIZE,1, dlgpath }, { SGTEXT, 0, 0, 1,4, 6,1, "File:" }, { SGTEXT, 0, 0, 7,4, DLGFNAME_SIZE,1, dlgfname }, - { SGBUTTON, 0, 0, 55,1, 4,1, ".." }, - { SGBUTTON, 0, 0, 60,1, 3,1, "/" }, + { SGBUTTON, 0, 0, 39,1, 4,1, "_Up" }, + { SGBUTTON, 0, 0, 44,1, 5,1, "_CWD" }, + { SGBUTTON, 0, 0, 50,1, 6,1, "_Home" }, + { SGBUTTON, 0, 0, 57,1, 6,1, "_Root" }, { SGBOX, 0, 0, 1,6, 62,16, NULL }, { SGBOX, 0, 0, 62,7, 1,14, NULL }, { SGTEXT, SG_EXIT, 0, 2,6, DLGFILENAMES_SIZE,1, dlgfilenames[0] }, @@ -69,22 +86,46 @@ static SGOBJ fsdlg[] = { SGTEXT, SG_EXIT, 0, 2,19, DLGFILENAMES_SIZE,1, dlgfilenames[13] }, { SGTEXT, SG_EXIT, 0, 2,20, DLGFILENAMES_SIZE,1, dlgfilenames[14] }, { SGTEXT, SG_EXIT, 0, 2,21, DLGFILENAMES_SIZE,1, dlgfilenames[15] }, - { SGBUTTON, SG_TOUCHEXIT, 0, 62,6, 1,1, "\x01" }, /* Arrow up */ - { SGBUTTON, SG_TOUCHEXIT, 0, 62,21, 1,1, "\x02" }, /* Arrow down */ - { SGBUTTON, 0, 0, 14,23, 8,1, "Okay" }, - { SGBUTTON, 0, 0, 34,23, 8,1, "Cancel" }, - { -1, 0, 0, 0,0, 0,0, NULL } + { SGSCROLLBAR, SG_TOUCHEXIT, 0, 62, 7, 0, 0, NULL }, /* Scrollbar */ + { SGBUTTON, SG_TOUCHEXIT, 0, 62, 6,1,1, "\x01", SG_SHORTCUT_UP }, + { SGBUTTON, SG_TOUCHEXIT, 0, 62,21,1,1, "\x02", SG_SHORTCUT_DOWN }, + { SGCHECKBOX, SG_EXIT, 0, 2,23, 19,1, "_Show hidden files" }, + { SGBUTTON, SG_DEFAULT, 0, 32,23, 8,1, "OK" }, + { SGBUTTON, SG_CANCEL, 0, 50,23, 8,1, "Cancel" }, + { SGSTOP, 0, 0, 0,0, 0,0, NULL } +}; + + +static int ypos = -1; /* First entry number to be displayed. If -1, file selector start on the 1st file */ + /* else we continue from the previous position when SDLGui_FileSelect is called again */ +static bool refreshentries; /* Do we have to update the file names in the dialog? */ +static int entries; /* How many files are in the actual directory? */ +static int oldMouseY = 0; /* Keep the latest Y mouse position for scrollbar move computing */ +static int mouseClicked = 0; /* used to know if mouse if down for the first time or not */ +static int mouseIsOut = 0; /* used to keep info that mouse if above or under the scrollbar when mousebutton is down */ +static float scrollbar_Ypos = 0.0; /* scrollbar heigth */ + +static char *dirpath; /* for get_dtype() */ +#ifndef HAVE_DIRENT_D_TYPE +enum { + DT_UNKNOWN, + DT_LNK, + DT_DIR, + DT_REG }; +#endif + +/* Convert file position (in file list) to scrollbar y position */ +static void DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(void); /*-----------------------------------------------------------------------*/ -/* - Update the file name strings in the dialog. - Returns FALSE if it failed, TRUE on success. -*/ -static int DlgFileSelect_RefreshEntries(struct dirent **files, char *path, - BOOL browsingzip, int entries, int ypos) +/** + * Update the file name strings in the dialog. + * Returns false if it failed, true on success. + */ +static int DlgFileSelect_RefreshEntries(struct dirent **files, char *path, bool browsingzip) { int i; char *tempstr = malloc(FILENAME_MAX); @@ -92,13 +133,13 @@ static int DlgFileSelect_RefreshEntries( if (!tempstr) { perror("DlgFileSelect_RefreshEntries"); - return FALSE; + return false; } /* Copy entries to dialog: */ - for(i=0; i<16; i++) + for (i=0; id_name); - if( browsingzip ) + if (browsingzip) { - if( tempstr[strlen(tempstr)-1] == '/' ) + if (File_DoesFileNameEndWithSlash(tempstr)) dlgfilenames[i][0] = SGFOLDER; /* Mark folders */ } else { if( stat(tempstr, &filestat)==0 && S_ISDIR(filestat.st_mode) ) dlgfilenames[i][0] = SGFOLDER; /* Mark folders */ - if (ZIP_FileNameIsZIP(tempstr) && browsingzip == FALSE) + if (ZIP_FileNameIsZIP(tempstr) && browsingzip == false) dlgfilenames[i][0] = SGFOLDER; /* Mark .ZIP archives as folders */ } + + fsdlg[SGFSDLG_ENTRYFIRST+i].flags |= SG_EXIT; } else + { dlgfilenames[i][0] = 0; /* Clear entry */ + fsdlg[SGFSDLG_ENTRYFIRST+i].flags &= ~SG_EXIT; + } } free(tempstr); - return TRUE; + return true; } /*-----------------------------------------------------------------------*/ -/* - Show and process a file selection dialog. - Returns TRUE if the use selected "okay", FALSE if "cancel". - input: zip_path = pointer to buffer to contain file path within a selected - zip file, or NULL if browsing zip files is disallowed. - bAllowNew: TRUE if the user is allowed to insert new file names. -*/ -int SDLGui_FileSelect(char *path_and_name, char *zip_path, BOOL bAllowNew) +/** + * Remove all hidden files (files with file names that begin with a dot) from + * the list. + */ +static void DlgFileSelect_RemoveHiddenFiles(struct dirent **files) +{ + int i; + int nActPos = -1; + int nOldEntries; + + nOldEntries = entries; + + /* Scan list for hidden files and remove them. */ + for (i = 0; i < nOldEntries; i++) + { + /* Does file name start with a dot? -> hidden file! */ + if (files[i]->d_name[0] == '.') + { + if (nActPos == -1) + nActPos = i; + /* Remove file from list: */ + free(files[i]); + files[i] = NULL; + entries -= 1; + } + } + + /* Now close the gaps in the list: */ + if (nActPos != -1) + { + for (i = nActPos; i < nOldEntries; i++) + { + if (files[i] != NULL) + { + /* Move entry to earlier position: */ + files[nActPos] = files[i]; + files[i] = NULL; + nActPos += 1; + } + } + } +} + +/** + * Reset focus to first entry if necessary. + */ +static void DlgFileSelect_ResetFocus(void) +{ + int i; + + for (i = SGFSDLG_ENTRYFIRST+1; i <= SGFSDLG_ENTRYLAST; i++) + { + if (fsdlg[i].state & SG_FOCUSED) + { + fsdlg[i].state &= ~SG_FOCUSED; + fsdlg[SGFSDLG_ENTRYFIRST].state |= SG_FOCUSED; + break; + } + } +} + +/*-----------------------------------------------------------------------*/ +/** + * Prepare to scroll up one entry. + */ +static void DlgFileSelect_ScrollUp(void) +{ + if (ypos > 0) + { + --ypos; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + refreshentries = true; + } +} + + +/*-----------------------------------------------------------------------*/ +/** + * Prepare to scroll down one entry. + */ +static void DlgFileSelect_ScrollDown(void) +{ + if (ypos+SGFS_NUMENTRIES < entries) + { + ++ypos; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + refreshentries = true; + } +} + +/*-----------------------------------------------------------------------*/ +/** + * Manage the scrollbar up or down. + */ +static void DlgFileSelect_ManageScrollbar(void) +{ + int b, x, y; + int scrollY, scrollYmin, scrollYmax, scrollH_half; + float scrollMove; + + SDL_GetMouseState(&x, &y); + + /* If mouse is down on the scrollbar for the first time */ + if (fsdlg[SGFSDLG_SCROLLBAR].state & SG_MOUSEDOWN) { + if (mouseClicked == 0) { + mouseClicked = 1; + mouseIsOut = 0; + oldMouseY = y; + } + } + /* Mouse button is up on the scrollbar */ + else { + mouseClicked = 0; + oldMouseY = y; + mouseIsOut = 0; + } + + /* If mouse Y position didn't change */ + if (oldMouseY == y) + return; + + /* Compute scrollbar ymin and ymax values */ + + scrollYmin = (fsdlg[SGFSDLG_SCROLLBAR].y + fsdlg[0].y) * sdlgui_fontheight; + scrollYmax = (fsdlg[SGFSDLG_DOWN].y + fsdlg[0].y) * sdlgui_fontheight; + + scrollY = fsdlg[SGFSDLG_SCROLLBAR].y * sdlgui_fontheight + fsdlg[SGFSDLG_SCROLLBAR].h + fsdlg[0].y * sdlgui_fontheight; + scrollH_half = scrollY + fsdlg[SGFSDLG_SCROLLBAR].w / 2; + scrollMove = (float)(y-oldMouseY)/sdlgui_fontheight; + + /* Verify if mouse is not above the scrollbar area */ + if (y < scrollYmin) { + mouseIsOut = SCROLLOUT_ABOVE; + oldMouseY = y; + return; + } + if (mouseIsOut == SCROLLOUT_ABOVE && y < scrollH_half) { + oldMouseY = y; + return; + } + + /* Verify if mouse is not under the scrollbar area */ + if (y > scrollYmax) { + mouseIsOut = SCROLLOUT_UNDER; + oldMouseY = y; + return; + } + if (mouseIsOut == SCROLLOUT_UNDER && y > scrollH_half) { + oldMouseY = y; + return; + } + + mouseIsOut = 0; + + scrollbar_Ypos += scrollMove; + oldMouseY = y; + + /* Verifiy if scrollbar is in correct inferior boundary */ + if (scrollbar_Ypos < 0) + scrollbar_Ypos = 0.0; + + /* Verifiy if scrollbar is in correct superior boundary */ + b = (int) (scrollbar_Ypos * ((float)entries/(float)(SGFS_NUMENTRIES-2)) + 0.5); + if (b+SGFS_NUMENTRIES >= entries) { + ypos = entries - SGFS_NUMENTRIES; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + } + + refreshentries = true; +} + +/*-----------------------------------------------------------------------*/ +/** + * Handle SDL events. + */ +static void DlgFileSelect_HandleSdlEvents(SDL_Event *pEvent) +{ + int oldypos = ypos; + switch (pEvent->type) + { +#if WITH_SDL2 + case SDL_MOUSEWHEEL: + if (pEvent->wheel.y>0) + DlgFileSelect_ScrollUp(); + else if (pEvent->wheel.y<0) + DlgFileSelect_ScrollDown(); + break; +#else + case SDL_MOUSEBUTTONDOWN: + if (pEvent->button.button == SDL_BUTTON_WHEELUP) + DlgFileSelect_ScrollUp(); + else if (pEvent->button.button == SDL_BUTTON_WHEELDOWN) + DlgFileSelect_ScrollDown(); + break; +#endif + case SDL_KEYDOWN: + switch (pEvent->key.keysym.sym) + { + case SDLK_UP: + DlgFileSelect_ScrollUp(); + break; + case SDLK_DOWN: + DlgFileSelect_ScrollDown(); + break; + case SDLK_HOME: + ypos = 0; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + break; + case SDLK_END: + ypos = entries-SGFS_NUMENTRIES; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + break; + case SDLK_PAGEUP: + ypos -= SGFS_NUMENTRIES; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + break; + case SDLK_PAGEDOWN: + if (ypos+2*SGFS_NUMENTRIES < entries) + ypos += SGFS_NUMENTRIES; + else + ypos = entries-SGFS_NUMENTRIES; + DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(); + break; + default: + break; + } + break; + default: + break; + } + + if (ypos < 0) { + ypos = 0; + scrollbar_Ypos = 0.0; + } + + if (ypos != oldypos) + refreshentries = true; +} + + +/*-----------------------------------------------------------------------*/ +/** + * Free file entries + */ +static struct dirent **files_free(struct dirent **files) +{ + int i; + if (files != NULL) + { + for(i=0; id_name; + const char *name2 = (*d2)->d_name; +#ifndef HAVE_DIRENT_D_TYPE + int type1 = DT_UNKNOWN; + int type2 = DT_UNKNOWN; +#else + int type1 = (*d1)->d_type; + int type2 = (*d2)->d_type; +#endif + + /* OS / file system that doesn't support d_type field, or symlink */ + if (type1 == DT_UNKNOWN || type1 == DT_LNK) + type1 = get_dtype(name1); + if (type2 == DT_UNKNOWN || type2 == DT_LNK) + type2 = get_dtype(name2); + + if (type1 == DT_DIR) + { + if (type2 != DT_DIR) + return -1; + } else if (type2 == DT_DIR) + { + if (type1 != DT_DIR) + return 1; + } + return strcasecmp(name1, name2); +} + +/*-----------------------------------------------------------------------*/ +/** + * Create and return suitable path into zip file + */ +static char* zip_get_path(const char *zipdir, const char *zipfilename, int browsingzip) +{ + if (browsingzip) + { + char *zippath; + zippath = malloc(strlen(zipdir) + strlen(zipfilename) + 1); + strcpy(zippath, zipdir); + strcat(zippath, zipfilename); + return zippath; + } + return strdup(""); +} + +/** + * string for zip root needs to be empty, check and correct if needed + */ +static void correct_zip_root(char *zippath) +{ + if (zippath[0] == PATHSEP && !zippath[1]) + { + zippath[0] = '\0'; + } +} + +/** + * Convert Ypos to Y scrollbar position + */ +static void DlgFileSelect_Convert_ypos_to_scrollbar_Ypos(void) +{ + if (entries <= SGFS_NUMENTRIES) + scrollbar_Ypos = 0.0; + else + scrollbar_Ypos = (float)ypos / ((float)entries/(float)(SGFS_NUMENTRIES-2)); +} + +/*-----------------------------------------------------------------------*/ +/** + * Show and process a file selection dialog. + * Returns path/name user selected or NULL if user canceled + * input: zip_path = pointer's pointer to buffer to contain file path + * within a selected zip file, or NULL if browsing zip files is disallowed. + * bAllowNew: true if the user is allowed to insert new file names. + */ +char* SDLGui_FileSelect(const char *title, const char *path_and_name, char **zip_path, bool bAllowNew) { - int i,n; - int entries = 0; /* How many files are in the actual directory? */ - int ypos = 0; struct dirent **files = NULL; char *pStringMem; + char *retpath = NULL; + const char *home; char *path, *fname; /* The actual file and path names */ - BOOL reloaddir = TRUE; /* Do we have to reload the directory file list? */ - BOOL refreshentries = TRUE; /* Do we have to update the file names in the dialog? */ - int retbut; - int oldcursorstate; - int selection = -1; /* The actual selection, -1 if none selected */ + bool reloaddir = true; /* Do we have to reload the directory file list? */ + int retbut, len; + bool bOldMouseVisibility; + int selection; /* The selection index */ char *zipfilename; /* Filename in zip file */ char *zipdir; - BOOL browsingzip = FALSE; /* Are we browsing an archive? */ + bool browsingzip = false; /* Are we browsing an archive? */ zip_dir *zipfiles = NULL; + SDL_Event sdlEvent; + int yScrollbar_size; /* Size of the vertical scrollbar */ + union { + char *mtxt; + const char *ctxt; + } dlgtitle; /* A hack to silent recent GCCs warnings */ + bool KeepCurrentObject; + + dlgtitle.ctxt = title; + + /* If this is the first call to SDLGui_FileSelect, we reset scrollbar_Ypos and ypos */ + /* Else, we keep the previous value of scrollbar_Ypos and update ypos below, to open */ + /* the fileselector at the same position it was used */ + if ( ypos < 0 ) + { + scrollbar_Ypos = 0.0; + ypos = 0; + } + + refreshentries = true; + entries = 0; /* Allocate memory for the file and path name strings: */ pStringMem = malloc(4 * FILENAME_MAX); path = pStringMem; fname = pStringMem + FILENAME_MAX; - zipfilename = pStringMem + 2 * FILENAME_MAX; - zipdir = pStringMem + 3 * FILENAME_MAX; - + zipdir = pStringMem + 2 * FILENAME_MAX; + zipfilename = pStringMem + 3 * FILENAME_MAX; zipfilename[0] = 0; + fname[0] = 0; + path[0] = 0; + + len = strlen(title); + fsdlg[SGFSDLG_TITLE].txt = dlgtitle.mtxt; + fsdlg[SGFSDLG_TITLE].x = TITLE_OFFSET + (TITLE_MAXLEN-len)/2; + fsdlg[SGFSDLG_TITLE].w = len; + + /* Save mouse state and enable cursor */ + bOldMouseVisibility = SDL_ShowCursor(SDL_QUERY); + SDL_ShowCursor(SDL_ENABLE); SDLGui_CenterDlg(fsdlg); if (bAllowNew) @@ -179,118 +625,151 @@ int SDLGui_FileSelect(char *path_and_nam } /* Prepare the path and filename variables */ - File_splitpath(path_and_name, path, fname, NULL); + if (path_and_name && path_and_name[0]) + { + strncpy(path, path_and_name, FILENAME_MAX); + path[FILENAME_MAX-1] = '\0'; + } + if (!File_DirExists(path)) + { + File_SplitPath(path, path, fname, NULL); + if (!(File_DirExists(path) || getcwd(path, FILENAME_MAX))) + { + perror("SDLGui_FileSelect: non-existing path and CWD failed"); + goto clean_exit; + } + } + + File_MakeAbsoluteName(path); + File_MakeValidPathName(path); File_ShrinkName(dlgpath, path, DLGPATH_SIZE); File_ShrinkName(dlgfname, fname, DLGFNAME_SIZE); - /* Save old mouse cursor state and enable cursor anyway */ - oldcursorstate = SDL_ShowCursor(SDL_QUERY); - if (oldcursorstate == SDL_DISABLE) - SDL_ShowCursor(SDL_ENABLE); + /* The first time we display the dialog, we reset the current position */ + /* On next calls, current_object's value will be kept to handle scrolling */ + KeepCurrentObject = false; do { if (reloaddir) { - if (strlen(path) >= FILENAME_MAX) - { - fprintf(stderr, "SDLGui_FileSelect: Path name too long!\n"); - free(pStringMem); - return FALSE; - } - - /* Free old allocated memory: */ - if (files != NULL) - { - for(i=0; i