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"
26 #include "floppydrive.h"
33 // Icons, in GIMP "C" format
34 #include "gfx/scroll-left.c"
35 #include "gfx/scroll-right.c"
41 unsigned int bytesPerPixel; // 3:RGB, 4:RGBA
42 unsigned char pixelData[];
45 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 };
49 #define SCROLL_HOT_WIDTH 48
50 #define DS_XPOS ((VIRTUAL_SCREEN_WIDTH - DS_WIDTH) / 2)
51 #define DS_YPOS ((VIRTUAL_SCREEN_HEIGHT - DS_HEIGHT) / 2)
56 int diskSelectorState = DSS_HIDDEN;
57 int diskSelected = -1;
58 int lastDiskSelected = -1;
63 int rsbPos = DS_WIDTH;
65 int textScrollCount = 0;
69 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.
73 // We make provision for sets of 32 or less...
75 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.
79 uint8_t num; // # of disks in this set
80 std::string name; // The name of this disk set
81 // std::string fullPath; // The path to the containing folder
82 std::string image[32]; // List of disk images in this set
83 std::string imgName[32];// List of human readable names of disk images
84 uint32_t crc[32]; // List of CRC32s of the disk images in the set
85 uint32_t crcFound[32]; // List of CRC32s actually discovered on filesystem
92 // Struct to hold filenames & full paths to same
100 // FileStruct(): diskSet(NULL) {}
101 // ~FileStruct() { if (diskSet != NULL) delete diskSet; }
103 // Functor, to presumably make the std::sort go faster
104 bool operator()(const FileStruct & a, const FileStruct & b) const
106 return (strcasecmp(a.image.c_str(), b.image.c_str()) < 0 ? true : false);
111 static SDL_Texture * window = NULL;
112 static SDL_Texture * charStamp = NULL;
113 static uint32_t windowPixels[DS_WIDTH * DS_HEIGHT];
114 static uint32_t stamp[FONT_WIDTH * FONT_HEIGHT];
115 SDL_Texture * scrollLeftIcon = NULL;
116 SDL_Texture * scrollRightIcon = NULL;
117 bool DiskSelector::showWindow = false;
118 std::vector<FileStruct> fsList;
121 void DiskSelector::Init(SDL_Renderer * renderer)
123 window = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888,
124 SDL_TEXTUREACCESS_TARGET, DS_WIDTH, DS_HEIGHT);
125 charStamp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
126 SDL_TEXTUREACCESS_TARGET, FONT_WIDTH, FONT_HEIGHT);
130 WriteLog("GUI (DiskSelector): Could not create window!\n");
134 if (SDL_SetTextureBlendMode(window, SDL_BLENDMODE_BLEND) == -1)
135 WriteLog("GUI (DiskSelector): Could not set blend mode for window.\n");
137 if (SDL_SetTextureBlendMode(charStamp, SDL_BLENDMODE_BLEND) == -1)
138 WriteLog("GUI (DiskSelector): Could not set blend mode for charStamp.\n");
140 scrollLeftIcon = GUI::CreateTexture(renderer, &scroll_left);
141 scrollRightIcon = GUI::CreateTexture(renderer, &scroll_right);
143 for(uint32_t i=0; i<DS_WIDTH*DS_HEIGHT; i++)
144 windowPixels[i] = 0xEF007F00;
146 SDL_UpdateTexture(window, NULL, windowPixels, 128 * sizeof(Uint32));
148 DrawFilenames(renderer);
153 // Find all disks images top level call
155 void DiskSelector::FindDisks(void)
158 FindDisks(settings.disksPath);
159 std::sort(fsList.begin(), fsList.end(), FileStruct());
160 // Calculate the number of columns in the file selector...
161 numColumns = (int)ceilf((float)fsList.size() / 27.0f);
162 WriteLog("GUI (DiskSelector)::FindDisks(): # of columns is %i (%i files)\n", numColumns, fsList.size());
166 OK, so the way that you can determine if a file is a directory in a cross-platform way is to do an opendir() call on a discovered filename. If it returns NULL, then it's a regular file and not a directory. Though I think the 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 uint8_t * DiskSelector::ReadFile(const char * filename, uint32_t * size)
346 FILE * fp = fopen(filename, "r");
351 fseek(fp, 0, SEEK_END);
353 fseek(fp, 0, SEEK_SET);
355 uint8_t * buffer = (uint8_t *)malloc(*size);
356 fread(buffer, 1, *size, fp);
363 bool DiskSelector::HasLegalExtension(const char * name)
365 // Find the file's extension, if any
366 const char * ext = strrchr(name, '.');
368 // No extension, so fuggetaboutit
372 // Otherwise, look for a legal extension
373 // We should be smarter than this, and look at headers & file sizes instead
374 if ((strcasecmp(ext, ".dsk") == 0)
375 || (strcasecmp(ext, ".do") == 0)
376 || (strcasecmp(ext, ".po") == 0)
377 || (strcasecmp(ext, ".woz") == 0))
384 void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
386 if (SDL_SetRenderTarget(renderer, window) < 0)
388 WriteLog("GUI: Could not set Render Target to overlay... (%s)\n", SDL_GetError());
392 // 3 columns of 16 chars apiece (with 8X16 font), 18 rows
393 // 3 columns of 18 chars apiece (with 7X12 font), 24 rows
394 // 3 columns of 21 chars apiece (with 6X11 font), 27 rows
396 unsigned int count = 0;
397 unsigned int fsStart = colStart * 27;
400 // Draw partial columns (for scrolling left/right)
401 // [could probably combine these...]
402 if (textScrollCount < 0)
404 int partialColStart = (colStart - 1) * 27;
405 offset = -1 * textScrollCount;
407 for(unsigned int y=0; y<27; y++)
409 for(unsigned int i=22+textScrollCount, x=0; i<21; i++, x++)
411 if (i >= fsList[partialColStart + y].image.length())
414 DrawCharacter(renderer, x + 1, y + 1, fsList[partialColStart + y].image[i], false);
418 else if (textScrollCount > 0)
420 offset = 22 - textScrollCount;
422 for(unsigned int y=0; y<27; y++)
424 for(unsigned int i=textScrollCount, x=0; i<21; i++, x++)
426 if (i >= fsList[fsStart + y].image.length())
429 DrawCharacter(renderer, x + 1, y + 1, fsList[fsStart + y].image[i], false);
436 while (fsStart < fsList.size())
438 // int currentX = (count / 18) * 17;
439 // int currentY = (count % 18);
440 // int currentX = (count / 24) * 19;
441 // int currentY = (count % 24);
442 int currentX = (count / 27) * 22;
443 int currentY = (count % 27);
445 // for(unsigned int i=0; i<16; i++)
446 // for(unsigned int i=0; i<18; i++)
447 for(unsigned int i=0; i<21; i++)
449 if (i >= fsList[fsStart].image.length())
452 bool invert = (diskSelected == (int)fsStart ? true : false);
453 DrawCharacter(renderer, currentX + i + 1 + offset, currentY + 1, fsList[fsStart].image[i], invert);
459 // if (count >= (18 * 3))
460 // if (count >= (24 * 3))
461 if (count >= (27 * 3))
465 // If a disk is selected, show it on the top line in inverse video
466 if (diskSelected > -1)
468 for(unsigned int i=0; i<65; i++)
470 if (i >= fsList[diskSelected].image.length())
473 DrawCharacter(renderer, i + 1, 0, fsList[diskSelected].image[i], true);
477 // Set render target back to default
478 SDL_SetRenderTarget(renderer, NULL);
482 void DiskSelector::DrawCharacter(SDL_Renderer * renderer, int x, int y, uint8_t c, bool invert/*=false*/)
484 uint32_t inv = (invert ? 0x000000FF : 0x00000000);
485 uint32_t pixel = 0xFFFFC000; // RRGGBBAA
486 uint8_t * ptr = (uint8_t *)&font10pt[(c - 0x20) * FONT_WIDTH * FONT_HEIGHT];
488 dst.x = x * FONT_WIDTH, dst.y = y * FONT_HEIGHT, dst.w = FONT_WIDTH, dst.h = FONT_HEIGHT;
490 for(int i=0; i<FONT_WIDTH*FONT_HEIGHT; i++)
491 stamp[i] = (pixel | ptr[i]) ^ inv;
493 SDL_UpdateTexture(charStamp, NULL, stamp, FONT_WIDTH * sizeof(Uint32));
494 SDL_RenderCopy(renderer, charStamp, NULL, &dst);
498 void DiskSelector::ShowWindow(int drive)
500 diskSelectorState = DSS_SHOWN;
507 void DiskSelector::MouseDown(int32_t x, int32_t y, uint32_t buttons)
509 if (!showWindow || !entered)
512 if ((diskSelectorState == DSS_LSB_SHOWING) || (diskSelectorState == DSS_LSB_SHOWN))
515 textScrollCount = 21;
519 diskSelectorState = DSS_LSB_HIDING;
526 if ((diskSelectorState == DSS_RSB_SHOWING) || (diskSelectorState == DSS_RSB_SHOWN))
529 textScrollCount = -21;
531 if ((colStart + 3) == numColumns)
533 diskSelectorState = DSS_RSB_HIDING;
540 if (diskSelected != -1)
542 floppyDrive[0].LoadImage(fsList[diskSelected].fullPath.c_str(), driveNumber);
549 void DiskSelector::MouseUp(int32_t x, int32_t y, uint32_t buttons)
557 void DiskSelector::MouseMove(int32_t x, int32_t y, uint32_t buttons)
562 // Check to see if DS has been hovered yet, and, if so, set a flag to show
564 if (!entered && ((x >= DS_XPOS) && (x <= (DS_XPOS + DS_WIDTH))
565 && (y >= DS_YPOS) && (y <= (DS_YPOS + DS_HEIGHT))))
568 // Check to see if the DS, since being hovered, is now no longer being
570 //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...
571 if (entered && ((x < DS_XPOS) || (x > (DS_XPOS + DS_WIDTH))
572 || (y < DS_YPOS) || (y > (DS_YPOS + DS_HEIGHT))))
574 diskSelectorState = DSS_HIDDEN;
584 // Bail out if the DS hasn't been entered yet
590 +-----+---------------------+-----+
593 +-----+---------------------+-----+
595 | | x is here and state is DSS_SHOWN
596 | x is here and state is DSS_LSB_SHOWING or DSS_RSB_SHOWING
597 x is here and state is DSS_SHOWN
600 if (x < (DS_XPOS + SCROLL_HOT_WIDTH))
602 if ((colStart > 0) && (diskSelectorState == DSS_SHOWN))
604 diskSelectorState = DSS_LSB_SHOWING;
608 else if (x > (DS_XPOS + DS_WIDTH - SCROLL_HOT_WIDTH))
610 if (((colStart + 3) < numColumns) && (diskSelectorState == DSS_SHOWN))
612 diskSelectorState = DSS_RSB_SHOWING;
618 // Handle the excluded middle :-P
619 if ((diskSelectorState == DSS_LSB_SHOWING)
620 || (diskSelectorState == DSS_LSB_SHOWN))
622 diskSelectorState = DSS_LSB_HIDING;
625 else if ((diskSelectorState == DSS_RSB_SHOWING)
626 || (diskSelectorState == DSS_RSB_SHOWN))
628 diskSelectorState = DSS_RSB_HIDING;
633 // The -1 terms move the origin to the upper left corner (from 1 in, and 1
635 int xChar = ((x - DS_XPOS) / FONT_WIDTH) - 1;
636 int yChar = ((y - DS_YPOS) / FONT_HEIGHT) - 1;
637 diskSelected = ((xChar / 22) * 27) + yChar + (colStart * 27);
639 if ((yChar < 0) || (yChar >= 27)
640 || (diskSelected >= (int)fsList.size())
641 || (diskSelectorState == DSS_LSB_SHOWING)
642 || (diskSelectorState == DSS_LSB_SHOWN)
643 || (diskSelectorState == DSS_RSB_SHOWING)
644 || (diskSelectorState == DSS_RSB_SHOWN))
647 if (diskSelected != lastDiskSelected)
649 HandleSelection(sdlRenderer);
650 lastDiskSelected = diskSelected;
655 void DiskSelector::HandleGUIState(void)
660 if ((lsbPos > (SCROLL_HOT_WIDTH - 40)) && (diskSelectorState == DSS_LSB_SHOWING))
662 diskSelectorState = DSS_LSB_SHOWN;
663 lsbPos = SCROLL_HOT_WIDTH - 40;
666 else if ((lsbPos < -40) && (diskSelectorState == DSS_LSB_HIDING))
668 diskSelectorState = DSS_SHOWN;
672 else if ((rsbPos < (DS_WIDTH - SCROLL_HOT_WIDTH)) && (diskSelectorState == DSS_RSB_SHOWING))
674 diskSelectorState = DSS_RSB_SHOWN;
675 rsbPos = DS_WIDTH - SCROLL_HOT_WIDTH;
678 else if ((rsbPos > DS_WIDTH) && (diskSelectorState == DSS_RSB_HIDING))
680 diskSelectorState = DSS_SHOWN;
685 if (textScrollCount < 0)
687 textScrollCount += 2;
689 if (textScrollCount > 0)
695 else if (textScrollCount > 0)
697 textScrollCount -= 2;
699 if (textScrollCount < 0)
708 void DiskSelector::HandleSelection(SDL_Renderer * renderer)
710 SDL_UpdateTexture(window, NULL, windowPixels, 128 * sizeof(Uint32));
711 DrawFilenames(renderer);
716 void DiskSelector::Render(SDL_Renderer * renderer)
718 if (!(window && showWindow))
723 if (((diskSelectorState != DSS_LSB_SHOWN)
724 && (diskSelectorState != DSS_RSB_SHOWN)
725 && (diskSelectorState != DSS_SHOWN))
726 || (textScrollCount != 0) || refresh)
727 HandleSelection(renderer);
729 // Render scroll arrows (need to figure out why no alpha!)
730 SDL_SetRenderTarget(renderer, window);
731 SDL_Rect dst2 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
733 SDL_RenderCopy(renderer, scrollLeftIcon, NULL, &dst2);
734 SDL_Rect dst3 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
736 SDL_RenderCopy(renderer, scrollRightIcon, NULL, &dst3);
737 SDL_SetRenderTarget(renderer, NULL);
739 SDL_Rect dst = { DS_XPOS, DS_YPOS, DS_WIDTH, DS_HEIGHT };
740 SDL_RenderCopy(renderer, window, NULL, &dst);