1#!/usr/bin/env ruby 2require 'tk' 3 4begin 5 # try to use Img extension 6 require 'tkextlib/tkimg' 7rescue Exception 8 # cannot use Img extention --> ignore 9end 10 11 12############################ 13# scrolled_canvas 14class TkScrolledCanvas < TkCanvas 15 include TkComposite 16 17 def initialize_composite(keys={}) 18 @h_scr = TkScrollbar.new(@frame) 19 @v_scr = TkScrollbar.new(@frame) 20 21 @canvas = TkCanvas.new(@frame) 22 @path = @canvas.path 23 24 @canvas.xscrollbar(@h_scr) 25 @canvas.yscrollbar(@v_scr) 26 27 TkGrid.rowconfigure(@frame, 0, :weight=>1, :minsize=>0) 28 TkGrid.columnconfigure(@frame, 0, :weight=>1, :minsize=>0) 29 30 @canvas.grid(:row=>0, :column=>0, :sticky=>'news') 31 @h_scr.grid(:row=>1, :column=>0, :sticky=>'ew') 32 @v_scr.grid(:row=>0, :column=>1, :sticky=>'ns') 33 34 delegate('DEFAULT', @canvas) 35 delegate('background', @canvas, @h_scr, @v_scr) 36 delegate('activebackground', @h_scr, @v_scr) 37 delegate('troughcolor', @h_scr, @v_scr) 38 delegate('repeatdelay', @h_scr, @v_scr) 39 delegate('repeatinterval', @h_scr, @v_scr) 40 delegate('borderwidth', @frame) 41 delegate('relief', @frame) 42 43 delegate_alias('canvasborderwidth', 'borderwidth', @canvas) 44 delegate_alias('canvasrelief', 'relief', @canvas) 45 46 delegate_alias('scrollbarborderwidth', 'borderwidth', @h_scr, @v_scr) 47 delegate_alias('scrollbarrelief', 'relief', @h_scr, @v_scr) 48 49 configure(keys) unless keys.empty? 50 end 51end 52 53############################ 54class PhotoCanvas < TkScrolledCanvas 55 56USAGE = <<EOT 57--- WHAT IS --- 58You can write comments on the loaded image, and save it as a Postscipt 59file (original image file is not modified). Each comment is drawn as a 60set of an indicator circle, an arrow, and a memo text. See the following 61how to write comments. 62This can save the list of memo texts to another file. It may useful to 63search the saved Postscript file by the comments on them. 64This may not support multibyte characters (multibyte texts are broken on 65a Postscript file). It depends on features of canvas widgets of Tcl/Tk 66libraries linked your Ruby/Tk. If you use Tcl/Tk8.0-jp (Japanized Tcl/Tk), 67you can (possibly) get a Japanese Postscript file. 68 69--- BINDINGS --- 70* Button-1 : draw comments by following steps 71 1st - Set center of a indicator circle. 72 2nd - Set head position of an arrow. 73 3rd - Set tail position of an arrow, and show an entry box. 74 Input a memo text and hit 'Enter' key to entry the comment. 75 76* Button-2-drag : scroll the canvas 77 78* Button-3 : when drawing, cancel current drawing 79 80* Double-Button-3 : delete the clicked comment (text, arrow, and circle) 81EOT 82 83 def initialize(*args) 84 super(*args) 85 86 self.highlightthickness = 0 87 self.selectborderwidth = 0 88 89 @photo = TkPhotoImage.new 90 @img = TkcImage.new(self, 0, 0, :image=>@photo) 91 92 width = self.width 93 height = self.height 94 @scr_region = [-width, -height, width, height] 95 self.scrollregion(@scr_region) 96 self.xview_moveto(0.25) 97 self.yview_moveto(0.25) 98 99 @col = 'red' 100 @font = 'Helvetica -12' 101 102 @memo_id_num = -1 103 @memo_id_head = 'memo_' 104 @memo_id_tag = nil 105 @overlap_d = 2 106 107 @state = TkVariable.new 108 @border = 2 109 @selectborder = 1 110 @delta = @border + @selectborder 111 @entry = TkEntry.new(self, :relief=>:ridge, :borderwidth=>@border, 112 :selectborderwidth=>@selectborder, 113 :highlightthickness=>0) 114 @entry.bind('Return'){@state.value = 0} 115 116 @mode = old_mode = 0 117 118 _state0() 119 120 bind('2', :x, :y){|x,y| scan_mark(x,y)} 121 bind('B2-Motion', :x, :y){|x,y| scan_dragto(x,y)} 122 123 bind('3'){ 124 next if (old_mode = @mode) == 0 125 @items.each{|item| item.delete } 126 _state0() 127 } 128 129 bind('Double-3', :widget, :x, :y){|w, x, y| 130 next if old_mode != 0 131 x = w.canvasx(x) 132 y = w.canvasy(y) 133 tag = nil 134 w.find_overlapping(x - @overlap_d, y - @overlap_d, 135 x + @overlap_d, y + @overlap_d).find{|item| 136 ! (item.tags.find{|name| 137 if name =~ /^(#{@memo_id_head}\d+)$/ 138 tag = $1 139 end 140 }.empty?) 141 } 142 w.delete(tag) if tag 143 } 144 end 145 146 #----------------------------------- 147 private 148 def _state0() # init 149 @mode = 0 150 151 @memo_id_num += 1 152 @memo_id_tag = @memo_id_head + @memo_id_num.to_s 153 154 @target = nil 155 @items = [] 156 @mark = [0, 0] 157 bind_remove('Motion') 158 bind('ButtonRelease-1', proc{|x,y| _state1(x,y)}, '%x', '%y') 159 end 160 161 def _state1(x,y) # set center 162 @mode = 1 163 164 @target = TkcOval.new(self, 165 [canvasx(x), canvasy(y)], [canvasx(x), canvasy(y)], 166 :outline=>@col, :width=>3, :tags=>[@memo_id_tag]) 167 @items << @target 168 @mark = [x,y] 169 170 bind('Motion', proc{|x,y| _state2(x,y)}, '%x', '%y') 171 bind('ButtonRelease-1', proc{|x,y| _state3(x,y)}, '%x', '%y') 172 end 173 174 def _state2(x,y) # create circle 175 @mode = 2 176 177 r = Integer(Math.sqrt((x-@mark[0])**2 + (y-@mark[1])**2)) 178 @target.coords([canvasx(@mark[0] - r), canvasy(@mark[1] - r)], 179 [canvasx(@mark[0] + r), canvasy(@mark[1] + r)]) 180 end 181 182 def _state3(x,y) # set line start 183 @mode = 3 184 185 @target = TkcLine.new(self, 186 [canvasx(x), canvasy(y)], [canvasx(x), canvasy(y)], 187 :arrow=>:first, :arrowshape=>[10, 14, 5], 188 :fill=>@col, :tags=>[@memo_id_tag]) 189 @items << @target 190 @mark = [x, y] 191 192 bind('Motion', proc{|x,y| _state4(x,y)}, '%x', '%y') 193 bind('ButtonRelease-1', proc{|x,y| _state5(x,y)}, '%x', '%y') 194 end 195 196 def _state4(x,y) # create line 197 @mode = 4 198 199 @target.coords([canvasx(@mark[0]), canvasy(@mark[1])], 200 [canvasx(x), canvasy(y)]) 201 end 202 203 def _state5(x,y) # set text 204 @mode = 5 205 206 if x - @mark[0] >= 0 207 justify = 'left' 208 dx = - @delta 209 210 if y - @mark[1] >= 0 211 anchor = 'nw' 212 dy = - @delta 213 else 214 anchor = 'sw' 215 dy = @delta 216 end 217 else 218 justify = 'right' 219 dx = @delta 220 221 if y - @mark[1] >= 0 222 anchor = 'ne' 223 dy = - @delta 224 else 225 anchor = 'se' 226 dy = @delta 227 end 228 end 229 230 bind_remove('Motion') 231 232 @entry.value = '' 233 @entry.configure(:justify=>justify, :font=>@font, :foreground=>@col) 234 235 ewin = TkcWindow.new(self, [canvasx(x)+dx, canvasy(y)+dy], 236 :window=>@entry, :state=>:normal, :anchor=>anchor, 237 :tags=>[@memo_id_tag]) 238 239 @entry.focus 240 @entry.grab 241 @state.wait 242 @entry.grab_release 243 244 ewin.delete 245 246 @target = TkcText.new(self, [canvasx(x), canvasy(y)], 247 :anchor=>anchor, :justify=>justify, 248 :fill=>@col, :font=>@font, :text=>@entry.value, 249 :tags=>[@memo_id_tag]) 250 251 _state0() 252 end 253 254 #----------------------------------- 255 public 256 def load_photo(filename) 257 @photo.configure(:file=>filename) 258 end 259 260 def modified? 261 ! ((find_withtag('all') - [@img]).empty?) 262 end 263 264 def fig_erase 265 (find_withtag('all') - [@img]).each{|item| item.delete} 266 end 267 268 def reset_region 269 width = @photo.width 270 height = @photo.height 271 272 if width > @scr_region[2] 273 @scr_region[0] = -width 274 @scr_region[2] = width 275 end 276 277 if height > @scr_region[3] 278 @scr_region[1] = -height 279 @scr_region[3] = height 280 end 281 282 self.scrollregion(@scr_region) 283 self.xview_moveto(0.25) 284 self.yview_moveto(0.25) 285 end 286 287 def get_texts 288 ret = [] 289 find_withtag('all').each{|item| 290 if item.kind_of?(TkcText) 291 ret << item[:text] 292 end 293 } 294 ret 295 end 296end 297############################ 298 299# define methods for menu 300def open_file(canvas, fname) 301 if canvas.modified? 302 ret = Tk.messageBox(:icon=>'warning',:type=>'okcancel',:default=>'cancel', 303 :message=>'Canvas may be modified. Realy erase? ') 304 return if ret == 'cancel' 305 end 306 307 filetypes = [ 308 ['GIF Files', '.gif'], 309 ['GIF Files', [], 'GIFF'], 310 ['PPM Files', '.ppm'], 311 ['PGM Files', '.pgm'] 312 ] 313 314 begin 315 if Tk::Img::package_version != '' 316 filetypes << ['JPEG Files', ['.jpg', '.jpeg']] 317 filetypes << ['PNG Files', '.png'] 318 filetypes << ['PostScript Files', '.ps'] 319 filetypes << ['PDF Files', '.pdf'] 320 filetypes << ['Windows Bitmap Files', '.bmp'] 321 filetypes << ['Windows Icon Files', '.ico'] 322 filetypes << ['PCX Files', '.pcx'] 323 filetypes << ['Pixmap Files', '.pixmap'] 324 filetypes << ['SGI Files', '.sgi'] 325 filetypes << ['Sun Raster Files', '.sun'] 326 filetypes << ['TGA Files', '.tga'] 327 filetypes << ['TIFF Files', '.tiff'] 328 filetypes << ['XBM Files', '.xbm'] 329 filetypes << ['XPM Files', '.xpm'] 330 end 331 rescue 332 end 333 334 filetypes << ['ALL Files', '*'] 335 336 fpath = Tk.getOpenFile(:filetypes=>filetypes) 337 return if fpath.empty? 338 339 begin 340 canvas.load_photo(fpath) 341 rescue => e 342 Tk.messageBox(:icon=>'error', :type=>'ok', 343 :message=>"Fail to read '#{fpath}'.\n#{e.message}") 344 end 345 346 canvas.fig_erase 347 canvas.reset_region 348 349 fname.value = fpath 350end 351 352# -------------------------------- 353def save_memo(canvas, fname) 354 initname = fname.value 355 if initname != '-' 356 initname = File.basename(initname, File.extname(initname)) 357 fpath = Tk.getSaveFile(:filetypes=>[ ['Text Files', '.txt'], 358 ['ALL Files', '*'] ], 359 :initialfile=>initname) 360 else 361 fpath = Tk.getSaveFile(:filetypes=>[ ['Text Files', '.txt'], 362 ['ALL Files', '*'] ]) 363 end 364 return if fpath.empty? 365 366 begin 367 fid = open(fpath, 'w') 368 rescue => e 369 Tk.messageBox(:icon=>'error', :type=>'ok', 370 :message=>"Fail to open '#{fname.value}'.\n#{e.message}") 371 end 372 373 begin 374 canvas.get_texts.each{|txt| 375 fid.print(txt, "\n") 376 } 377 ensure 378 fid.close 379 end 380end 381 382# -------------------------------- 383def ps_print(canvas, fname) 384 initname = fname.value 385 if initname != '-' 386 initname = File.basename(initname, File.extname(initname)) 387 fpath = Tk.getSaveFile(:filetypes=>[ ['Postscript Files', '.ps'], 388 ['ALL Files', '*'] ], 389 :initialfile=>initname) 390 else 391 fpath = Tk.getSaveFile(:filetypes=>[ ['Postscript Files', '.ps'], 392 ['ALL Files', '*'] ]) 393 end 394 return if fpath.empty? 395 396 bbox = canvas.bbox('all') 397 canvas.postscript(:file=>fpath, :x=>bbox[0], :y=>bbox[1], 398 :width=>bbox[2] - bbox[0], :height=>bbox[3] - bbox[1]) 399end 400 401# -------------------------------- 402def quit(canvas) 403 ret = Tk.messageBox(:icon=>'warning', :type=>'okcancel', 404 :default=>'cancel', 405 :message=>'Realy quit? ') 406 exit if ret == 'ok' 407end 408 409# -------------------------------- 410# setup root 411root = TkRoot.new(:title=>'Fig Memo') 412 413# create canvas frame 414canvas = PhotoCanvas.new(root).pack(:fill=>:both, :expand=>true) 415usage_frame = TkFrame.new(root, :relief=>:ridge, :borderwidth=>2) 416hide_btn = TkButton.new(usage_frame, :text=>'hide usage', 417 :font=>{:size=>8}, :pady=>1, 418 :command=>proc{usage_frame.unpack}) 419hide_btn.pack(:anchor=>'e', :padx=>5) 420usage = TkLabel.new(usage_frame, :text=>PhotoCanvas::USAGE, 421 :font=>'Helvetica 8', :justify=>:left).pack 422 423show_usage = proc{ 424 usage_frame.pack(:before=>canvas, :fill=>:x, :expand=>true) 425} 426 427fname = TkVariable.new('-') 428f = TkFrame.new(root, :relief=>:sunken, :borderwidth=>1).pack(:fill=>:x) 429label = TkLabel.new(f, :textvariable=>fname, 430 :font=>{:size=>-12, :weight=>:bold}, 431 :anchor=>'w').pack(:side=>:left, :fill=>:x, :padx=>10) 432 433# create menu 434mspec = [ 435 [ ['File', 0], 436 ['Show Usage', proc{show_usage.call}, 5], 437 '---', 438 ['Open Image File', proc{open_file(canvas, fname)}, 0], 439 ['Save Memo Texts', proc{save_memo(canvas, fname)}, 0], 440 '---', 441 ['Save Postscript', proc{ps_print(canvas, fname)}, 5], 442 '---', 443 ['Quit', proc{quit(canvas)}, 0] 444 ] 445] 446root.add_menubar(mspec) 447 448# manage wm_protocol 449root.protocol(:WM_DELETE_WINDOW){quit(canvas)} 450 451# show usage 452show_usage.call 453 454# -------------------------------- 455# start eventloop 456Tk.mainloop 457