/* Pinger: a count-down ('kitchen') timer. */ /* ------------------------------------------------------------------ */ /* This is a sample stand-alone Java application with frame, menus, */ /* and a custom component that shows how to use images and graphics. */ /* This version needs Java 1.1 (or later) to run. */ /* */ /* When the timer ends, an audio clip is played. You'll only hear */ /* this if: */ /* -- you have a sound card or equivalent installed and working. */ /* -- you have speakers, headphone, etc. attached to the sound */ /* card, in range of your hearing, and switched on if need be. */ /* -- the file Pinger.au is available (this can be any sound clip */ /* in .au format, 0.5 seconds or less; a default is supplied). */ /* You can specify the name of an alternative soundclip file when the */ /* pinger is started. For example: java Pinger myclip.au */ /* ------------------------------------------------------------------ */ /* For the latest version, go to http://www2.hursley.ibm.com/netrexx */ /* ------------------------------------------------------------------ */ /* Mike Cowlishaw -- April 1996 - October 1997 */ options binary -- optional, for speed import sun.audio. -- for sound clip player /* ------------------------------------------------------------------ */ /* Pinger -- a stand-alone application for the Java Virtual Machine */ /* ------------------------------------------------------------------ */ class Pinger extends Frame- adapter implements WindowListener, ActionListener properties constant defaultclip='Pinger.au' -- default soundclip file properties private -- active dialogs -- aboutdialog=Dialog helpdialog =Dialog -- menu selection objects -- menuhelp=MenuItem("Help") menuabout=MenuItem("About") /* The 'main' method is called when this class is started as an application */ /* The optional argument is the name of the sound clip file to use */ method main(s=String[]) static Pinger("Pinger", Rexx(s)) -- make a Pinger, with title /* The constructor/initializer for Pinger */ method Pinger(title, soundclip) super(title) -- pass title to Frame addWindowListener(this) -- we want window events -- setup the logical structure of the frame createmenus -- set up menubar setLayout(BorderLayout()) -- we must have a layout timer=PingerComponent() -- make new timer add("Center", timer) -- add new timer image this.pack -- recalculate the frame -- position centre-screen sizex=200; sizey=175 screen=Toolkit.getDefaultToolkit.getScreenSize setBounds((screen.width-sizex)%2,(screen.height-sizey)%2, sizex, sizey) this.show -- make us visible timer.prime(soundclip) -- prime the audio system return -- JVM will wait for threads to end /* Create menus and menu bar */ method createmenus bar=MenuBar() -- create a MenuBar -- Help drop-down menu -- help=Menu("Help") -- create a Menu help.add(menuhelp) -- help.addSeparator -- [how to add a separator] help.add(menuabout) bar.add(help) -- add the menu to the MenuBar setMenuBar(bar) -- add the menubar to the Frame -- say we want the MenuItem events to come to us menuhelp.addActionListener(this) menuabout.addActionListener(this) /* Method for handling Button and MenuItem events */ method actionPerformed(a=ActionEvent) source=a.getSource -- the sender object if source<=MenuItem then -- .. from a MenuItem select label menuitems when source=menuhelp then do if helpdialog=null then helpdialog=PingerDialog(this, PingerDialog.HELP) helpdialog.show end when source=menuabout then do -- aboutdialog varies, so we always construct a new one aboutdialog=PingerDialog(this, PingerDialog.ABOUT) aboutdialog.show end end menuitems /* windowClosing -- called when the window is closed. We need to handle this to end the program. */ method windowClosing(e=WindowEvent) exit /* ------------------------------------------------------------------ */ /* PingerTime -- an object that holds a time, initially 00:00 */ /* ------------------------------------------------------------------ */ /* As an object, it can be protected for safe multiple-thread access. */ class PingerTime properties public -- we're just a data receptacle mm=0 -- minutes ss=0 -- seconds /* ------------------------------------------------------------------ */ /* PingerComponent -- a custom Pinger component */ /* ------------------------------------------------------------------ */ class PingerComponent extends Canvas- -- Canvas is a drawing area adapter implements MouseListener, MouseMotionListener properties constant minmm=0; maxmm=99 -- bounds minss=0; maxss=59 -- .. properties private -- Timing properties current=PingerTime() -- current time [minutes and seconds] timer =PingerTimer -- main timer, null unless started spinner=PingerSpinner -- spin-button, non-null if spinning -- Sound stuff bleeper=PingerBleeper -- a bleeper object -- Drawing and layout properties shadow=Image -- shadow image width=0; height=0 -- current picture dimensions draw=Graphics -- 'context' where we can draw background=Color.yellow -- guess what badsize =boolean 1 -- too small resizing=boolean 0 -- we are preparing new shadow spinbutw=0 -- spin button width spinbuth=10 -- spin button height [fixed] controlw=0 -- control button width controlh=24 -- control button height [fixed] gap=5 -- margin [fixed] timerect =Rectangle -- where time will go timefont ="TimesRoman" -- font face for time timepoints=0 -- pointsize for time timeweight=Font.BOLD -- weight for time timecol =Color.black -- color for time timedi =boolean -- time needs redraw buttons =6 -- number of buttons but=Rectangle[buttons] -- button rectangles -- 0/1 are Ups; 2/3 are Downs -- 4/5 are reset/start bcol=Color[buttons] -- button colors btext=String[buttons] -- button labels ben=boolean[buttons] -- button enabled bup=boolean[buttons] -- button up bdi=boolean[buttons] -- button dirty (needs redraw) over=-1 -- button we are over [-1 is none] /* Construct the component */ method PingerComponent super() addMouseListener(this) -- we want mouse events .. addMouseMotionListener(this) -- .. and mouse movements /* update -- called in reponse to repaint() */ -- We update our off-screen image here, to avoid embarrassment to the -- AWT caused by asynchronous calls to paint() method update(g=Graphics) -- redraw areas that have changed since last repaint. We draw into -- an off-screen image, later copied to the screen in a single call. loop i=0 to 5 -- redraw dirty buttons if bdi[i] then do drawbutton(i) bdi[i]=0 end end i if timedi then do drawtime(timerect) -- redraw the time timedi=0 end paint(g) /* paint -- called when the window needs to be resized or redrawn */ method paint(g=Graphics) if resizing then return -- ignore paints while recalculating if shadow=null | width<>getSize.width | height<>getSize.height then do resizing=1 newsize return end g.drawImage(shadow, 0, 0, this) -- copy to screen /* newsize -- here when a new size detected */ method newsize width=getSize.width; height=getSize.height -- The very first time that we get here, there won't be an existing -- Image, so we have to create it. It cannot be set up in advance, -- as there's no physical context earlier. shadow=createImage(width, height) -- need new image draw=shadow.getGraphics -- for graphics if height=but[b].x then if x=but[b].y then if ynull then do; spinner.halt; spinner=null; end old=over; over=new -- indicate button state change if old >=0 then do; bup[old]=1; bdi[old]=1; end if over>=0 then do; bup[over]=1; bdi[over]=1; end this.repaint /* Drag is treated just like Move (redraws button if leaves button) */ method mouseDragged(m=MouseEvent) mouseMoved(m) /* mouseReleased redraws the button, if we're over one */ method mouseReleased(m=MouseEvent) if spinner<>null then do; spinner.halt; spinner=null; end new=hit(m) if new=-1 then return -- not over a button over=new bup[over]=1; bdi[over]=1 this.repaint /* mousePressed takes an action then redraws the button, if we're over one */ method mousePressed(m=MouseEvent) new=hit(m) if new=-1 then return -- not over a button over=new bup[over]=0; bdi[over]=1 if spinner<>null then do; spinner.halt; spinner=null; end -- just in case -- [from here, all paths should initiate a repaint eventually] if \ben[over] then this.repaint -- not enabled, just repaint else select label action -- enabled, so take action when over=0 then spinner=PingerSpinner(this, 1, +1) when over=1 then spinner=PingerSpinner(this, 0, +1) when over=2 then spinner=PingerSpinner(this, 1, -1) when over=3 then spinner=PingerSpinner(this, 0, -1) when over=4 then do -- reset if timer<>null then stoptimer -- stop the timer do protect current current.mm=0; current.ss=0 -- back to zero -- [or could simply make a new PingerTime object] end newtime end when over=5 then do if timer<>null then stoptimer -- Stop or Beep state else timer=PingerTimer(this) -- Start state settextStart this.repaint end end action /* mouseEntered and mouseExited call our mouseMoved to ensure known state */ method mouseEntered(m=MouseEvent); mouseMoved(m) method mouseExited(m=MouseEvent); mouseMoved(m) /* Increase or decrease minutes and seconds numbers Arg1 is increment (+1 or -1) returns 1 if result is 00:00 */ method bumpmm(inc=int) returns boolean do protect current current.mm=current.mm+inc if current.mm>maxmm then current.mm=maxmm else if current.mmmaxss then /* carry */ do current.ss=minss current.mm=current.mm+1 if current.mm>maxmm then do; current.mm=maxmm; current.ss=maxss; end end if current.ssnull; ben[5]=ben[4] settextStart loop i=0 to 5; bdi[i]=1; end -- redraw all buttons timedi=1 -- redraw the time this.repaint -- update display return atzero /* set the text for button 5 */ method settextStart do protect current atzero=current.mm=0 & current.ss=0 -- take safe snapshot end if atzero then do if timer=null then btext[5]="Time?" else btext[5]="Beep!" end else /* nonzero */ do if timer=null then btext[5]="Start" else btext[5]="Stop" end bdi[5]=1 -- button needs redraw /* prime -- called to initialize the sound system */ method prime(filename) if filename='' then filename=Pinger.defaultclip bleeper=PingerBleeper(filename) -- make the bleeper object /* ping -- sound the bleeper once */ method ping bleeper.bleep /* stoptimer -- called when the timer is to be stopped */ method stoptimer -- closing: stop the timer last, because we may have been called from it savetimer=timer; timer=null newtime -- update display if savetimer<>null then do; savetimer.halt; savetimer=null; end return /* ------------------------------------------------------------------ */ /* PingerSpinner -- a class for the spinner Thread */ /* ------------------------------------------------------------------ */ class PingerSpinner extends Thread count=0 -- ticks owner=PingerComponent -- who we work for mins=boolean -- 1 if a minutes spinner inc=int -- up or down spin=boolean 1 -- spin allowed properties constant slow=400 -- initial delay time [ms] fast=30 -- fastest delay time [ms] over=6 -- how many ticks to accelerate over /* Construct with Arg1 is parent object Arg2 is 1=minute, 0=seconds Arg3 is increment (+1 or -1) */ method PingerSpinner(newowner=PingerComponent, newmins=boolean, newinc=int) owner=newowner; mins=newmins; inc=newinc this.start /* This runs so long as the button is held down (after which halt is set), bumping either minutes or seconds, at intervals */ method run loop until \spin -- always bump once if mins then owner.bumpmm(inc) else owner.bumpss(inc) count=count+1 if count<=over then wait=fast+(over+1-count)*(slow-fast)%over else wait=fast sleep(wait) catch InterruptedException return end /* This method is called to request that the spin stop. We use this pending request, as a direct call to stop could leave the call to owner unfinished. */ method halt spin=0 /* ------------------------------------------------------------------ */ /* PingerTimer -- a main pinger timer Thread */ /* ------------------------------------------------------------------ */ /* This thread must update the pinger in real time, so it keeps */ /* an eye on 'wall-clock' time so the timer cannot drift. Any delays */ /* will be corrected as soon as the thread gets control. */ /* */ /* When done, it notifies its owner with calls to the 'ping' method, */ /* for each bleep, and then to the 'stoptimer' method. */ class PingerTimer extends Thread owner=PingerComponent -- who we work for started=System.currentTimeMillis -- timestamp of when we were born down=boolean 1 -- count down while 1 /* Construct with Arg1 is parent object */ method PingerTimer(newowner=PingerComponent) owner=newowner this.setPriority(Thread.MAX_PRIORITY) -- time matters, here this.start -- off we go /* This runs until stopped, or we reach 0 */ method run loop millisecs=started+1000 by 1000 -- when next tick -- calculate how long to next second wait=millisecs-System.currentTimeMillis if wait<=0 then iterate -- badly behind sleep(wait) if \down then leave -- halt request if owner.bumpss(-1) then leave -- decrement and quit if 0 catch InterruptedException return end if down then loop for 3 -- reach here iff countdown not halted owner.ping sleep(600) catch InterruptedException nop end owner.stoptimer -- definitely done /* This method is called to request that the count stop. We use this pending request, as a direct call to stop could leave the bumpss call to owner unfinished. */ method halt down=0 /* ------------------------------------------------------------------ */ /* PingerBleeper -- the sound maker */ /* ------------------------------------------------------------------ */ class PingerBleeper soundfile=File -- File of The Sound okfile=boolean 0 -- 1 if the file is good input=InputStream -- .. as a stream audio=AudioStream -- .. as audio /* Constructor. Arg1 is beep file (.au) to play */ method PingerBleeper(filename) -- first ensure that the file exists soundfile=File(filename) if \soundfile.exists | \soundfile.isFile then do say 'The audio file "'filename'" could not be found' return end okfile=1 -- file is plausible -- now prime the audio subsystem by sending it a 0 byte. This loads -- the audio software so the final bleeps will be prompt (starting -- up the audio subsystem can take seconds on some platforms). do input=ByteArrayInputStream([byte 0]) -- next lines prime the audio subsystem, by trying to play an -- invalid stream audio=AudioStream(input) AudioPlayer.player.start(audio) catch IOException -- we expect to get here end /* bleep once */ method bleep if \okfile then return -- no good file to play do -- set the input stream each time ('rewind') input=FileInputStream(soundfile) audio=AudioStream(input) catch IOException say "Could not play file '"soundfile"'" okfile=0 -- don't even try next time end AudioPlayer.player.start(audio) /* ------------------------------------------------------------------ */ /* PingerDialog -- tell about us */ /* ------------------------------------------------------------------ */ class PingerDialog extends Dialog adapter implements WindowListener owner=Pinger -- who we work for form=int -- form of text -- Drawing and layout properties shadow=Image -- shadow image d=Graphics -- where we can draw width=0; height=0 -- current picture dimensions properties constant HELP=0 -- build Help text ABOUT=1 -- build About text /* Construct a general dialog for Pinger */ method PingerDialog(newowner=Pinger, newform=int) super(newowner, title(newform), 1) owner=newowner; form=newform addWindowListener(this) -- we want window events -- position centre-screen sizex=400; sizey=300 -- TV shape screen=Toolkit.getDefaultToolkit.getScreenSize this.addNotify -- ensure peer exists setBounds((screen.width-sizex)%2,(screen.height-sizey)%2, sizex, sizey) setResizable(0) -- fixed size panel, please -- display (show) will be triggered by creator /* provide a title string, on demand */ method title(titleform=int) static returns String if titleform=HELP then return "Help for Pinger" return "About Pinger" /* update -- overridden, because we set background */ method update(g=Graphics); paint(g) /* paint -- called when the window needs to be redrawn */ method paint(g=Graphics) -- we do not need to protect the image, here, as there should be -- only one. However, a race condition is possible, so we check for -- an uninitialized 'shadow'. if width<>getSize.width | height<>getSize.height then newsize else if shadow<>null then g.drawImage(shadow, 0, 0, this) /* newsize -- here when a new size detected; should be called once */ method newsize /* make a new image to draw in, if needed */ width=getSize.width; height=getSize.height shadow=this.createImage(width, height) -- make image d=shadow.getGraphics -- context to draw d.setColor(Color.white) -- background d.fillRect(0, 0, width, height) -- .. d.setFont(Font("TimesRoman", Font.PLAIN, 20)) -- measure font fm=d.getFontMetrics -- get metrics h=fm.getHeight+2 -- +2 pels leading y=(height-h*5)%3 -- offset -- Add some text, etc. Any graphics allowed, here. if form=ABOUT then do secs=int Date().getTime//7000 d.setColor(Color.getHSBColor(secs/6999, 1, 0.5)) d.drawString("Simple 'Pinger' application", 20, y+h) d.drawString("For the NetRexx source, see:", 20, y+h*2) d.drawString(" http://www2.hursley.ibm.com/netrexx/", 20, y+h*3) d.drawString("Mike Cowlishaw, 1996-1997 ", 20, y+h*4) end else /* Help */ do d.setColor(Color.blue.darker) d.drawString("This is a 'kitchen timer' application", 20, y+h) d.drawString("Set the time using the unmarked buttons.", 20, y+h*2) d.drawString("Press 'Start' to start countdown.", 20, y+h*3) d.drawString("Press 'Reset' to zero timer.", 20, y+h*4) end d.setFont(Font("Helvetica", Font.PLAIN, 12)) d.setColor(Color.black) d.drawString("Compiled with" version, 20, y+h*5) this.repaint /* windowClosing -- called when the window is closed. We need to handle this to end the program. */ method windowClosing(e=WindowEvent) owner.requestFocus this.dispose() return