#!/usr/bin/env ruby
require 'fox16'
include Fox
class ImageWindow < FXMainWindow
include Responder
def initialize(app)
# Invoke base class initialize first
super(app, "FOX Image Viewer: - untitled", :opts => DECOR_ALL, :width => 850, :height => 600)
# Recently used files list
@mrufiles = FXRecentFiles.new
# Make some icons
fileopenicon = getIcon("fileopen.png")
filesaveicon = getIcon("filesave.png")
cuticon = getIcon("cut.png")
copyicon = getIcon("copy.png")
pasteicon = getIcon("paste.png")
uplevelicon = getIcon("tbuplevel.png")
paletteicon = getIcon("colorpal.png")
# Make color dialog
colordlg = FXColorDialog.new(self, "Color Dialog")
# Make menu bar
menubar = FXMenuBar.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|FRAME_RAISED)
# Status bar
statusbar = FXStatusBar.new(self,
LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|STATUSBAR_WITH_DRAGCORNER)
# Docking sites
topDockSite = FXDockSite.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
FXDockSite.new(self, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X)
FXDockSite.new(self, LAYOUT_SIDE_LEFT|LAYOUT_FILL_Y)
FXDockSite.new(self, LAYOUT_SIDE_RIGHT|LAYOUT_FILL_Y)
# Splitter
splitter = FXSplitter.new(self, (LAYOUT_SIDE_TOP|LAYOUT_FILL_X|
LAYOUT_FILL_Y| SPLITTER_TRACKING|SPLITTER_VERTICAL|SPLITTER_REVERSED))
# Tool bar is docked inside the top one for starters
toolbarShell = FXToolBarShell.new(self)
toolbar = FXToolBar.new(topDockSite, toolbarShell,
PACK_UNIFORM_WIDTH|PACK_UNIFORM_HEIGHT|FRAME_RAISED|LAYOUT_FILL_X)
FXToolBarGrip.new(toolbar, toolbar, FXToolBar::ID_TOOLBARGRIP, TOOLBARGRIP_DOUBLE)
# File menu
filemenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&File", nil, filemenu)
# Edit Menu
editmenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&Edit", nil, editmenu)
# Manipulation Menu
manipmenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar,"&Manipulation", nil, manipmenu)
# View menu
viewmenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&View", nil, viewmenu)
# Help menu
helpmenu = FXMenuPane.new(self)
FXMenuTitle.new(menubar, "&Help", nil, helpmenu, LAYOUT_RIGHT)
# Sunken border for image widget
imagebox = FXHorizontalFrame.new(splitter,
FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y,
:padLeft => 0, :padRight => 0, :padTop => 0, :padBottom => 0)
# Make image widget
@imageview = FXImageView.new(imagebox, :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y)
# Sunken border for file list
@filebox = FXHorizontalFrame.new(splitter,
LAYOUT_FILL_X|LAYOUT_FILL_Y,
:padLeft => 0, :padRight => 0, :padTop => 0, :padBottom => 0)
# Make file list
fileframe = FXHorizontalFrame.new(@filebox,
FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y,
:padLeft => 0, :padRight => 0, :padTop => 0, :padBottom => 0, :hSpacing => 0, :vSpacing => 0)
@filelist = FXFileList.new(fileframe,
:opts => LAYOUT_FILL_X|LAYOUT_FILL_Y|ICONLIST_MINI_ICONS|ICONLIST_AUTOSIZE)
@filelist.connect(SEL_DOUBLECLICKED, method(:onCmdFileList))
FXButton.new(@filebox, "\tUp one level\tGo up to higher directory.",
uplevelicon, @filelist, FXFileList::ID_DIRECTORY_UP,
BUTTON_TOOLBAR|FRAME_RAISED|LAYOUT_FILL_Y)
# Toobar buttons: File manipulation
openBtn = FXButton.new(toolbar, "&Open\tOpen Image\tOpen image file.", fileopenicon,
:opts => ICON_ABOVE_TEXT|BUTTON_TOOLBAR|FRAME_RAISED)
openBtn.connect(SEL_COMMAND, method(:onCmdOpen))
saveBtn = FXButton.new(toolbar, "&Save\tSave Image\tSave image file.", filesaveicon,
:opts => ICON_ABOVE_TEXT|BUTTON_TOOLBAR|FRAME_RAISED)
saveBtn.connect(SEL_COMMAND, method(:onCmdSave))
# Toobar buttons: Editing
FXButton.new(toolbar, "Cut\tCut", cuticon,
:opts => ICON_ABOVE_TEXT|BUTTON_TOOLBAR|FRAME_RAISED)
FXButton.new(toolbar, "Copy\tCopy", copyicon,
:opts => ICON_ABOVE_TEXT|BUTTON_TOOLBAR|FRAME_RAISED)
FXButton.new(toolbar, "Paste\tPaste", pasteicon,
:opts => ICON_ABOVE_TEXT|BUTTON_TOOLBAR|FRAME_RAISED)
# Color
FXButton.new(toolbar, "&Colors\tColors\tDisplay color dialog.", paletteicon,
colordlg, FXWindow::ID_SHOW,
ICON_ABOVE_TEXT|BUTTON_TOOLBAR|FRAME_RAISED|LAYOUT_RIGHT)
# File Menu entries
FXMenuCommand.new(filemenu, "&Open...\tCtl-O\tOpen image file.", fileopenicon).connect(SEL_COMMAND, method(:onCmdOpen))
FXMenuCommand.new(filemenu, "&Save...\tCtl-S\tSave image file.", filesaveicon).connect(SEL_COMMAND, method(:onCmdSave))
FXMenuCommand.new(filemenu, "Dump", nil, getApp(), FXApp::ID_DUMP)
# Recent file menu; this automatically hides if there are no files
sep1 = FXMenuSeparator.new(filemenu)
sep1.target = @mrufiles
sep1.selector = FXRecentFiles::ID_ANYFILES
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_1)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_2)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_3)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_4)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_5)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_6)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_7)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_8)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_9)
FXMenuCommand.new(filemenu, nil, nil, @mrufiles, FXRecentFiles::ID_FILE_10)
FXMenuCommand.new(filemenu, "&Clear Recent Files", nil,
@mrufiles, FXRecentFiles::ID_CLEAR)
sep2 = FXMenuSeparator.new(filemenu)
sep2.target = @mrufiles
sep2.selector = FXRecentFiles::ID_ANYFILES
FXMenuCommand.new(filemenu, "&Quit\tCtl-Q").connect(SEL_COMMAND, method(:onCmdQuit))
# Edit Menu entries
FXMenuCommand.new(editmenu, "&Undo\tCtl-Z\tUndo last change.")
FXMenuCommand.new(editmenu, "&Redo\tCtl-R\tRedo last undo.")
FXMenuCommand.new(editmenu, "&Copy\tCtl-C\tCopy selection to clipboard.", copyicon)
FXMenuCommand.new(editmenu, "C&ut\tCtl-X\tCut selection to clipboard.", cuticon)
FXMenuCommand.new(editmenu, "&Paste\tCtl-V\tPaste from clipboard.", pasteicon)
FXMenuCommand.new(editmenu, "&Delete\t\tDelete selection.")
# Manipulation Menu entries
rotate90Cmd = FXMenuCommand.new(manipmenu, "Rotate 90\t\tRotate 90 degrees.")
rotate90Cmd.connect(SEL_COMMAND) { @imageview.image.rotate(90) }
rotate90Cmd.connect(SEL_UPDATE, method(:onUpdImage))
rotate180Cmd = FXMenuCommand.new(manipmenu, "Rotate 180\t\tRotate 180 degrees.")
rotate180Cmd.connect(SEL_COMMAND) { @imageview.image.rotate(180) }
rotate180Cmd.connect(SEL_UPDATE, method(:onUpdImage))
rotate270Cmd = FXMenuCommand.new(manipmenu, "Rotate -90\t\tRotate -90 degrees.")
rotate270Cmd.connect(SEL_COMMAND) { @imageview.image.rotate(270) }
rotate270Cmd.connect(SEL_UPDATE, method(:onUpdImage))
mirrorHorCmd = FXMenuCommand.new(manipmenu, "Mirror Hor.\t\tMirror Horizontally.")
mirrorHorCmd.connect(SEL_COMMAND) { @imageview.image.mirror(true, false) }
mirrorHorCmd.connect(SEL_UPDATE, method(:onUpdImage))
mirrorVerCmd = FXMenuCommand.new(manipmenu, "Mirror Ver.\t\tMirror Vertically.")
mirrorVerCmd.connect(SEL_COMMAND) { @imageview.image.mirror(false, true) }
mirrorVerCmd.connect(SEL_UPDATE, method(:onUpdImage))
scaleCmd = FXMenuCommand.new(manipmenu, "Scale...\t\tScale image.")
scaleCmd.connect(SEL_COMMAND, method(:onCmdScale))
scaleCmd.connect(SEL_UPDATE, method(:onUpdImage))
cropCmd = FXMenuCommand.new(manipmenu, "Crop...\t\tCrop image.")
cropCmd.connect(SEL_COMMAND, method(:onCmdCrop))
cropCmd.connect(SEL_UPDATE, method(:onUpdImage))
# View Menu entries
FXMenuCheck.new(viewmenu, "File list\t\tDisplay file list.",
@filebox, FXWindow::ID_TOGGLESHOWN)
FXMenuCheck.new(viewmenu,
"Show hidden files\t\tShow hidden files and directories.",
@filelist, FXFileList::ID_TOGGLE_HIDDEN)
FXMenuSeparator.new(viewmenu)
FXMenuRadio.new(viewmenu,
"Show small icons\t\tDisplay directory with small icons.",
@filelist, FXFileList::ID_SHOW_MINI_ICONS)
FXMenuRadio.new(viewmenu,
"Show big icons\t\tDisplay directory with big icons.",
@filelist, FXFileList::ID_SHOW_BIG_ICONS)
FXMenuRadio.new(viewmenu,
"Show details view\t\tDisplay detailed directory listing.",
@filelist, FXFileList::ID_SHOW_DETAILS)
FXMenuSeparator.new(viewmenu)
FXMenuRadio.new(viewmenu, "Rows of icons\t\tView row-wise.",
@filelist, FXFileList::ID_ARRANGE_BY_ROWS)
FXMenuRadio.new(viewmenu, "Columns of icons\t\tView column-wise.",
@filelist,FXFileList::ID_ARRANGE_BY_COLUMNS)
FXMenuSeparator.new(viewmenu)
FXMenuCheck.new(viewmenu, "Toolbar\t\tDisplay toolbar.",
toolbar, FXWindow::ID_TOGGLESHOWN)
FXMenuCommand.new(viewmenu, "Float toolbar\t\tUndock the toolbar.", nil, toolbar, FXToolBar::ID_DOCK_FLOAT)
FXMenuCommand.new(viewmenu, "Dock toolbar top\t\tDock the toolbar on the top.", nil, toolbar, FXToolBar::ID_DOCK_TOP)
FXMenuCommand.new(viewmenu, "Dock toolbar left\t\tDock the toolbar on the left.", nil, toolbar, FXToolBar::ID_DOCK_LEFT)
FXMenuCommand.new(viewmenu, "Dock toolbar right\t\tDock the toolbar on the right.", nil, toolbar, FXToolBar::ID_DOCK_RIGHT)
FXMenuCommand.new(viewmenu, "Dock toolbar bottom\t\tDock the toolbar on the bottom.", nil, toolbar, FXToolBar::ID_DOCK_BOTTOM)
FXMenuSeparator.new(viewmenu)
FXMenuCheck.new(viewmenu, "Status line\t\tDisplay status line.",
statusbar, FXWindow::ID_TOGGLESHOWN)
# Help Menu entries
FXMenuCommand.new(helpmenu, "&About FOX...").connect(SEL_COMMAND) {
FXMessageBox.new(self, "About Image Viewer",
"Image Viewer demonstrates the FOX ImageView widget.\n\n" +
"Using the FOX C++ GUI Library (http://www.fox-toolkit.org)\n\n" +
"Copyright (C) 2000 Jeroen van der Zijp ([email protected])", nil,
MBOX_OK|DECOR_TITLE|DECOR_BORDER).execute
}
# Make a tool tip
FXToolTip.new(getApp(), TOOLTIP_NORMAL)
# Recent files
@mrufiles.connect(SEL_COMMAND) do |sender, sel, filename|
@filename = filename
@filelist.currentFile = @filename
loadImage(@filename)
end
# Initialize file name and pattern for file dialog
@filename = "untitled"
@preferredFileFilter = 0
end
# Convenience function to construct a PNG icon
def getIcon(filename)
filename = File.expand_path("../icons/#{filename}", __FILE__)
File.open(filename, "rb") do |f|
FXPNGIcon.new(getApp(), f.read)
end
end
def hasExtension?(filename, ext)
File.basename(filename, ext) != File.basename(filename)
end
# Load the named image file
def loadImage(file)
img = nil
if hasExtension?(file, ".gif")
img = FXGIFImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".bmp")
img = FXBMPImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".xpm")
img = FXXPMImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".png")
img = FXPNGImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".jpg")
img = FXJPGImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".pcx")
img = FXPCXImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".tif")
img = FXTIFImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".tga")
img = FXTGAImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
elsif hasExtension?(file, ".ico")
img = FXICOImage.new(getApp(), nil, IMAGE_KEEP|IMAGE_SHMI|IMAGE_SHMP)
end
# Perhaps failed?
if img.nil?
FXMessageBox.error(self, MBOX_OK, "Error loading image",
"Unsupported image type: #{file}")
return
end
# Load it...
getApp().beginWaitCursor do
FXFileStream.open(file, FXStreamLoad) { |stream| img.loadPixels(stream) }
img.create
@imageview.image = img
end
end
# Save image to named file
def saveImage(file)
getApp().beginWaitCursor do
FXFileStream.open(file, FXStreamSave) { |stream| @imageview.image.savePixels(stream) }
end
end
# Open a new file
def onCmdOpen(sender, sel, ptr)
openDialog = FXFileDialog.new(self, "Open Image")
openDialog.filename = @filename
patterns = ["All Files (*)",
"GIF Image (*.gif)",
"BMP Image (*.bmp)",
"XPM Image (*.xpm)",
"PCX Image (*.pcx)",
"ICO Image (*.ico)",
"PNG Image (*.png)",
"JPEG Image (*.jpg)",
"TIFF Image (*.tif)",
"TARGA Image (*.tga)"
]
openDialog.patternList = patterns
openDialog.currentPattern = @preferredFileFilter
if openDialog.execute != 0
@preferredFileFilter = openDialog.currentPattern
@filename = openDialog.filename
@filelist.currentFile = @filename
@mrufiles.appendFile(@filename)
loadImage(@filename)
end
return 1
end
# Save this file
def onCmdSave(sender, sel, ptr)
saveDialog = FXFileDialog.new(self, "Save Image")
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
@filename = saveDialog.filename
@filelist.currentFile = @filename
@mrufiles.appendFile(@filename)
saveImage(@filename)
end
return 1
end
# Quit the application
def onCmdQuit(sender, sel, ptr)
# Write new window size back to registry
getApp().reg().writeIntEntry("SETTINGS", "x", x)
getApp().reg().writeIntEntry("SETTINGS", "y", y)
getApp().reg().writeIntEntry("SETTINGS", "width", width)
getApp().reg().writeIntEntry("SETTINGS", "height", height)
# Height of file list
getApp().reg().writeIntEntry("SETTINGS", "fileheight", @filebox.height)
# Was file box shown?
getApp().reg().writeBoolEntry("SETTINGS", "filesshown", @filebox.shown)
# Current directory
getApp().reg().writeStringEntry("SETTINGS", "directory", @filelist.directory)
# Quit
getApp().exit(0)
end
# Command message from the file list
def onCmdFileList(sender, sel, index)
if index >= 0
if @filelist.isItemDirectory(index)
@filelist.directory = @filelist.getItemPathname(index)
elsif @filelist.isItemFile(index)
@filename = @filelist.getItemPathname(index)
@mrufiles.appendFile(@filename)
loadImage(@filename)
end
end
return 1
end
# Scale
def onCmdScale(sender, sel, ptr)
image = @imageview.image
sx = FXDataTarget.new(image.width)
sy = FXDataTarget.new(image.height)
scalepanel = FXDialogBox.new(self, "Scale Image To Size")
frame = FXHorizontalFrame.new(scalepanel,
LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
FXLabel.new(frame, "W:", nil, LAYOUT_CENTER_Y)
FXTextField.new(frame, 5, sx, FXDataTarget::ID_VALUE,
LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
FXLabel.new(frame, "H:", nil, LAYOUT_CENTER_Y)
FXTextField.new(frame, 5, sy, FXDataTarget::ID_VALUE,
LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
FXButton.new(frame, "Cancel", nil, scalepanel, FXDialogBox::ID_CANCEL,
LAYOUT_CENTER_Y|FRAME_RAISED|FRAME_THICK,
:padLeft => 20, :padRight => 20, :padTop => 4, :padBottom => 4)
FXButton.new(frame, "OK", nil, scalepanel, FXDialogBox::ID_ACCEPT,
LAYOUT_CENTER_Y|FRAME_RAISED|FRAME_THICK,
:padLeft => 30, :padRight => 30, :padTop => 4, :padBottom => 4)
return 1 if (scalepanel.execute == 0)
return 1 if (sx.value < 1 || sy.value < 1)
image.scale(sx.value, sy.value)
@imageview.image = image
end
# Crop
def onCmdCrop(sender, sel, ptr)
image = @imageview.image
cx = FXDataTarget.new(0)
cy = FXDataTarget.new(0)
cw = FXDataTarget.new(image.width)
ch = FXDataTarget.new(image.height)
croppanel = FXDialogBox.new(self, "Crop image")
frame = FXHorizontalFrame.new(croppanel,
LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
FXLabel.new(frame, "X:", nil, LAYOUT_CENTER_Y)
FXTextField.new(frame, 5, cx, FXDataTarget::ID_VALUE,
LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
FXLabel.new(frame, "Y:", nil, LAYOUT_CENTER_Y)
FXTextField.new(frame, 5, cy, FXDataTarget::ID_VALUE,
LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
FXLabel.new(frame, "W:", nil, LAYOUT_CENTER_Y)
FXTextField.new(frame, 5, cw, FXDataTarget::ID_VALUE,
LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
FXLabel.new(frame, "H:", nil, LAYOUT_CENTER_Y)
FXTextField.new(frame, 5, ch, FXDataTarget::ID_VALUE,
LAYOUT_CENTER_Y|FRAME_SUNKEN|FRAME_THICK|JUSTIFY_RIGHT)
FXButton.new(frame, "Cancel", nil, croppanel, FXDialogBox::ID_CANCEL,
LAYOUT_CENTER_Y|FRAME_RAISED|FRAME_THICK,
:padLeft => 20, :padRight => 20, :padTop => 4, :padBottom => 4)
FXButton.new(frame, "OK", nil, croppanel, FXDialogBox::ID_ACCEPT,
LAYOUT_CENTER_Y|FRAME_RAISED|FRAME_THICK,
:padLeft => 30, :padRight => 30, :padTop => 4, :padBottom => 4)
return 1 if (croppanel.execute == 0)
return 1 if (cx.value < 0 || cy.value < 0 ||
cx.value+cw.value > image.width ||
cy.value+ch.value > image.height)
image.crop(cx.value, cy.value, cw.value, ch.value)
@imageview.image = image
end
# Update image
def onUpdImage(sender, sel, ptr)
if @imageview.image
sender.handle(self, FXSEL(SEL_COMMAND, FXWindow::ID_ENABLE), nil)
else
sender.handle(self, FXSEL(SEL_COMMAND, FXWindow::ID_DISABLE), nil)
end
end
# Create and show window
def create
# Get size, etc. from registry
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)
fh = getApp().reg().readIntEntry("SETTINGS", "fileheight", 100)
fs = getApp().reg().readBoolEntry("SETTINGS", "filesshown", true)
dir = getApp().reg().readStringEntry("SETTINGS", "directory", "~")
# Starting directory for the files list
@filelist.directory = dir
# Height of files list
@filebox.height = fh
# Is it visible right now?
if !fs
@filebox.hide
end
# Reposition window to specified x, y, w and h
position(xx, yy, ww, hh)
# Create and show
super # i.e. FXMainWindow::create()
show(PLACEMENT_SCREEN)
end
end
if __FILE__ == $0
# Make application
application = FXApp.new("ImageViewer", "FoxTest")
# Make window
window = ImageWindow.new(application)
# Handle interrupts to terminate program gracefully
application.addSignal("SIGINT", window.method(:onCmdQuit))
# Create it
application.create
# Passed image file?
if ARGV.length > 0
window.loadImage(ARGV[0])
end
# Run
application.run
end