Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#!/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