PureBasic ImagePickerGadget() - A Cross-Platform Solution

Share your advanced PureBasic knowledge/code with the community.
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

Here's a cross-platform, lightweight, module-based image picker gadget, that will display images from a source folder onto any container for viewing and selection.


* the fully-functional code for this screenshot (including the sample images) can be found in the second post of this thread (linked).
Image
Gadget Features

  1. blackboxed module design with dynamically-created child gadgets and bound event handling - no risk of namespace clashes
  2. canvas-based and lightweight, utilising only 2 canvas gadgets and 1 window with 1 image gadget for previews
  3. multiple instances of the gadget can be implemented simultaneously within the same namespace/application
  4. supported containers include windows, container gadgets, canvas gadgets (as container), and panel gadgets
  5. supported images types include BMP, JPG, PNG, GIF, TIFF - image libraries loaded dynamically only as required
  6. the maximum number of images that can be loaded is limited by system resources and canvas limitations (32767 pixels)
  7. the gadget can be dropped onto any panel or multiple panels of the panel gadget, each with variable settings
  8. two modes of implementation - basic (with only 3 parameters) and advanced (with 3 + 10 optional parameters)
  9. the thumbnails can be sized and spaced as desired, including options for border & background colours and title display
  10. selected images can be displayed with the built-in previewer (auto-orientation) or returned to the calling application
  11. natively-drawn sizeable scroll bar - requires no additional image assets (supports vertical scrolling only)
  12. features a busy notification window when loading big or large number of images
  13. scrolling can be effected by:
  • the mouse wheel
  • dragging the scroll bar button
  • clicking on any part of the scroll bar to jump to selection
  • touching & dragging on any part of the container (touch-responsive)

ImagePickerGadget.pbi Module

Here's the module for the image picker gadget (ImagePickerGadget.pbi):

Code: Select all

;====================================================================
;   
;   Cross-Platform Canvas-Based Image Picker Module
;   
;   features:
;   1.  blackboxed module design 
;   2.  canvas-based and lightweight
;   3.  fully-contained event handling
;   4.  multiple instances simultaneously
;   5.  basic implementation mode (3 parameters only)
;   6.  advanced implementation (3 + 10 optional parameters)
;   7.  supports BMP, JPG, PNG, GIF, TIFF image types
;   8.  supports different sized images and orientations
;   9.  image libraries loaded dynamically as required
;   10. loadable image count contingent on canvas limitations
;   11. busy notification window when images are still loading
;   12. custom-sizable thumbnails with border, background, title
;   13. custom-sizable image preview window (auto-orientation)
;   14. option to display or return selected images
;   15. custom-drawn scroll bar w/sizable width
;   16. scroll functions (supports vertical scrolling only):
;       - mouse wheel
;       - scroll bar button
;       - jump-to on scroll bar
;       - touch & drag on container
;   17. can be implemented onto these containers:
;       - windows
;       - container gadgets
;       - canvas gadgets (with container flag)
;       - panel gadgets (on multiple panels)
;        
;   tested & working with PureBasic v6.12 LTS (x64) on:
;   1. Windows 8.1, 10 (Intel)
;   2. Windows 11 (Intel & ARM)
;   3. Ubuntu Linux 20.04, 24.10 (Intel & ARM)
;   4. macOS High Sierra, Catalina (Intel)
;   5. macOS Sonoma (Silicon M1)
;
;   special thanks to PureBasic forum users:
;   1. @deseven for providing a workaround for an image-clipping
;      issue affecting later versions of macOS (line 220)
;   2. @HeXOR for identifying an error that occurs when the
;      canvas size limit of 32767 is exceeded (line 343)
;   3. @AZJIO for identifying an error that occurs when the image
;      folder is missing a trailing path separator (line 159)
;
;   by TI-994A - free to use, improve, share...
;
;   22nd November 2024 - Singapore
;
;====================================================================

DeclareModule ImagePicker
  
  #ImageSelected = #PB_Event_FirstCustomValue
  
  Define parent, panelIndex, scrollerWidth, selectedImage, previewSelectedImage, previewImageSize, thumbnailSize, thumbnailSpacing, thumbnailTitle
  Define thumbnailBorder = -1, thumbnailBackground = -1, gadgetBackground = -1
  Define.s selectedImageName, imagesFolder
  
  Declare initImagePicker()  
  Declare ImagePicker(parent, imagesFolder.s, previewSelectedImage)    
  
EndDeclareModule

Module ImagePicker   
  EnableExplicit
  
  Structure coords
    x.d
    y.d
  EndStructure  
  
  Structure img
    imgName.s
    imgNum.i
    imgX1.d
    imgX2.d
    imgY1.d
    imgY2.d
  EndStructure
  
  Structure imagePickerInstance
    click.coords
    scroll.coords
    canvas.i
    scroller.i
    scrollerWidth.i
    scrollerVisible.i
    scrollSpeed.i
    scrollFactor.d     
    parent.i
    parentWidth.i
    parentHeight.i
    panelIndex.i
    previewImageWindow.i    
    previewImageViewer.i      
    thumbnailSize.i
    thumbnailSpacing.i
    thumbnailTitle.i
    thumbnailBorder.i
    thumbnailBackground.i    
    previewSelectedImage.i
    previewImageSize.i
    gadgetBackground.i
    imagesFolder.s    
    Array images.img(0)     
  EndStructure
  
  Global Dim picker.imagePickerInstance(0) 
  Global index
  
  Declare ImagePicker(x, y.s, z)
  Declare initImagePicker()
  Declare windowHandler()  
  Declare gadgetHandler()  
  Declare loadImages()
  Declare hourGlass()
  Declare displayImage(x)  
  Declare scrollCanvas(x, y)
  Declare drawScroller(*x.coords)  
  Declare checkClickPoint(*x.coords)  
  Declare validateDirectory(x.s)
  
  Macro IIf(a, b, c)
    If Bool(a):b:Else:c:EndIf
  EndMacro
  
  #canvasMaxSize = 32767
  
  Procedure ImagePicker(_parent, _imagesFolder.s, _previewSelectedImage)    
    Shared parent, imagesFolder, previewSelectedImage
    
    parent = _parent
    imagesFolder = _imagesFolder
    previewSelectedImage = _previewSelectedImage    
    
    initImagePicker()
    
  EndProcedure
  
  Procedure initImagePicker()    
    Shared parent, panelIndex, scrollerWidth, previewSelectedImage, previewImageSize, gadgetBackground, thumbnailSize, thumbnailSpacing, thumbnailTitle, thumbnailBorder, thumbnailBackground, imagesFolder.s
    Protected parentIsWindow, selectedPanel, invalidParameters.s
    Static init    
    
    CompilerIf #PB_Compiler_Version => 603
      If Not init
        CompilerIf #PB_Compiler_DPIAware
          MessageRequester("Image Picker Gadget:", "The gadget does not support DPI awareness. Please turn off the option for best results.")
        CompilerEndIf
      EndIf
    CompilerEndIf
    
    If parent = #Null Or (Not IsWindow(parent) And Not IsGadget(parent))
      invalidParameters = "parent value"
    EndIf
    
    If Right(imagesFolder, 1) <> #PS$
      imagesFolder + #PS$
    EndIf
    
    If imagesFolder = #Null$ Or Not validateDirectory(imagesFolder)
      If invalidParameters <> #Null$
        invalidParameters + ", "
      EndIf
      invalidParameters + "images folder"
    EndIf
    
    If Trim(invalidParameters) <> #Null$
      MessageRequester("Image Picker Gadget:", "The following parameters are invalid: " + invalidParameters)
      CloseGadgetList()
      ProcedureReturn
    EndIf          
    
    If init
      index + 1      
    EndIf          
    init = #True     
    
    ReDim picker(index)   
    picker(index)\parent = parent
    picker(index)\panelIndex = panelIndex
    picker(index)\imagesFolder = imagesFolder   
    picker(index)\thumbnailTitle = thumbnailTitle    
    picker(index)\previewSelectedImage = previewSelectedImage
    
    IIf(scrollerWidth <= 0, picker(index)\scrollerWidth = 10, picker(index)\scrollerWidth = scrollerWidth)
    IIF(thumbnailSize <= 0, picker(index)\thumbnailSize = 100, picker(index)\thumbnailSize = thumbnailSize)
    IIF(thumbnailSpacing <= 0, picker(index)\thumbnailSpacing = 10, picker(index)\thumbnailSpacing = thumbnailSpacing)            
    IIF(thumbnailBorder < 0, picker(index)\thumbnailBorder = #White, picker(index)\thumbnailBorder = thumbnailBorder)            
    IIF(thumbnailBackground < 0, picker(index)\thumbnailBackground = #Black, picker(index)\thumbnailBackground = thumbnailBackground)            
    IIF(gadgetBackground < 0, picker(index)\gadgetBackground = #Black, picker(index)\gadgetBackground = gadgetBackground)            
    IIf(previewImageSize < 100, picker(index)\previewImageSize = 500, picker(index)\previewImageSize = previewImageSize)    
    
    hourglass()
    
    If IsWindow(parent)
      parentIsWindow = #True
      picker(index)\parentWidth = WindowWidth(picker(index)\parent)
      picker(index)\parentHeight = WindowHeight(picker(index)\parent)
      UseGadgetList(WindowID(picker(index)\parent))
    Else
      picker(index)\parentWidth = GadgetWidth(picker(index)\parent)
      picker(index)\parentHeight = GadgetHeight(picker(index)\parent)
      If GadgetType(picker(index)\parent) = #PB_GadgetType_Panel
        OpenGadgetList(picker(index)\parent, picker(index)\panelIndex)
        CompilerIf #PB_Compiler_OS = #PB_OS_Linux
          picker(index)\parentWidth - 2
          picker(index)\parentHeight - 30
        CompilerElse          
          picker(index)\parentWidth = GetGadgetAttribute(picker(index)\parent, #PB_Panel_ItemWidth)
          picker(index)\parentHeight = GetGadgetAttribute(picker(index)\parent, #PB_Panel_ItemHeight)
        CompilerEndIf
      Else
        OpenGadgetList(picker(index)\parent)
      EndIf        
    EndIf              

    CompilerIf #PB_Compiler_OS = #PB_OS_MacOS
      If Not parentIsWindow        
        If GadgetType(picker(index)\parent) = #PB_GadgetType_Panel 
          SetGadgetState(picker(index)\parent, picker(index)\panelIndex)
          selectedPanel = CocoaMessage(0, GadgetID(picker(index)\parent), "selectedTabViewItem")
          CocoaMessage(0, CocoaMessage(0, selectedPanel, "view"), "setClipsToBounds:", #True)            
          SetGadgetState(picker(index)\parent, 0)
        Else
          CocoaMessage(0, GadgetID(picker(index)\parent), "setClipsToBounds:", #True)          
        EndIf   
      EndIf      
      picker(index)\scrollSpeed = 1
    CompilerElse
      picker(index)\scrollSpeed = 50
    CompilerEndIf
    
    picker(index)\canvas = CanvasGadget(#PB_Any, 0, 0, picker(index)\parentWidth - picker(index)\scrollerWidth, picker(index)\parentHeight)
    picker(index)\scroller = CanvasGadget(#PB_Any, picker(index)\parentWidth - picker(index)\scrollerWidth, 0, picker(index)\scrollerWidth, picker(index)\parentHeight)
    
    SetGadgetData(picker(index)\canvas, index)
    SetGadgetData(picker(index)\scroller, index)
    
    loadImages()
    
    If picker(index)\scrollerVisible
      drawScroller(@picker(index)\scroll)
    EndIf       
    
    If parentIsWindow
      BindEvent(#PB_Event_Gadget, @gadgetHandler(), picker(index)\parent, picker(index)\canvas, #PB_All)
      BindEvent(#PB_Event_Gadget, @gadgetHandler(), picker(index)\parent, picker(index)\scroller, #PB_All)
    Else      
      BindGadgetEvent(picker(index)\canvas, @gadgetHandler(), #PB_All) 
      BindGadgetEvent(picker(index)\scroller, @gadgetHandler(), #PB_All)         
      CloseGadgetList()
      CloseGadgetList()
    EndIf   
    
    panelIndex = #Null
    scrollerWidth = #Null    
    imagesFolder = #Null$
    thumbnailSize = #Null
    thumbnailSize = #Null
    thumbnailSpacing = #Null
    thumbnailTitle = #False
    thumbnailBorder = -1
    thumbnailBackground = -1         
    gadgetBackground = -1
    previewImageSize = #Null
    previewSelectedImage = #False    
    
    hourglass()
    
  EndProcedure
  
  Procedure drawScroller(*scroll.coords)
    Protected scrollerBarColour, scrollerButtonColour, scrollerButtonRadius, scrollY.d
    
    scrollY = *scroll\y
    scrollerButtonColour = #Blue
    scrollerBarColour = RGB(222, 222, 222)    
    scrollerButtonRadius = picker(index)\scrollerWidth / 2
    
    If scrollY < scrollerButtonRadius 
      scrollY = scrollerButtonRadius 
    ElseIf scrollY > picker(index)\parentHeight - (scrollerButtonRadius)
      scrollY = picker(index)\parentHeight - (scrollerButtonRadius)
    EndIf          
    
    StartDrawing(CanvasOutput(picker(index)\scroller))
    RoundBox(0, 0, picker(index)\scrollerWidth, GadgetHeight(picker(index)\scroller), scrollerButtonRadius, scrollerButtonRadius, scrollerBarColour)
    Circle(scrollerButtonRadius, scrollY, scrollerButtonRadius, scrollerButtonColour)
    StopDrawing()   
    
  EndProcedure  
  
  Procedure loadImages()
    Protected directory, titleFontID, titleWidth, titleHeight, canvasNewRow, canvasNewHeight, drawX, drawY, image, imgCount, imgWidth, imgHeight, imgAspectRatio.d
    Protected.s fileName, fileExtension, canvasPos.coords
    Static jpgDecoderLoaded, pngDecoderLoaded, gifDecoderLoaded, tifDecoderLoaded
    
    canvasPos\x = picker(index)\thumbnailSpacing
    canvasPos\y = picker(index)\thumbnailSpacing    
    
    titleHeight = Int(picker(index)\thumbnailSize / 15)
    If titleHeight < 10 : titleHeight = 10 : EndIf    
    titleFontID = FontID(LoadFont(#PB_Any, "Arial", titleHeight))
    
    directory = ExamineDirectory(#PB_Any, picker(index)\imagesFolder, "*.*")
    If directory
      While NextDirectoryEntry(directory)    
        If DirectoryEntryType(directory) = #PB_DirectoryEntry_File
          fileName = DirectoryEntryName(directory)
          fileExtension = LCase(GetExtensionPart(fileName))
          If fileExtension = "bmp" Or fileExtension = "jpg" Or fileExtension = "jpeg" Or fileExtension = "png" Or fileExtension = "gif" Or fileExtension = "tif" Or fileExtension = "tiff" 
            Select fileExtension
              Case "jpg", "jpeg"
                If Not jpgDecoderLoaded
                  UseJPEGImageDecoder()
                  jpgDecoderLoaded = #True
                EndIf                
              Case "png"
                If Not pngDecoderLoaded
                  UsePNGImageDecoder()
                  pngDecoderLoaded = #True
                EndIf
              Case "gif"
                If Not gifDecoderLoaded
                  UseGIFImageDecoder()
                  gifDecoderLoaded = #True
                EndIf
              Case "tif", "tiff"
                If Not tifDecoderLoaded
                  UseTIFFImageDecoder()
                  tifDecoderLoaded = #True
                EndIf                  
            EndSelect                         
            If canvasPos\x + picker(index)\thumbnailSize + picker(index)\thumbnailSpacing > GadgetWidth(picker(index)\canvas)
              canvasPos\x = picker(index)\thumbnailSpacing
              canvasPos\y + picker(index)\thumbnailSize + picker(index)\thumbnailSpacing
              canvasNewHeight = canvasPos\y + picker(index)\thumbnailSize + picker(index)\thumbnailSpacing          
              canvasNewRow = #True
              If canvasNewHeight > GadgetHeight(picker(index)\canvas) 
                If canvasNewHeight < #canvasMaxSize                  
                  ResizeGadget(picker(index)\canvas, #PB_Ignore, #PB_Ignore, #PB_Ignore, canvasNewHeight)
                  picker(index)\scrollFactor = (canvasNewHeight - picker(index)\parentHeight) / picker(index)\parentHeight
                  picker(index)\scrollerVisible = #True                  
                Else
                  MessageRequester("Image Picker Gadget:", "Maximum display limit reached. " + Str(imgCount - 1) + " images loaded.")
                  Break
                EndIf
              EndIf        
            EndIf              
            image = LoadImage(#PB_Any, picker(index)\imagesFolder + fileName)
            If IsImage(image)              
              imgCount + 1
              ReDim picker(index)\images(imgCount)            
              picker(index)\images(imgCount)\imgNum = image            
              picker(index)\images(imgCount)\imgName = fileName                            
              imgWidth = ImageWidth(picker(index)\images(imgCount)\imgNum)
              imgHeight = ImageHeight(picker(index)\images(imgCount)\imgNum)
              If imgWidth > imgHeight                
                imgAspectRatio = imgHeight / imgWidth
                imgWidth = picker(index)\thumbnailSize
                imgHeight = imgWidth * imgAspectRatio
                drawX = canvasPos\x
                drawY = canvasPos\y + ((picker(index)\thumbnailSize - imgHeight) / 2)
              Else
                imgAspectRatio = imgWidth / imgHeight
                imgHeight = picker(index)\thumbnailSize                
                imgWidth = imgHeight * imgAspectRatio
                drawY = canvasPos\y 
                drawX = canvasPos\x + ((picker(index)\thumbnailSize - imgWidth) / 2)
              EndIf              
              StartDrawing(CanvasOutput(picker(index)\canvas))                            
              If imgCount = 1 Or canvasNewRow
                Box(0, canvasPos\y - picker(index)\thumbnailSpacing, GadgetWidth(picker(index)\canvas), picker(index)\thumbnailSize + (picker(index)\thumbnailSpacing * 2), picker(index)\gadgetBackground)     
                canvasNewRow = #False
              EndIf                           
              If picker(index)\thumbnailBackground > -1
                Box(canvasPos\x, canvasPos\y, picker(index)\thumbnailSize, picker(index)\thumbnailSize, picker(index)\thumbnailBackground)
              EndIf              
              DrawImage(ImageID(picker(index)\images(imgCount)\imgNum), drawX, drawY, imgWidth, imgHeight)
              If picker(index)\thumbnailTitle
                DrawingMode(#PB_2DDrawing_AlphaBlend)
                ClipOutput(canvasPos\x, canvasPos\y, picker(index)\thumbnailSize, titleHeight + 10)
                Box(canvasPos\x, canvasPos\y, picker(index)\thumbnailSize, titleHeight + 10 , RGBA(0, 0, 0, 100))
                DrawingFont(titleFontID)
                DrawingMode(#PB_2DDrawing_Transparent)
                titleWidth = TextWidth(fileName)
                If titleWidth => picker(index)\thumbnailSize
                  While titleWidth > picker(index)\thumbnailSize - 40
                    fileName = Left(fileName, Len(fileName) - 1)
                    titleWidth = TextWidth(fileName)
                  Wend      
                  fileName + "..."
                  titleWidth = TextWidth(fileName)
                EndIf
                DrawText(canvasPos\x + picker(index)\thumbnailSize / 2 - titleWidth / 2, canvasPos\y + 5, fileName, #White)                
                UnclipOutput()
              EndIf              
              If picker(index)\thumbnailBorder > -1
                DrawingMode(#PB_2DDrawing_Outlined)
                Box(canvasPos\x, canvasPos\y, picker(index)\thumbnailSize, picker(index)\thumbnailSize, picker(index)\thumbnailBorder)
              EndIf             
              picker(index)\images(imgCount)\imgX1 = canvasPos\x : picker(index)\images(imgCount)\imgX2 = canvasPos\x + picker(index)\thumbnailSize
              picker(index)\images(imgCount)\imgY1 = canvasPos\y : picker(index)\images(imgCount)\imgY2 = canvasPos\y + picker(index)\thumbnailSize
              StopDrawing()      
              canvasPos\x + picker(index)\thumbnailSize + picker(index)\thumbnailSpacing
            EndIf
          EndIf
        EndIf
      Wend
      FinishDirectory(directory)
    EndIf
    
  EndProcedure
  
  Procedure hourglass()
    Static hourglassWindow
    
    If IsWindow(hourglassWindow)
      CloseWindow(hourglassWindow)
    Else
      hourglassWindow = OpenWindow(#PB_Any, 0, 0, 200, 50, "ImagePickerGadget()", #PB_Window_BorderLess | #PB_Window_WindowCentered)
      SetWindowColor(hourglassWindow, RGB(250, 200, 210))
      TextGadget(#PB_Any, 0, 15, 200, 30, "Loading images...", #PB_Text_Center)         
    EndIf
    
  EndProcedure
  
  Procedure displayImage(selectedImageIndex)
    Protected image, tempImage, imgWidth, imgHeight, imgAspectRatio.d, imageName.s
    
    image = picker(index)\images(selectedImageIndex)\imgNum
    imageName = picker(index)\images(selectedImageIndex)\imgName
    
    CopyImage(image, tempImage)    
    imgWidth = ImageWidth(tempImage)
    imgHeight = ImageHeight(tempImage)     
    
    If imgWidth > imgHeight                
      imgAspectRatio = imgHeight / imgWidth
      imgWidth = picker(index)\previewImageSize
      imgHeight = imgWidth * imgAspectRatio
    Else
      imgAspectRatio = imgWidth / imgHeight
      imgHeight = picker(index)\previewImageSize
      imgWidth = imgHeight * imgAspectRatio      
    EndIf       
    
    ResizeImage(tempImage, imgWidth, imgHeight)    
    
    If Not IsWindow(picker(index)\previewImageWindow)            
      picker(index)\previewImageWindow = OpenWindow(#PB_Any, 0, 0, imgWidth, imgHeight, imageName, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)      
      picker(index)\previewImageViewer = ImageGadget(#PB_Any, 0, 0, imgWidth, imgHeight, ImageID(tempImage))     
      BindEvent(#PB_Event_CloseWindow, @windowHandler(), picker(index)\previewImageWindow)      
    Else      
      SetWindowTitle(picker(index)\previewImageWindow, imageName)
      If WindowWidth(picker(index)\previewImageWindow) <> imgWidth Or WindowHeight(picker(index)\previewImageWindow) <> imgHeight      
        ResizeWindow(picker(index)\previewImageWindow, #PB_Ignore, #PB_Ignore, imgWidth, imgHeight)
        ResizeGadget(picker(index)\previewImageViewer, #PB_Ignore, #PB_Ignore, imgWidth, imgHeight)
      EndIf      
      SetGadgetState(picker(index)\previewImageViewer, ImageID(tempImage))      
    EndIf
    
  EndProcedure
  
  Procedure validateDirectory(imagesFolder.s)    
    Protected directory, directoryValid, fileExtension.s
    
    directory = ExamineDirectory(#PB_Any, imagesFolder, "*.*")
    If directory
      While NextDirectoryEntry(directory)
        If DirectoryEntryType(directory) = #PB_DirectoryEntry_File
          fileExtension = LCase(GetExtensionPart(DirectoryEntryName(directory)))
          If DirectoryEntrySize(directory) > 0 And (fileExtension = "bmp" Or fileExtension = "jpg" Or fileExtension = "jpeg" Or fileExtension = "png" Or fileExtension = "gif" Or fileExtension = "tif" Or fileExtension = "tiff")
            directoryValid = #True
            Break
          EndIf        
        EndIf
      Wend
      FinishDirectory(directory)
    EndIf      
    
    ProcedureReturn directoryValid
  EndProcedure
  
  Procedure scrollCanvas(deltaY, scrollbar)      
    Protected canvasY, newCanvasY
    
    canvasY = GadgetY(picker(index)\canvas)
    
    If scrollbar
      newCanvasY = deltaY
    Else    
      newCanvasY = canvasY + deltaY
    EndIf
    
    If newCanvasY < -(GadgetHeight(picker(index)\canvas) - picker(index)\parentHeight)
      canvasY = -(GadgetHeight(picker(index)\canvas) - picker(index)\parentHeight)
    ElseIf newCanvasY > 0
      canvasY = 0
    Else
      canvasY = newCanvasY
    EndIf           
    
    ResizeGadget(picker(index)\canvas, #PB_Ignore, canvasY, #PB_Ignore, #PB_Ignore)   
    
    ProcedureReturn canvasY
  EndProcedure
  
  Procedure checkClickPoint(*click.coords)
    Protected i, selectedImageIndex
    
    For i = 1 To ArraySize(picker(index)\images())
      If *click\x > picker(index)\images(i)\imgX1 And *click\x < picker(index)\images(i)\imgX2
        If *click\y > picker(index)\images(i)\imgY1 And *click\y < picker(index)\images(i)\imgY2
          selectedImageIndex = i
        EndIf
      EndIf    
    Next i   
    
    ProcedureReturn selectedImageIndex
  EndProcedure
  
  Procedure windowHandler()
    CloseWindow(EventWindow())    
  EndProcedure
  
  Procedure gadgetHandler()  
    Shared selectedImage, selectedImageName
    Protected i, delta, selectedImageIndex, click.coords
    Static scrollerOn, touchScrollerOn, touchScrollStart
    
    index = GetGadgetData(EventGadget())    
    
    Select EventGadget()
        
      Case picker(index)\scroller
        Select EventType()               
            
          Case #PB_EventType_LeftButtonDown
            picker(index)\scroll\y = GetGadgetAttribute(picker(index)\scroller, #PB_Canvas_MouseY)                            
            scrollerOn = #True
            drawScroller(@picker(index)\scroll)                  
            scrollCanvas(-(picker(index)\scroll\y * picker(index)\scrollFactor), #True) 
            
          Case #PB_EventType_LeftButtonUp              
            scrollerOn = #False
            
          Case #PB_EventType_MouseMove
            If scrollerOn And picker(index)\scrollerVisible
              picker(index)\scroll\y = GetGadgetAttribute(picker(index)\scroller, #PB_Canvas_MouseY)                            
              If picker(index)\scroll\y > 0 And picker(index)\scroll\y < picker(index)\parentHeight
                drawScroller(@picker(index)\scroll)                  
                scrollCanvas(-(picker(index)\scroll\y * picker(index)\scrollFactor), #True) 
              EndIf                
            EndIf
            
        EndSelect
        
      Case picker(index)\canvas          
        Select EventType()               
            
          Case #PB_EventType_LeftClick
            click\x = GetGadgetAttribute(picker(index)\canvas, #PB_Canvas_MouseX)
            click\y = GetGadgetAttribute(picker(index)\canvas, #PB_Canvas_MouseY)              
            selectedImageIndex = checkClickPoint(@click)
            If selectedImageIndex
              If picker(index)\previewSelectedImage
                displayImage(selectedImageIndex)
              Else
                selectedImage = picker(index)\images(selectedImageIndex)\imgNum
                selectedImageName = picker(index)\images(selectedImageIndex)\imgName
                PostEvent(#ImageSelected)
              EndIf
            EndIf
            
          Case #PB_EventType_MouseWheel               
            If picker(index)\scrollerVisible
              picker(index)\scroll\y = Abs(scrollCanvas(GetGadgetAttribute(picker(index)\canvas, #PB_Canvas_WheelDelta) * picker(index)\scrollSpeed, #False)) / picker(index)\scrollFactor                         
              drawScroller(@picker(index)\scroll)
            EndIf
            
          Case #PB_EventType_LeftButtonDown
            touchScrollStart = GetGadgetAttribute(picker(index)\canvas, #PB_Canvas_MouseY)                            
            touchScrollerOn = #True
            
          Case #PB_EventType_LeftButtonUp              
            touchScrollerOn = #False
            
          Case #PB_EventType_MouseMove
            If touchScrollerOn And picker(index)\scrollerVisible
              delta = touchScrollStart - GetGadgetAttribute(picker(index)\canvas, #PB_Canvas_MouseY)                
              picker(index)\scroll\y = Abs(scrollCanvas(-delta, #False))  / picker(index)\scrollFactor 
              drawScroller(@picker(index)\scroll)
            EndIf              
            
        EndSelect
        
    EndSelect
    
  EndProcedure 
  
EndModule

Implementation Examples

NOTE: The image picker gadget module should be saved as ImagePickerGadget.pbi and included in the implementations.

Here's a basic-mode implementation in its simplest form:

Code: Select all

XIncludeFile "ImagePickerGadget.pbi"
mainWindow = OpenWindow(#PB_Any, 0, 0, 270, 270, "Image Picker Gadget (basic mode)", 
                        #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
container = ContainerGadget(#PB_Any, 10, 10, 250, 250)
ImagePicker::ImagePicker(container, imagesFolder$, #True)
While WaitWindowEvent() ! #PB_Event_CloseWindow : Wend


Here's an advanced-mode implementation with all the options:

Code: Select all

XIncludeFile "ImagePickerGadget.pbi"
mainWindow = OpenWindow(#PB_Any, 0, 0, 490, 380, "Image Picker Gadget (advanced mode)", 
                        #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
container = ContainerGadget(#PB_Any, 10, 10, 470, 350)

UseModule ImagePicker
  parent = container
  scrollerWidth = 10
  thumbnailSize = 200
  thumbnailSpacing = 20
  thumbnailTitle = #True
  thumbnailBorder = #Black  
  thumbnailBackground = #Black
  gadgetBackground = RGB(214, 197, 236)
  previewImageSize = 600
  previewSelectedImage = #True
  imagesFolder = imagesFolder$
  initImagePicker()
UnuseModule ImagePicker

While WaitWindowEvent() ! #PB_Event_CloseWindow : Wend


Here's another advaced-mode implementation on multiple panels in a panel gadget:

Code: Select all

XIncludeFile "ImagePickerGadget.pbi"
mainWindow = OpenWindow(#PB_Any, 0, 0, 480, 380, "Image Picker Gadget (advanced mode)", 
                        #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
SetWindowColor(mainWindow, RGB(214, 197, 236))

; works with panel gadgets on multiple panels
panel = PanelGadget(#PB_Any, 10, 10, 450, 350)
AddGadgetItem(panel, 0, "Image Set 1")

UseModule ImagePicker
  parent = panel
  panelIndex = 0                 ; drawn on the 1st panel
  scrollerWidth = 10             ; scroller width = 10px
  thumbnailSize = 120            ; thumbnail size = 180px
  thumbnailSpacing = 15          ; thumbnail spacing = 20px
  thumbnailTitle = #True         ; thumbnail title = display
  thumbnailBorder = #Black       ; thumbnail border colour = black
  thumbnailBackground = #Gray    ; thumbnail background colour = gray  
  previewImageSize = 300         ; image preview window = 300px
  previewSelectedImage = #True   ; selected image will be displayed
  imagesFolder = imagesFolder$   ; the images source folder (full or relative path)
  initImagePicker()
UnuseModule ImagePicker  

; for additional panels in the panel gadget
OpenGadgetList(panel)
AddGadgetItem(panel, 1, "Image Set 2")
UseModule ImagePicker
  parent = panel
  panelIndex = 1                 ; drawn on the 2nd panel with all default options
  previewSelectedImage = #True
  imagesFolder = imagesFolder$
  initImagePicker()
UnuseModule ImagePicker

While WaitWindowEvent() ! #PB_Event_CloseWindow : Wend


It's a simple plug & play tool with much room to grow. So, please feel free to expand upon it as required.

I hope that it proves to be a useful addition to the PureBasic toolbox. :D


Updates

27th November 2024:
  • originally supported only JPG and PNG image types to minimise image library size
  • added support for BMP, GIF, TIFF image types by loading the respective libraries dynamically only as required
  • added a fix for newer versions of macOS where contents overflow panels in panel gadgets (thanks to @deseven)
  • added a fix for panel gadgets where the contents get clipped (affected all platforms)

28th November 2024:
  • program crashes when attempting to load images beyond the canvas size limit (32767 pixels)
  • added a fix to stop loading images once this limit is reached with notification (thanks to @HeXOR)

29th November 2024:
  • the gadget now supports images of different sizes and orientations - the preview window will auto-rotate accordingly
  • the thumbnails will be displayed in a square frame with options to display a border, background, and image-name titling
  • the gadget also features a background-colour option and a busy notification window when loading large number of images
  • program crashes while loading images if the user-provided image folder value is missing the trailing path separator
  • added a fix to check and append the path separator to the image folder if missing (thanks to @AZJIO)
Last edited by TI-994A on Fri Nov 29, 2024 8:42 pm, edited 3 times in total.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

Image
Here's the fully-functional code for this screenshot, which includes a procedure to download the sample images from DropBox into a temporary image folder:

Code: Select all

XIncludeFile "ImagePickerGadget.pbi"

Declare previewImage(x, y.s)
Declare.s downloadImages()

mainWindow = OpenWindow(#PB_Any, 0, 0, 710, 550, "Image Picker Gadget (full demo)",
                        #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
SetWindowColor(mainWindow, RGB(214, 197, 236))   ; for demo aesthetics only

; download sample images from DropBox into a temporary images folder
imagesFolder$ = downloadImages()   

; works with container gadgets
container = ContainerGadget(#PB_Any, 30, 30, 470, 350)
UseModule ImagePicker
  parent = container
  scrollerWidth = 10
  thumbnailSize = 200
  thumbnailSpacing = 20
  thumbnailTitle = #True
  thumbnailBorder = #Black
  thumbnailBackground = RGB(214, 197, 236)  
  gadgetBackground = #White
  previewImageSize = 800
  previewSelectedImage = #True
  imagesFolder = imagesFolder$
  initImagePicker()
UnuseModule ImagePicker

; works with canvas gadgets (with the container flag applied)
canvas = CanvasGadget(#PB_Any, 530, 30, 150, 350, #PB_Canvas_Container)
UseModule ImagePicker
  parent = canvas
  scrollerWidth = 10
  thumbnailSize = 100
  thumbnailSpacing = 10
  thumbnailTitle = #True
  thumbnailBorder = #Black
  thumbnailBackground = RGB(214, 197, 236)  
  gadgetBackground = #White  
  previewImageSize = 500
  previewSelectedImage = #True
  imagesFolder = imagesFolder$
  initImagePicker()
UnuseModule ImagePicker

; works with panel gadgets on multiple panels
panel = PanelGadget(#PB_Any, 30, 410, 650, 120)
AddGadgetItem(panel, 0, "Image Set 1")
UseModule ImagePicker
  parent = panel
  panelIndex = 0
  scrollerWidth = 10
  thumbnailSize = 57
  thumbnailSpacing = 10
  thumbnailTitle = #True
  thumbnailBorder = #Black
  thumbnailBackground = RGB(214, 197, 236)  
  gadgetBackground = #White  
  previewImageSize = 800
  previewSelectedImage = #True
  imagesFolder = imagesFolder$
  initImagePicker()
UnuseModule ImagePicker

; for additional panels in the panel gadget
OpenGadgetList(Panel)
AddGadgetItem(panel, 1, "Image Set 2")
UseModule ImagePicker
  parent = panel
  panelIndex = 1
  scrollerWidth = 10
  thumbnailSize = 57
  thumbnailBorder = #True
  thumbnailSpacing = 10
  previewSelectedImage = #False   ; selected images from this panel will be displayed by the parent app
  imagesFolder = imagesFolder$
  initImagePicker()
UnuseModule ImagePicker

; an image viewer to preview images when [previewSelectedImage = #False]
previewWindow = OpenWindow(#PB_Any, 0, 0, 400, 220, "Main App Image Viewer",
                           #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
previewGadget = ImageGadget(#PB_Any, 0, 0, 400, 200, 0) 
HideWindow(previewWindow, #True)

Repeat
  event = WaitWindowEvent()
  Select event
      
    ; this event will be triggered if [previewSelectedImage = #False]
    ; to return the selected image number for your necessary action 
    Case ImagePicker::#ImageSelected   
      
      ; the image and file name are passed to a procedure for preview
      previewImage(ImagePicker::selectedImage, ImagePicker::selectedImageName)               
      
    Case #PB_Event_CloseWindow
      If EventWindow() = previewWindow
        HideWindow(previewWindow, #True)
      ElseIf EventWindow() = mainWindow 
        appQuit = #True      
      EndIf      
      
  EndSelect  
Until appQuit

; procedure to preview the selected image
Procedure previewImage(image, imageName.s)    
  Shared previewWindow, previewGadget
  Define imgAspectRatio.d
  
  SetWindowTitle(previewWindow, "Previewing: " + imageName)

  CopyImage(image, tempImage)
  
  imgWidth = ImageWidth(tempImage)
  imgHeight = ImageHeight(tempImage)     
  
  If imgWidth > imgHeight                
    imgAspectRatio = imgHeight / imgWidth
    imgWidth = 600
    imgHeight = imgWidth * imgAspectRatio
  Else
    imgAspectRatio = imgWidth / imgHeight
    imgHeight = 600
    imgWidth = imgHeight * imgAspectRatio      
  EndIf       
  
  HideWindow(previewWindow, #False)         
  ResizeImage(tempImage, imgWidth, imgHeight)      
  ResizeWindow(previewWindow, #PB_Ignore, #PB_Ignore, imgWidth, imgHeight)
  SetGadgetState(previewGadget, ImageID(tempImage))
  
EndProcedure

; procedure to download sample images from DropBox
Procedure.s downloadImages() 
 
  ; create a temporary folder for the images
  imagesFolder$ = GetTemporaryDirectory() + "images2" + #PS$
  If FileSize(imagesFolder$) <> -2
    If Not CreateDirectory(imagesFolder$)    
      MessageRequester("Custom Image Picker", "Error locating/creating images folder!")
      End
    EndIf
  EndIf
  
  ; download text file containing image-links list  
  imgLinksFile$ = GetTemporaryDirectory() + "imageLinks2.txt"
  If FileSize(imgLinksFile$) < 1  
    CompilerIf #PB_Compiler_Version < 600
      InitNetwork()
    CompilerEndIf 
    If Not ReceiveHTTPFile("https://www.dropbox.com/scl/fi/revs2u0chjcebrrbawi5k/imageLinks.txt?" +
                           "rlkey=n2t54h8h1ojohadtchug3e1t4&st=aqetnubp&dl=1", imgLinksFile$)
      MessageRequester("Custom Image Picker", "Error downloading image links file!")
      End
    EndIf
  EndIf
  
  ; download sample images into temporary images folder  
  If FileSize(imgLinksFile$) > 0
    If OpenFile(0, imgLinksFile$)
      While Not Eof(0)
        imageLinkInfo$ = ReadString(0)   
        imageFileLink$ = StringField(imageLinkInfo$, 2, ",")
        imageFileName$ = imagesFolder$ + StringField(imageLinkInfo$, 1, ",")        
        If FileSize(imageFileName$) < 1  
          If Not ReceiveHTTPFile(imageFileLink$, imageFileName$)
            imageDownloadFailed = #True
          EndIf                
        EndIf                
      Wend
    EndIf    
  EndIf  
  
  If imageDownloadFailed        
    MessageRequester("Custom Image Picker", "Error downloading some/all images!")    
  EndIf
  
  ProcedureReturn imagesFolder$  
EndProcedure

As always, I hope it's useful. :D
Last edited by TI-994A on Fri Nov 29, 2024 9:17 pm, edited 3 times in total.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
User avatar
moulder61
Enthusiast
Enthusiast
Posts: 188
Joined: Sun Sep 19, 2021 6:16 pm
Location: U.K.

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by moulder61 »

Hi TI-994A,

This is almost exactly what I've been looking for. I was trying to write my own wallpaper picker utility but didn't know where to start? I think I can probably use your code as a base for creating what I want. 8)

Thanks,

Moulder.
"If it ain't broke, fix it until it is!

This message is brought to you thanks to SenselessComments.com

My PB stuff for Linux: "https://u.pcloud.link/publink/show?code ... z3MR0T3jyV
Bitblazer
Enthusiast
Enthusiast
Posts: 761
Joined: Mon Apr 10, 2017 6:17 pm
Location: Germany
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by Bitblazer »

Looks nice, would be great if it was using the MIT license. Because it is definately something that is currently missing and i assume the MIT license is fine to you too.
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

moulder61 wrote: Tue Nov 26, 2024 10:33 pmThis is almost exactly what I've been looking for. I was trying to write my own wallpaper picker utility...
Hi Moulder. I'm glad to hear that. Actually, you could easily use it right out of the box, just like a gadget.

BTW, I just posted an update to the module code in the first post. It now supports more image types and contains some additional fixes.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

Bitblazer wrote: Tue Nov 26, 2024 11:35 pm Looks nice, would be great if it was using the MIT license...
Thanks Bitblazer. These forum posts are usually free to use, so it's quite moot to attach any licensing to them. The important thing is that the code works and proves helpful to the PureBasic forum users.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
BarryG
Addict
Addict
Posts: 4118
Joined: Thu Apr 18, 2019 8:17 am

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by BarryG »

The screenshots look great! :)
User avatar
moulder61
Enthusiast
Enthusiast
Posts: 188
Joined: Sun Sep 19, 2021 6:16 pm
Location: U.K.

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by moulder61 »

TI-994A wrote: Wed Nov 27, 2024 12:51 pm
Hi Moulder. I'm glad to hear that. Actually, you could easily use it right out of the box, just like a gadget.

BTW, I just posted an update to the module code in the first post. It now supports more image types and contains some additional fixes.
Hi TI-994A,

I don't want to steal your work as such, but it will at least give me an idea where to start and how to accomplish certain things. I'm working on a few projects, and find it difficult to work stuff out for myself, but I'm a bit stubborn and too proud to ask for much help, so I fumble my way along, getting by with looking at what others have done to see if I can cannibalise it for my requirements?

I've examined code by yourself, and others, and although it often looks very complicated, I have picked up some tips here and there, so thank you(all) for your contributions. :D

Moulder.
"If it ain't broke, fix it until it is!

This message is brought to you thanks to SenselessComments.com

My PB stuff for Linux: "https://u.pcloud.link/publink/show?code ... z3MR0T3jyV
User avatar
le_magn
Enthusiast
Enthusiast
Posts: 277
Joined: Wed Aug 24, 2005 12:11 pm
Location: Italia

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by le_magn »

Hi think i encouneterd a little bug basically I used as reference the steam images cache folder imagesFolder = “C:\Program Files (x86)\Steam\appcache\librarycache\”, it loads the images and everything seems ok, but if I scroll the images from the scrollbar, got to 1/4 of the scrollbar the images stop scrolling, I tried with the 2 and 3 example codes
Image
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

BarryG wrote: Wed Nov 27, 2024 1:33 pm The screenshots look great! :)
Thanks Barry!

moulder61 wrote: Wed Nov 27, 2024 1:37 pm...I have picked up some tips here and there, so thank you(all) for your contributions. :D
Thanks Moulder. I'm always glad to be of help. Please, feel free to cannibalise away! :lol:
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

le_magn wrote: Wed Nov 27, 2024 2:00 pm...it loads the images and everything seems ok, but if I scroll the images from the scrollbar, got to 1/4 of the scrollbar the images stop scrolling, I tried with the 2 and 3 example codes
Hi @le_magn. So, the app did not crash or hang? It simply couldn't scroll past the first quarter way down?

In my testing I have been able to load 600 high resolution photographs of different sizes and dimensions. It took nearly 3 minutes to complete, and the application paged 17GB of RAM once all the images were fully loaded and drawn. But even so, it worked smoothly as expected without any lag or issue.

I'm not really sure what could be causing this issue of yours, but best guess would have to be the number of images or total load capacity.

Perhaps you could advise on the nature of the contents in the C:\Program Files (x86)\Steam\appcache\librarycache\ folder.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
User avatar
le_magn
Enthusiast
Enthusiast
Posts: 277
Joined: Wed Aug 24, 2005 12:11 pm
Location: Italia

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by le_magn »

hi, not crash or hang simple image not scroll down but if i scroll up is ok, the folder contain mixed image type, jpg, png, svg and some non image file...
Image
Image
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

le_magn wrote: Wed Nov 27, 2024 5:40 pm hi, not crash or hang simple image not scroll down but if i scroll up is ok, the folder contain mixed image type, jpg, png, svg and some non image file...
Image
That's cool. Is that a modified version of the ImagePickerGadget()? If so, perhaps you could post the modified code for us to take a look.

It could be an issue with the scroll factor since the thumbnails are all of different heights.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
User avatar
le_magn
Enthusiast
Enthusiast
Posts: 277
Joined: Wed Aug 24, 2005 12:11 pm
Location: Italia

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by le_magn »

no i used standard example: this is the example, from tath point the scrollbar go down but images not scroll, if i go up images scroll up ok
Image
Image
User avatar
TI-994A
Addict
Addict
Posts: 2698
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

Re: PureBasic ImagePickerGadget() - A Cross-Platform Solution

Post by TI-994A »

le_magn wrote: Wed Nov 27, 2024 8:40 pm no i used standard example: this is the example, from tath point the scrollbar go down but images not scroll, if i go up images scroll up ok
Image
Noted.

Are you using the latest ImagePickerGadget()? I had updated the code earlier today.

In any case, would you mind providing a text listing of the image folder, showing the file names, types, sizes, and total number of files? Even a screenshot would do.

Thank you.
Last edited by TI-994A on Wed Nov 27, 2024 11:05 pm, edited 1 time in total.
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D
Post Reply