#!/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 = < DECOR_ALL, :width => 850, :height => 600, :padLeft => 0, :padRight => 0) # Icons for list items File.open(File.expand_path("../icons/bluebullet14x14.gif", __FILE__), "rb") do |f| bytes = f.read @itemIcon = FXGIFIcon.new(getApp(), bytes) end File.open(File.expand_path("../icons/transpbullet14x14.gif", __FILE__), "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.exist? 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