4 // Floppy disk selector GUI
6 // © 2014-2018 Underground Software
8 // JLH = James Hammons <jlhamm@acm.org>
11 // --- ---------- -----------------------------------------------------------
12 // JLH 10/13/2013 Created this file
16 // - Fix bug where hovering on scroll image causes it to fly across the screen
20 #include "diskselector.h"
27 #include "floppydrive.h"
34 // Icons, in GIMP "C" format
35 #include "gfx/scroll-left.c"
36 #include "gfx/scroll-right.c"
42 unsigned int bytesPerPixel; // 3:RGB, 4:RGBA
43 unsigned char pixelData[];
46 enum { DSS_SHOWING, DSS_HIDING, DSS_SHOWN, DSS_HIDDEN, DSS_LSB_SHOWING, DSS_LSB_SHOWN, DSS_LSB_HIDING, DSS_RSB_SHOWING, DSS_RSB_SHOWN, DSS_RSB_HIDING, DSS_TEXT_SCROLLING };
50 #define SCROLL_HOT_WIDTH 48
51 #define DS_XPOS ((VIRTUAL_SCREEN_WIDTH - DS_WIDTH) / 2)
52 #define DS_YPOS ((VIRTUAL_SCREEN_HEIGHT - DS_HEIGHT) / 2)
55 static bool entered = false;
56 static int driveNumber;
57 static int diskSelectorState = DSS_HIDDEN;
58 static int diskSelected = -1;
59 static int lastDiskSelected = -1;
60 static int numColumns;
61 static int colStart = 0;
62 static int dxLeft = 0;
63 static int dxRight = 0;
64 static int rsbPos = DS_WIDTH;
65 static int lsbPos = -40;
66 static int textScrollCount = 0;
67 static bool refresh = false;
70 So, how this will work for multiple columns, where the number of columns is greater than 3, is to have an arrow button pop up on the left or right hand side (putting the mouse on the left or right side of the disk selector activates (shows) the button, if such a move can be made. Button hides when the mouse moves out of the hot zone or when it has no more effect.
74 // We make provision for sets of 32 or less...
76 The way the manifests are laid out, we make the assumption that the boot disk of a set is always listed first. Therefore, image[0] will always be the boot disk.
80 uint8_t num; // # of disks in this set
81 std::string name; // The name of this disk set
82 // std::string fullPath; // The path to the containing folder
83 std::string image[32]; // List of disk images in this set
84 std::string imgName[32];// List of human readable names of disk images
85 uint32_t crc[32]; // List of CRC32s of the disk images in the set
86 uint32_t crcFound[32]; // List of CRC32s actually discovered on filesystem
93 // Struct to hold filenames & full paths to same
101 // FileStruct(): diskSet(NULL) {}
102 // ~FileStruct() { if (diskSet != NULL) delete diskSet; }
104 // Functor, to presumably make the std::sort go faster
105 bool operator()(const FileStruct & a, const FileStruct & b) const
107 return (strcasecmp(a.image.c_str(), b.image.c_str()) < 0 ? true : false);
112 static SDL_Texture * window = NULL;
113 static uint32_t windowPixels[DS_WIDTH * DS_HEIGHT];
114 SDL_Texture * scrollLeftIcon = NULL;
115 SDL_Texture * scrollRightIcon = NULL;
116 bool DiskSelector::showWindow = false;
117 std::vector<FileStruct> fsList;
118 std::vector<FileStruct> hdList;
121 void DiskSelector::Init(SDL_Renderer * renderer)
123 window = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888,
124 SDL_TEXTUREACCESS_TARGET, DS_WIDTH, DS_HEIGHT);
128 WriteLog("GUI (DiskSelector): Could not create window!\n");
132 if (SDL_SetTextureBlendMode(window, SDL_BLENDMODE_BLEND) == -1)
133 WriteLog("GUI (DiskSelector): Could not set blend mode for window.\n");
135 scrollLeftIcon = GUI::CreateTexture(renderer, &scroll_left);
136 scrollRightIcon = GUI::CreateTexture(renderer, &scroll_right);
138 for(uint32_t i=0; i<DS_WIDTH*DS_HEIGHT; i++)
139 windowPixels[i] = 0xEF007F00;
141 SDL_UpdateTexture(window, NULL, windowPixels, DS_WIDTH * sizeof(Uint32));
144 DrawFilenames(renderer);
149 // Find all disks images top level call
151 void DiskSelector::FindDisks(void)
154 FindDisks(settings.disksPath);
155 std::sort(fsList.begin(), fsList.end(), FileStruct());
156 // Calculate the number of columns in the file selector...
157 numColumns = (int)ceilf((float)fsList.size() / 27.0f);
158 WriteLog("GUI (DiskSelector)::FindDisks(): # of columns is %i (%i files)\n", numColumns, fsList.size());
163 OK, so the way that you can determine if a file is a directory in a cross-
164 platform way is to do an opendir() call on a discovered filename. If it
165 returns NULL, then it's a regular file and not a directory. Though I think the
166 Linux method is more elegant. :-P
169 // Find all disks images within path (recursive call does depth first search)
171 void DiskSelector::FindDisks(const char * path)
173 DIR * dir = opendir(path);
177 WriteLog("GUI (DiskSelector)::FindDisks: Could not open directory \"%s\%!\n", path);
183 while ((ent = readdir(dir)) != NULL)
186 sprintf(buf, "%s/%s", path, ent->d_name);
188 // Cross-platform way to test if it's a directory...
189 DIR * test = opendir(buf);
191 // if ((ent->d_type == DT_REG) && HasLegalExtension(ent->d_name))
194 if (HasLegalExtension(ent->d_name))
197 fs.image = ent->d_name;
199 fsList.push_back(fs);
202 // else if (ent->d_type == DT_DIR)
205 // Make sure we close the thing, since it's a bona-fide dir!
208 // Only recurse if the directory is not one of the special ones...
209 if ((strcmp(ent->d_name, "..") != 0)
210 && (strcmp(ent->d_name, ".") != 0))
212 // Check to see if this is a special directory with a manifest
214 sprintf(buf2, "%s/manifest.txt", buf);
215 FILE * fp = fopen(buf2, "r");
217 // No manifest means it's just a regular directory...
222 // Read the manifest and all that good stuff
224 ReadManifest(fp, &fs.diskSet);
227 // Finally, check that the stuff in the manifest is
228 // actually in the directory...
229 if (CheckManifest(buf, &fs.diskSet) == true)
232 fs.image = fs.diskSet.name;
233 fsList.push_back(fs);
236 WriteLog("Manifest for '%s' failed check phase.\n", fs.diskSet.name.c_str());
238 printf("Name found: \"%s\" (%d)\nDisks:\n", fs.diskSet.name.c_str(), fs.diskSet.num);
239 for(int i=0; i<fs.diskSet.num; i++)
240 printf("%s (CRC: %08X)\n", fs.diskSet.image[i].c_str(), fs.diskSet.crc[i]);
252 void DiskSelector::ReadManifest(FILE * fp, DiskSet * ds)
260 fgets(line, 0x10000, fp);
263 if ((line[0] == '#') || (line[0] == '\n'))
264 ; // Do nothing with comments or blank lines...
271 if (strncmp(line, "diskset", 7) == 0)
273 sscanf(line, "diskset=\"%[^\"]\"", buf);
276 else if (strncmp(line, "disks", 5) == 0)
278 sscanf(line, "disks=%hhd", &ds->num);
280 else if (strncmp(line, "disk", 4) == 0)
282 int n = sscanf(line, "disk=%s %s (%s)", buf, crcbuf, altName);
284 if ((n == 2) || (n == 3))
286 ds->image[disksFound] = buf;
287 ds->crc[disksFound] = strtoul(crcbuf, NULL, 16);
291 ds->imgName[disksFound] = altName;
294 // Find the file's extension, if any
295 char * ext = strrchr(buf, '.');
297 // Kill the disk extension, if it exists
301 ds->imgName[disksFound] = buf;
305 WriteLog("Malformed disk descriptor in manifest at line %d\n", lineNo);
310 if (disksFound != ds->num)
311 WriteLog("Found only %d entries in manifest, expected %hhd\n", disksFound, ds->num);
315 bool DiskSelector::CheckManifest(const char * path, DiskSet * ds)
319 for(int i=0; i<ds->num; i++)
321 std::string filename = path;
323 filename += ds->image[i];
325 uint8_t * buf = ReadFile(filename.c_str(), &size);
329 ds->crcFound[i] = CRC32(buf, size);
333 if (ds->crc[i] != ds->crcFound[i])
335 WriteLog("Warning: Bad CRC32 for '%s'. Expected: %08X, found: %08X\n", ds->image[i], ds->crc[i], ds->crcFound[i]);
340 return (found == ds->num ? true : false);
344 bool DiskSelector::HasLegalExtension(const char * name)
346 // Find the file's extension, if any
347 const char * ext = strrchr(name, '.');
349 // No extension, so fuggetaboutit
353 // Otherwise, look for a legal extension
354 // We should be smarter than this, and look at headers & file sizes instead
355 if ((strcasecmp(ext, ".dsk") == 0)
356 || (strcasecmp(ext, ".do") == 0)
357 || (strcasecmp(ext, ".po") == 0)
358 || (strcasecmp(ext, ".woz") == 0))
366 // Find all disks images top level call
368 void DiskSelector::FindHardDisks(void)
371 FindHardDisks(settings.disksPath);
372 std::sort(hdList.begin(), hdList.end(), FileStruct());
373 WriteLog("GUI (DiskSelector)::FindHardDisks(): # of HDs is %i\n", hdList.size());
378 // Find all hard disk images within path (recursive call does depth first search)
380 void DiskSelector::FindHardDisks(const char * path)
382 DIR * dir = opendir(path);
386 WriteLog("GUI (DiskSelector)::FindHardDisks: Could not open directory \"%s\%!\n", path);
392 while ((ent = readdir(dir)) != NULL)
395 sprintf(buf, "%s/%s", path, ent->d_name);
397 // Cross-platform way to test if it's a directory (test = NULL -> file)
398 DIR * test = opendir(buf);
402 const char * ext = strrchr(ent->d_name, '.');
405 && ((strcasecmp(ext, ".2mg") == 0)
406 || (strcasecmp(ext, ".hdv") == 0)))
409 fs.image = ent->d_name;
411 hdList.push_back(fs);
416 // Make sure we close the thing, since it's a bona-fide dir!
419 // Only recurse if the directory is not one of the special ones...
420 if ((strcmp(ent->d_name, "..") != 0)
421 && (strcmp(ent->d_name, ".") != 0))
432 void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
434 if (SDL_SetRenderTarget(renderer, window) < 0)
436 WriteLog("GUI: Could not set Render Target to overlay... (%s)\n", SDL_GetError());
440 // 3 columns of 16 chars apiece (with 8X16 font), 18 rows
441 // 3 columns of 18 chars apiece (with 7X12 font), 24 rows
442 // 3 columns of 21 chars apiece (with 6X11 font), 27 rows
444 unsigned int count = 0;
445 unsigned int fsStart = colStart * 27;
448 // Draw partial columns (for scrolling left/right)
449 // [could probably combine these...]
450 if (textScrollCount < 0)
452 int partialColStart = (colStart - 1) * 27;
453 offset = -1 * textScrollCount;
455 for(unsigned int y=0; y<27; y++)
457 for(unsigned int i=22+textScrollCount, x=0; i<21; i++, x++)
459 if (i >= fsList[partialColStart + y].image.length())
462 GUI::DrawCharacter(renderer, x + 1, y + 1, fsList[partialColStart + y].image[i], false);
466 else if (textScrollCount > 0)
468 offset = 22 - textScrollCount;
470 for(unsigned int y=0; y<27; y++)
472 for(unsigned int i=textScrollCount, x=0; i<21; i++, x++)
474 if (i >= fsList[fsStart + y].image.length())
477 GUI::DrawCharacter(renderer, x + 1, y + 1, fsList[fsStart + y].image[i], false);
484 while (fsStart < fsList.size())
486 // int currentX = (count / 18) * 17;
487 // int currentY = (count % 18);
488 // int currentX = (count / 24) * 19;
489 // int currentY = (count % 24);
490 int currentX = (count / 27) * 22;
491 int currentY = (count % 27);
493 // for(unsigned int i=0; i<16; i++)
494 // for(unsigned int i=0; i<18; i++)
495 for(unsigned int i=0; i<21; i++)
497 if (i >= fsList[fsStart].image.length())
500 bool invert = (diskSelected == (int)fsStart ? true : false);
501 GUI::DrawCharacter(renderer, currentX + i + 1 + offset, currentY + 1, fsList[fsStart].image[i], invert);
507 // if (count >= (18 * 3))
508 // if (count >= (24 * 3))
509 if (count >= (27 * 3))
513 // If a disk is selected, show it on the top line in inverse video
514 if (diskSelected > -1)
516 for(unsigned int i=0; i<65; i++)
518 if (i >= fsList[diskSelected].image.length())
521 GUI::DrawCharacter(renderer, i + 1, 0, fsList[diskSelected].image[i], true);
525 // Set render target back to default
526 SDL_SetRenderTarget(renderer, NULL);
530 void DiskSelector::ShowWindow(int drive)
532 diskSelectorState = DSS_SHOWN;
539 void DiskSelector::HideWindow(void)
541 diskSelectorState = DSS_HIDDEN;
551 void DiskSelector::MouseDown(int32_t x, int32_t y, uint32_t buttons)
553 if (!showWindow || !entered)
556 if ((diskSelectorState == DSS_LSB_SHOWING) || (diskSelectorState == DSS_LSB_SHOWN))
559 textScrollCount = 21;
563 diskSelectorState = DSS_LSB_HIDING;
570 if ((diskSelectorState == DSS_RSB_SHOWING) || (diskSelectorState == DSS_RSB_SHOWN))
573 textScrollCount = -21;
575 if ((colStart + 3) == numColumns)
577 diskSelectorState = DSS_RSB_HIDING;
584 if (diskSelected != -1)
586 floppyDrive[0].LoadImage(fsList[diskSelected].fullPath.c_str(), driveNumber);
593 void DiskSelector::MouseUp(int32_t x, int32_t y, uint32_t buttons)
601 void DiskSelector::MouseMove(int32_t x, int32_t y, uint32_t buttons)
606 // Check to see if DS has been hovered yet, and, if so, set a flag to show
608 if (!entered && ((x >= DS_XPOS) && (x <= (DS_XPOS + DS_WIDTH))
609 && (y >= DS_YPOS) && (y <= (DS_YPOS + DS_HEIGHT))))
612 // Check to see if the DS, since being hovered, is now no longer being
614 //N.B.: Should probably make like a 1/2 to 1 second timeout to allow for overshooting the edge of the thing, maybe have the window fade out gradually and let it come back if you enter before it leaves...
615 if (entered && ((x < DS_XPOS) || (x > (DS_XPOS + DS_WIDTH))
616 || (y < DS_YPOS) || (y > (DS_YPOS + DS_HEIGHT))))
618 diskSelectorState = DSS_HIDDEN;
628 // Bail out if the DS hasn't been entered yet
634 +-----+---------------------+-----+
637 +-----+---------------------+-----+
639 | | x is here and state is DSS_SHOWN
640 | x is here and state is DSS_LSB_SHOWING or DSS_RSB_SHOWING
641 x is here and state is DSS_SHOWN
644 if (x < (DS_XPOS + SCROLL_HOT_WIDTH))
646 if ((colStart > 0) && (diskSelectorState == DSS_SHOWN))
648 diskSelectorState = DSS_LSB_SHOWING;
652 else if (x > (DS_XPOS + DS_WIDTH - SCROLL_HOT_WIDTH))
654 if (((colStart + 3) < numColumns) && (diskSelectorState == DSS_SHOWN))
656 diskSelectorState = DSS_RSB_SHOWING;
662 // Handle the excluded middle :-P
663 if ((diskSelectorState == DSS_LSB_SHOWING)
664 || (diskSelectorState == DSS_LSB_SHOWN))
666 diskSelectorState = DSS_LSB_HIDING;
669 else if ((diskSelectorState == DSS_RSB_SHOWING)
670 || (diskSelectorState == DSS_RSB_SHOWN))
672 diskSelectorState = DSS_RSB_HIDING;
677 // The -1 terms move the origin to the upper left corner (from 1 in, and 1
679 int xChar = ((x - DS_XPOS) / FONT_WIDTH) - 1;
680 int yChar = ((y - DS_YPOS) / FONT_HEIGHT) - 1;
681 diskSelected = ((xChar / 22) * 27) + yChar + (colStart * 27);
683 if ((yChar < 0) || (yChar >= 27)
684 || (diskSelected >= (int)fsList.size())
685 || (diskSelectorState == DSS_LSB_SHOWING)
686 || (diskSelectorState == DSS_LSB_SHOWN)
687 || (diskSelectorState == DSS_RSB_SHOWING)
688 || (diskSelectorState == DSS_RSB_SHOWN))
691 if (diskSelected != lastDiskSelected)
693 HandleSelection(sdlRenderer);
694 lastDiskSelected = diskSelected;
699 void DiskSelector::HandleGUIState(void)
704 if ((lsbPos > (SCROLL_HOT_WIDTH - 40)) && (diskSelectorState == DSS_LSB_SHOWING))
706 diskSelectorState = DSS_LSB_SHOWN;
707 lsbPos = SCROLL_HOT_WIDTH - 40;
710 else if ((lsbPos < -40) && (diskSelectorState == DSS_LSB_HIDING))
712 diskSelectorState = DSS_SHOWN;
716 else if ((rsbPos < (DS_WIDTH - SCROLL_HOT_WIDTH)) && (diskSelectorState == DSS_RSB_SHOWING))
718 diskSelectorState = DSS_RSB_SHOWN;
719 rsbPos = DS_WIDTH - SCROLL_HOT_WIDTH;
722 else if ((rsbPos > DS_WIDTH) && (diskSelectorState == DSS_RSB_HIDING))
724 diskSelectorState = DSS_SHOWN;
729 if (textScrollCount < 0)
731 textScrollCount += 2;
733 if (textScrollCount > 0)
739 else if (textScrollCount > 0)
741 textScrollCount -= 2;
743 if (textScrollCount < 0)
752 void DiskSelector::HandleSelection(SDL_Renderer * renderer)
754 SDL_UpdateTexture(window, NULL, windowPixels, DS_WIDTH * sizeof(Uint32));
755 DrawFilenames(renderer);
760 void DiskSelector::Render(SDL_Renderer * renderer)
762 if (!(window && showWindow))
767 if (((diskSelectorState != DSS_LSB_SHOWN)
768 && (diskSelectorState != DSS_RSB_SHOWN)
769 && (diskSelectorState != DSS_SHOWN))
770 || (textScrollCount != 0) || refresh)
771 HandleSelection(renderer);
773 // Render scroll arrows (need to figure out why no alpha!)
774 SDL_SetRenderTarget(renderer, window);
775 SDL_Rect dst2 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
777 SDL_RenderCopy(renderer, scrollLeftIcon, NULL, &dst2);
778 SDL_Rect dst3 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
780 SDL_RenderCopy(renderer, scrollRightIcon, NULL, &dst3);
781 SDL_SetRenderTarget(renderer, NULL);
783 SDL_Rect dst = { DS_XPOS, DS_YPOS, DS_WIDTH, DS_HEIGHT };
784 SDL_RenderCopy(renderer, window, NULL, &dst);