#!/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 (jeroen@fox-toolkit.org)", 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) begin filename = File.join("icons", filename) icon = nil File.open(filename, "rb") do |f| icon = FXPNGIcon.new(getApp(), f.read) end icon rescue raise RuntimeError, "Couldn't load icon: #{filename}" 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