Skip to content
Snippets Groups Projects
WhatAQuietStiff.rb 10.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env ruby
    
    #
    # Depends on Kou's RSS Parser module (http://raa.ruby-lang.org/list.rhtml?name=rss).
    #
    # Potential enhancements:
    #
    # - preference of whether the pictures are listed latest first, or
    #   oldest first.
    # - option to always show the latest picture after a refresh
    #
    
    require 'fox16'
    require 'open-uri'
    
    begin
      require 'rss/parser'
      require 'rss/2.0'
    rescue LoadError
      require 'fox16/missingdep'
      MSG = <<EOM
      Sorry, this example depends on the RSS extension. Please
      check the Ruby Application Archives for an appropriate
      download site.
    EOM
      missingDependency(MSG)
    end
    
    include Fox
    
    class WhatAQuietWindow < FXMainWindow
    
      RSS_FEED_URL          = "http://whytheluckystiff.net/quiet/quiet.xml"
      DEFAULT_REFRESH_DELAY = 60 # in minutes
    
      def initialize(app)
        # Invoke base class initialize first
        super(app, "What a Quiet Stiff", :opts => DECOR_ALL, :width => 850, :height => 600, :padLeft => 0, :padRight => 0)
    
        # Icons for list items
        File.open("icons/bluebullet14x14.gif", "rb") do |f|
          bytes = f.read
          @itemIcon = FXGIFIcon.new(getApp(), bytes) 
        end
        File.open("icons/transpbullet14x14.gif", "rb") do |f|
          bytes = f.read
          @transpIcon = FXGIFIcon.new(getApp(), bytes) 
        end
    
        # Menubar
        menuBar = FXMenuBar.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
    
        # File menu
        fileMenu = FXMenuPane.new(self)
        saveCmd = FXMenuCommand.new(fileMenu, "Save selected image...")
        saveCmd.connect(SEL_COMMAND, method(:onCmdSave))
        saveCmd.connect(SEL_UPDATE, method(:onUpdSave))
        FXMenuCommand.new(fileMenu, "Preferences...").connect(SEL_COMMAND, method(:onCmdPreferences))
        FXMenuCommand.new(fileMenu, "&Quit\tCtrl+Q").connect(SEL_COMMAND, method(:onQuit))
        FXMenuTitle.new(menuBar, "&File", nil, fileMenu)
    
        # Help menu
        helpMenu = FXMenuPane.new(self)
        FXMenuTitle.new(menuBar, "&Help", nil, helpMenu, LAYOUT_RIGHT)
        aboutCmd = FXMenuCommand.new(helpMenu, "&About...")
        aboutCmd.connect(SEL_COMMAND) do
          FXMessageBox.information(self, MBOX_OK, "About This Program",
            "What a Quiet Stiff\nA Sliding Surface for Found Imagery\nCourtesy of http://whytheluckystiff.net")
        end
    
        # Respond to window close
        self.connect(SEL_CLOSE, method(:onQuit))
    
        # Main contents area is split left-to-right.
        splitter = FXSplitter.new(self, LAYOUT_FILL_X|LAYOUT_FILL_Y)
    
        # Put the list in a sunken frame
        listFrame = FXVerticalFrame.new(splitter,
          FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y,
          :padding => 00)
    
        # List of items appears on the left.
        @itemList = FXList.new(listFrame, :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y)
        @itemList.numVisible = 12
        @itemList.connect(SEL_COMMAND) do |sender, sel, itemIndex|
          @showLinkedImage = false
          getApp().beginWaitCursor do
            setImage(@itemList.getItemData(itemIndex))
            @itemList.setItemIcon(itemIndex, @transpIcon)
          end
        end
        
        # Sunken border for image widget
        imagebox = FXHorizontalFrame.new(splitter,
          FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y,
          :padding => 0)
      
        # Make image widget
        @imageview = FXImageView.new(imagebox, :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y)
        @imageview.enable
        @imageview.connect(SEL_LEFTBUTTONRELEASE) { toggleImage }
    
        # Cache previously viewed images in a Hash
        @cache = {}
        @showLinkedImage = false
    
        # Start out with the current feed's contents.
        refreshList
      end
    
      # Return true if an item is selected, false otherwise.
      def itemSelected?
        begin
          @itemList.itemSelected?(@itemList.currentItem)
        rescue IndexError
          false
        end
      end
    
      #
      # Enable or disable the "Save Image" command, depending on
      # whether or not any items are selected.
      #
      def onUpdSave(sender, sel, ptr)
        if itemSelected?
          sender.handle(self, FXSEL(SEL_COMMAND, ID_ENABLE), nil)
        else
          sender.handle(self, FXSEL(SEL_COMMAND, ID_DISABLE), nil)
        end
      end
    
      # Save the currently selected image to a file.
      def onCmdSave(sender, sel, ptr)
        saveDialog = FXFileDialog.new(self, "Save Image")
        saveDialog.filename = @itemList.getItemText(@itemList.currentItem)
        if saveDialog.execute != 0
          if File.exists? saveDialog.filename
            if MBOX_CLICKED_NO == FXMessageBox.question(self, MBOX_YES_NO,
              "Overwrite Image", "Overwrite existing image?")
              return 1
            end
          end
          getApp().beginWaitCursor do
            FXFileStream.open(saveDialog.filename, FXStreamSave) do |stream|
              @imageview.image.restore
              @imageview.image.savePixels(stream)
            end
          end
        end
      end
    
      # Display the Preferences dialog box.
      def onCmdPreferences(sender, sel, ptr)
        refreshDelayTarget = FXDataTarget.new(@refreshDelay)
    
        prefsDialog = FXDialogBox.new(self, "Preferences", :padding => 2)
    
        buttons = FXHorizontalFrame.new(prefsDialog, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X)
        FXFrame.new(buttons, LAYOUT_FILL_X)
        FXButton.new(buttons, "Cancel", nil, prefsDialog, FXDialogBox::ID_CANCEL,
          LAYOUT_SIDE_RIGHT|LAYOUT_CENTER_Y|FRAME_RAISED|FRAME_THICK,
          :padLeft => 20, :padRight => 20, :padTop => 4, :padBottom => 4)
        FXButton.new(buttons, "OK", nil, prefsDialog, FXDialogBox::ID_ACCEPT,
          LAYOUT_SIDE_RIGHT|LAYOUT_CENTER_Y|FRAME_RAISED|FRAME_THICK,
          :padLeft => 30, :padRight => 30, :padTop => 4, :padBottom => 4)
    
        FXHorizontalSeparator.new(prefsDialog, SEPARATOR_GROOVE|LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X)
    
        contents = FXMatrix.new(prefsDialog, 2, MATRIX_BY_COLUMNS|LAYOUT_FILL_X|LAYOUT_FILL_Y)
        FXLabel.new(contents, "Refresh Delay (minutes):", nil, LAYOUT_CENTER_Y)
        FXTextField.new(contents, 5, refreshDelayTarget, FXDataTarget::ID_VALUE,
          LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
    
        if prefsDialog.execute != 0
          @refreshDelay = refreshDelayTarget.value
        end
      end
    
      #
      # Given an RSS object, populate the list of images with one
      # per item in the RSS.
      #
      def populateItemList(rss)
        @itemList.clearItems
        liveItems = {}
        rss.items.each do |rssItem|
          srcURL  = getSourceURL(rssItem)
          linkURL = getLinkURL(rssItem)
          itemIcon = (@cache.key?(srcURL) || @cache.key?(linkURL)) ? @transpIcon : @itemIcon
          @itemList.appendItem(rssItem.title, itemIcon, rssItem)
          liveItems[srcURL] = 1 if @cache.key?(srcURL)
          liveItems[linkURL] = 1 if @cache.key?(linkURL)
        end
        @cache.delete_if { |key, value| !liveItems.key?(key) }
      end
    
      def toggleImage
        @showLinkedImage = !@showLinkedImage
        if itemSelected?
          itemIndex = @itemList.currentItem
          getApp().beginWaitCursor do
            setImage(@itemList.getItemData(itemIndex))
            @itemList.setItemIcon(itemIndex, @transpIcon)
          end
        end
      end
    
      def setImage(rssItem)
        url = getImageURL(rssItem)
        img = @cache[url]
        if img.nil?
          img = makeImage(url)
          img.create
          @cache[url] = img
        end
        @imageview.image = img
      end
    
      def getRSSFeed(url)
        rss = nil
        open(url) do |f|
          doc = f.read
          begin
            rss = RSS::Parser.parse(doc)
          rescue RSS::InvalidRSSError
            rss = RSS::Parser.parse(doc, false)
          end
        end
        rss
      end
    
      # Return the URL listed in the src tag of the description's HTML text.
      def getSourceURL(rssItem)
        rssItem.description =~ /src="(.*?)"/
        return $1
      end
      
      # Return the URL listed in the href tag of the description's HTML text.
      def getLinkURL(rssItem)
        rssItem.description =~ /href="(.*?)"/
        return $1
      end
    
      # Return the appropriate URL given the current settings.
      def getImageURL(rssItem)
        @showLinkedImage ? getLinkURL(rssItem) : getSourceURL(rssItem)
      end
    
      def getImageData(url)
        bytes = nil
        open(url, "rb") do |f|
          bytes = f.read
        end
        bytes
      end
    
      # This is a little weak...
      def makeImage(url)
        bytes = getImageData(url)
        extension = url.split('.').last.upcase
        case extension
          when "GIF"
            FXGIFImage.new(getApp(), bytes)
          when "JPG"
            FXJPGImage.new(getApp(), bytes)
          when "PNG"
            FXPNGImage.new(getApp(), bytes)
          else
            raise "Unrecognized file extension for: #{url}"
        end
      end
    
      def resizeItemList
        maxItemSize = 0
        @itemList.each do |listItem|
          itemSize = @itemList.font.getTextWidth(listItem.text)
          maxItemSize = [maxItemSize, itemSize].max
        end
        @itemList.parent.width = maxItemSize
      end
    
      def refreshList
        # Grab the latest RSS feed
        @rss = getRSSFeed(RSS_FEED_URL)
        
        # Repopulate the list with this set of items
        populateItemList(@rss)
      end
    
      def onRefreshList(sender, sel, ptr)
        # Refresh, then re-register the timeout
        getApp().beginWaitCursor { refreshList }
        getApp().addTimeout(1000*60*@refreshDelay, method(:onRefreshList))
      end
    
      def onQuit(sender, sel, ptr)
        writeRegistry
        getApp().exit(0)
      end
    
      def readRegistry
        xx = getApp().reg().readIntEntry("SETTINGS", "x", 0)
        yy = getApp().reg().readIntEntry("SETTINGS", "y", 0)
        ww = getApp().reg().readIntEntry("SETTINGS", "width", 850)
        hh = getApp().reg().readIntEntry("SETTINGS", "height", 600)
        @refreshDelay = getApp().reg().readIntEntry("SETTINGS", "refreshDelay", DEFAULT_REFRESH_DELAY)
      end
    
      def writeRegistry
        getApp().reg().writeIntEntry("SETTINGS", "x", x)
        getApp().reg().writeIntEntry("SETTINGS", "y", y)
        getApp().reg().writeIntEntry("SETTINGS", "width", width)
        getApp().reg().writeIntEntry("SETTINGS", "height", height)
        getApp().reg().writeIntEntry("SETTINGS", "refreshDelay", @refreshDelay)
      end
    
      def create
        # Do base class create first
        super
    
        readRegistry
    
        @itemIcon.create
        @transpIcon.create
    
        # Make the item list wide enough to show the longest item
        resizeItemList
        
        # Resize main window
        # Resize main window client area to fit image size
    #    resize(@imageview.contentWidth, @imageview.contentHeight)
    
        # Now show it
        show(PLACEMENT_SCREEN)
        
        # Start the updates timer
        getApp().addTimeout(1000*60*@refreshDelay, method(:onRefreshList))
      end
    end
    
    if __FILE__ == $0
      # Make application
      application = FXApp.new("WhatAQuietWindow", "FXRuby")
    
      # Make window
      window = WhatAQuietWindow.new(application)
    
      # Handle interrupts to terminate program gracefully
      application.addSignal("SIGINT", window.method(:onQuit))
    
      # Create it
      application.create
    
      # Run
      application.run
    end