2017-12-28 16:40:15 +00:00
/ *
* Copyright ( C ) 2012 Brendan Robert ( BLuRry ) brendan . robert @gmail.com.
*
* This library is free software ; you can redistribute it and / or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation ; either
* version 2 . 1 of the License , or ( at your option ) any later version .
*
* This library is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the GNU
* Lesser General Public License for more details .
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston ,
* MA 02110 - 1301 USA
* /
package jace.hardware ;
import jace.EmulatorUILogic ;
import jace.apple2e.MOS65C02 ;
import jace.apple2e.RAM128k ;
import jace.config.ConfigurableField ;
import jace.config.Name ;
import jace.core.Card ;
import jace.core.Computer ;
import jace.core.PagedMemory ;
import jace.core.RAMEvent ;
import jace.core.RAMEvent.TYPE ;
import jace.core.Utility ;
2023-07-03 20:44:23 +00:00
import jace.state.Stateful ;
2017-12-28 16:40:15 +00:00
import javafx.event.EventHandler ;
import javafx.geometry.Bounds ;
2023-07-03 20:44:23 +00:00
import javafx.geometry.Point2D ;
import javafx.geometry.Rectangle2D ;
2017-12-28 16:40:15 +00:00
import javafx.scene.Node ;
import javafx.scene.control.Label ;
import javafx.scene.input.MouseButton ;
import javafx.scene.input.MouseEvent ;
/ * *
* Apple Mouse interface implementation . This is fully compatible with several
* applications .
*
* @author Brendan Robert ( BLuRry ) brendan . robert @gmail.com
* /
@Stateful
@Name ( " Apple Mouse " )
public class CardAppleMouse extends Card {
@Stateful
public int mode ;
@Stateful
public boolean active ;
@Stateful
public boolean interruptOnMove ;
@Stateful
public boolean interruptOnPress ;
@Stateful
public boolean interruptOnVBL ;
@Stateful
public boolean button0press ;
@Stateful
public boolean button1press ;
@Stateful
public boolean button0pressLast ;
@Stateful
public boolean button1pressLast ;
@Stateful
public boolean isInterrupt ;
@Stateful
public boolean isVBL ;
@Stateful
public int statusByte ;
@Stateful
public Point2D lastMouseLocation ;
@Stateful
public Rectangle2D clampWindow = new Rectangle2D ( 0 , 0 , 0x03ff , 0x03ff ) ;
// By default, update 60 times a second -- roughly every VBL period (in theory)
@ConfigurableField ( name = " Update frequency " , shortName = " updateFreq " , category = " Mouse " , description = " # of CPU cycles between updates; affects polling and interrupt-based routines " )
public static int CYCLES_PER_UPDATE = ( int ) ( 1020484L / 60L ) ;
@ConfigurableField ( name = " Fullscreen fix " , shortName = " fsfix " , category = " Mouse " , description = " If the mouse pointer is a little off when in fullscreen, this should fix it. " )
public boolean fullscreenFix = true ;
@ConfigurableField ( name = " Blazing Paddles fix " , shortName = " bpfix " , category = " Mouse " , description = " Use different clamping values to make Blazing Paddles work more reliably. " )
public boolean blazingPaddles = false ;
Label mouseActive = Utility . loadIconLabel ( " input-mouse.png " ) . orElse ( null ) ;
public boolean movedSinceLastTick = false ;
public boolean movedSinceLastRead = false ;
public CardAppleMouse ( Computer computer ) {
super ( computer ) ;
}
@Override
public String getDeviceName ( ) {
return " Apple Mouse " ;
}
@Override
public void reset ( ) {
mode = 0 ;
clampWindow = new Rectangle2D ( 0 , 0 , 0x03ff , 0x03ff ) ;
deactivateMouse ( ) ;
}
EventHandler < MouseEvent > mouseHandler = this : : processMouseEvent ;
private void processMouseEvent ( MouseEvent event ) {
if ( event . getEventType ( ) = = MouseEvent . MOUSE_MOVED | | event . getEventType ( ) = = MouseEvent . MOUSE_DRAGGED ) {
Node source = ( Node ) event . getSource ( ) ;
updateLocation ( event . getSceneX ( ) , event . getSceneY ( ) , source . getBoundsInLocal ( ) ) ;
event . consume ( ) ;
}
if ( event . getEventType ( ) = = MouseEvent . MOUSE_PRESSED | | event . getEventType ( ) = = MouseEvent . MOUSE_DRAGGED ) {
mousePressed ( event ) ;
event . consume ( ) ;
} else if ( event . getEventType ( ) = = MouseEvent . MOUSE_RELEASED ) {
mouseReleased ( event ) ;
event . consume ( ) ;
}
}
private void updateLocation ( double x , double y , Bounds bounds ) {
double scaledX = x / bounds . getWidth ( ) ;
double scaledY = y / bounds . getHeight ( ) ;
lastMouseLocation = new Point2D ( scaledX , scaledY ) ;
movedSinceLastTick = true ;
movedSinceLastRead = true ;
}
/ *
* Coded against this information
* http : //stason.org/TULARC/pc/apple2/programmer/012-How-do-I-write-programs-which-use-the-mouse.html
* /
@Override
protected void handleFirmwareAccess ( int offset , TYPE type , int value , RAMEvent e ) {
/ *
* Screen holes
* $0478 + slot Low byte of absolute X position
* $04F8 + slot Low byte of absolute Y position
* $0578 + slot High byte of absolute X position
* $05F8 + slot High byte of absolute Y position
* $0678 + slot Reserved and used by the firmware
* $06F8 + slot Reserved and used by the firmware
* $0778 + slot Button 0 / 1 interrupt status byte
* $07F8 + slot Mode byte
*
* Interrupt status byte :
* Set by READMOUSE
* Bit 7 6 5 4 3 2 1 0
* | | | | | | | |
* | | | | | | | ` - - - Previously , button 1 was up ( 0 ) or down ( 1 )
* | | | | | | ` - - - - - Movement interrupt
* | | | | | ` - - - - - - - Button 0 / 1 interrupt
* | | | | ` - - - - - - - - - VBL interrupt
* | | | ` - - - - - - - - - - - Currently , button 1 is up ( 0 ) or down ( 1 )
* | | ` - - - - - - - - - - - - - X / Y moved since last READMOUSE
* | ` - - - - - - - - - - - - - - - Previously , button 0 was up ( 0 ) or down ( 1 )
* ` - - - - - - - - - - - - - - - - - Currently , button 0 is up ( 0 ) or down ( 1 )
*
* Mode byte
* Valid after calling SERVEMOUSE , cleared with READMOUSE
* Bit 7 6 5 4 3 2 1 0
* | | | | | | | |
* | | | | | | | ` - - - Mouse off ( 0 ) or on ( 1 )
* | | | | | | ` - - - - - Interrupt if mouse is moved
* | | | | | ` - - - - - - - Interrupt if button is pressed
* | | | | ` - - - - - - - - - Interrupt on VBL
* | | | ` - - - - - - - - - - - Reserved
* | | ` - - - - - - - - - - - - - Reserved
* | ` - - - - - - - - - - - - - - - Reserved
* ` - - - - - - - - - - - - - - - - - Reserved
* /
if ( type = = RAMEvent . TYPE . EXECUTE ) {
// This means the CPU is calling firmware at this location
switch ( offset - 0x080 ) {
case 0 :
setMouse ( ) ;
break ;
case 1 :
serveMouse ( ) ;
break ;
case 2 :
readMouse ( ) ;
break ;
case 3 :
clearMouse ( ) ;
break ;
case 4 :
posMouse ( ) ;
break ;
case 5 :
clampMouse ( ) ;
break ;
case 6 :
homeMouse ( ) ;
break ;
case 7 :
initMouse ( ) ;
break ;
case 8 :
getMouseClamp ( ) ;
break ;
}
// Always pass back RTS
e . setNewValue ( 0x060 ) ;
} else if ( type . isRead ( ) ) {
/ * Identification bytes
* $Cn05 = $38 $Cn07 = $18 $Cn0B = $01 $Cn0C = $20 $CnFB = $D6
* /
switch ( offset ) {
case 0x05 :
e . setNewValue ( 0x038 ) ;
break ;
case 0x07 :
e . setNewValue ( 0x018 ) ;
break ;
case 0x0B :
e . setNewValue ( 0x01 ) ;
break ;
case 0x0C :
e . setNewValue ( 0x020 ) ;
break ;
case 0x0FB :
e . setNewValue ( 0x0D6 ) ;
break ;
// As per the //gs firmware reference manual
case 0x08 :
// Pascal signature byte
e . setNewValue ( 0x001 ) ;
case 0x011 :
e . setNewValue ( 0x000 ) ;
break ;
// Function call offsets
case 0x12 :
e . setNewValue ( 0x080 ) ;
break ;
case 0x13 :
e . setNewValue ( 0x081 ) ;
break ;
case 0x14 :
e . setNewValue ( 0x082 ) ;
break ;
case 0x15 :
e . setNewValue ( 0x083 ) ;
break ;
case 0x16 :
e . setNewValue ( 0x084 ) ;
break ;
case 0x17 :
e . setNewValue ( 0x085 ) ;
break ;
case 0x18 :
e . setNewValue ( 0x086 ) ;
break ;
case 0x19 :
e . setNewValue ( 0x087 ) ;
break ;
case 0x1A :
e . setNewValue ( 0x088 ) ;
break ;
default :
e . setNewValue ( 0x069 ) ;
}
// System.out.println("Read mouse firmware at "+Integer.toHexString(e.getAddress())+" == "+Integer.toHexString(e.getNewValue()));
}
}
private MOS65C02 getCPU ( ) {
return ( MOS65C02 ) computer . getCpu ( ) ;
}
/ *
* $Cn12 SETMOUSE Sets mouse mode
* A = mouse operation mode ( 0 - f )
* C = 1 if illegal mode requested
* mode byte updated
* /
private void setMouse ( ) {
mode = getCPU ( ) . A & 0x0ff ;
if ( mode > 0x0f ) {
getCPU ( ) . C = 1 ;
return ;
} else {
getCPU ( ) . C = 0 ;
}
//Interrupt on VBL
interruptOnVBL = ( ( mode & 8 ) ! = 0 ) ;
//Mouse off (0) or on (1)
if ( ( mode & 1 ) = = 0 ) {
deactivateMouse ( ) ;
interruptOnMove = false ;
interruptOnPress = false ;
return ;
}
//Interrupt if mouse is moved
interruptOnMove = ( ( mode & 2 ) ! = 0 ) ;
//Interrupt if button is pressed
interruptOnPress = ( ( mode & 4 ) ! = 0 ) ;
activateMouse ( ) ;
}
/ *
* $Cn13 SERVEMOUSE Services mouse interrupt
* Test for interupt and clear mouse interrupt line
* Return C = 0 if mouse interrupt occurred
* Updates screen hole interrupt status bits
* /
private void serveMouse ( ) {
// If any interrupts are registered then
updateMouseState ( ) ;
if ( isInterrupt ) {
getCPU ( ) . C = 0 ;
} else {
getCPU ( ) . C = 1 ;
// System.out.println("MOUSE TRIGGERED INTERRUPT!");
}
// isInterrupt = false;
// isVBL=false;
}
/ *
* $Cn14 READMOUSE Reads mouse position
* Reads delta ( X / Y ) positions , updates abolute X / Y pos
* and reads button statuses
* Always returns C = 0
* Interrupt status bits cleared
* Screen hole positions for button / movement status bits updated
* /
private void readMouse ( ) {
updateMouseState ( ) ;
isInterrupt = false ;
isVBL = false ;
// set screen holes
getCPU ( ) . C = 0 ;
}
/ *
* $Cn16 POSMOUSE Sets mouse position to a user - defined pos
* Caller puts new position in screenhole
* Always returns C = 0
* /
private void posMouse ( ) {
// Ignore?
getCPU ( ) . C = 0 ;
}
/ *
* $Cn17 CLAMPMOUSE Sets mouse bounds in a window
* Sets up clamping window for mouse user
* Power up defaults are 0 - 1023 ( 0 - 3ff )
* Caller sets :
* A = 0 if setting X , 1 if setting Y
* $0478 = low byte of low clamp .
* $04F8 = low byte of high clamp .
* $0578 = high byte of low clamp .
* $05F8 = high byte of high clamp .
* //gs homes mouse to low address, but //c and //e do not
* /
private void clampMouse ( ) {
2023-07-03 20:44:23 +00:00
RAM128k memory = ( RAM128k ) computer . getMemory ( ) ;
2017-12-28 16:40:15 +00:00
byte clampMinLo = memory . getMainMemory ( ) . readByte ( 0x0478 ) ;
byte clampMaxLo = memory . getMainMemory ( ) . readByte ( 0x04F8 ) ;
byte clampMinHi = memory . getMainMemory ( ) . readByte ( 0x0578 ) ;
byte clampMaxHi = memory . getMainMemory ( ) . readByte ( 0x05F8 ) ;
int min = ( clampMinLo & 0x0ff ) | ( ( clampMinHi < < 8 ) & 0x0FF00 ) ;
int max = ( clampMaxLo & 0x0ff ) | ( ( clampMaxHi < < 8 ) & 0x0FF00 ) ;
if ( min > = 32768 ) {
min - = 65536 ;
}
if ( max > = 32768 ) {
max - = 65536 ;
}
if ( getCPU ( ) . A = = 0 ) {
if ( blazingPaddles ) {
min = - 1 ;
max = 281 ;
}
setClampWindowX ( min , max ) ;
} else {
if ( blazingPaddles ) {
min = - 1 ;
max = 193 ;
}
setClampWindowY ( min , max ) ;
}
}
/ *
* $Cn19 INITMOUSE Resets mouse clamps to default values ; sets mouse position to 0 , 0
* Sets screen holes to default values and sets clamping
* window to default value ( 000 - 3ff ) for both X and Y
* Exit : C = 0
* Screen holes are updated
* /
private void initMouse ( ) {
mouseActive . setText ( " Active " ) ;
EmulatorUILogic . addIndicator ( this , mouseActive , 2000 ) ;
setClampWindowX ( 0 , 0x3ff ) ;
setClampWindowY ( 0 , 0x3ff ) ;
clearMouse ( ) ;
}
/ *
* $Cn15 CLEARMOUSE Clears mouse position to 0 ( for delta mode )
* Resets buttons , movement and interrupt status bits to 0
* Intended to be used for delta mouse positioning instead of absolute positioning
* Always returns C = 0
* Interrupt status bits cleared
* Screen hole positions for button / movement status bits updated
* /
private void clearMouse ( ) {
isVBL = false ;
isInterrupt = false ;
button0press = false ;
button1press = false ;
button0pressLast = false ;
button1pressLast = false ;
homeMouse ( ) ;
}
/ *
* $Cn18 HOMEMOUSE Sets absolute position to upper - left corner of clamping window
* Exit : c = 0
* Screen hole positions are updated
* /
private void homeMouse ( ) {
lastMouseLocation = new Point2D ( 0 , 0 ) ;
updateMouseState ( ) ;
getCPU ( ) . C = 0 ;
}
/ * *
* Described in Apple Mouse technical note # 7
* Cn1A : Read mouse clamping values
* Register number is stored in $478 and ranges from x47 to x4e
* Return value should be stored in $5782
* Values should be returned in this order :
* MinXH , MinYH , MinXL , MinYL , MaxXH , MaxYH , MaxXL , MaxYL
* /
private void getMouseClamp ( ) {
byte reg = computer . getMemory ( ) . readRaw ( 0x0478 ) ;
byte val = 0 ;
switch ( reg - 0x047 ) {
2023-07-03 20:44:23 +00:00
case 0 - > val = ( byte ) ( ( int ) clampWindow . getMinX ( ) > > 8 ) ;
case 1 - > val = ( byte ) ( ( int ) clampWindow . getMinY ( ) > > 8 ) ;
case 2 - > val = ( byte ) ( ( int ) clampWindow . getMinX ( ) & 255 ) ;
case 3 - > val = ( byte ) ( ( int ) clampWindow . getMinY ( ) & 255 ) ;
case 4 - > val = ( byte ) ( ( int ) clampWindow . getMaxX ( ) > > 8 ) ;
case 5 - > val = ( byte ) ( ( int ) clampWindow . getMaxY ( ) > > 8 ) ;
case 6 - > val = ( byte ) ( ( int ) clampWindow . getMaxX ( ) & 255 ) ;
case 7 - > val = ( byte ) ( ( int ) clampWindow . getMaxY ( ) & 255 ) ;
2017-12-28 16:40:15 +00:00
}
computer . getMemory ( ) . write ( 0x0578 , val , false , false ) ;
}
/ *
* This is called whenever the mouse firmware has been activated in software
* /
private void activateMouse ( ) {
active = true ;
EmulatorUILogic . addMouseListener ( mouseHandler ) ;
}
/ *
* This is called whenever there is a hard reset or when the mouse is turned off
* /
private void deactivateMouse ( ) {
active = false ;
mode = 0 ;
interruptOnMove = false ;
interruptOnPress = false ;
EmulatorUILogic . removeMouseListener ( mouseHandler ) ;
}
@Override
protected void handleIOAccess ( int register , TYPE type , int value , RAMEvent e ) {
// No IO access necessary (is there?)
}
private int delay = CYCLES_PER_UPDATE ;
@Override
public void tick ( ) {
if ( ! active ) {
return ;
}
delay - - ;
if ( delay > 0 ) {
return ;
}
delay = CYCLES_PER_UPDATE ;
// If interrupts not used, just move on
if ( ! interruptOnMove & & ! interruptOnPress ) {
return ;
}
if ( interruptOnPress ) {
if ( button0press ! = button0pressLast | | button1press ! = button1pressLast ) {
isInterrupt = true ;
getCPU ( ) . generateInterrupt ( ) ;
return ;
}
}
if ( interruptOnMove ) {
if ( movedSinceLastTick ) {
isInterrupt = true ;
getCPU ( ) . generateInterrupt ( ) ;
}
}
movedSinceLastTick = false ;
}
@Override
public void notifyVBLStateChanged ( boolean state ) {
// VBL is false when it is the vertical blanking period
if ( ! state & & interruptOnVBL ) {
isVBL = true ;
isInterrupt = true ;
getCPU ( ) . generateInterrupt ( ) ;
}
}
private void updateMouseState ( ) {
double x = lastMouseLocation . getX ( ) ;
x * = clampWindow . getWidth ( ) ;
x + = clampWindow . getMinX ( ) ;
x = Math . min ( Math . max ( x , clampWindow . getMinX ( ) ) , clampWindow . getMaxX ( ) ) ;
double y = lastMouseLocation . getY ( ) ;
y * = clampWindow . getHeight ( ) ;
y + = clampWindow . getMinY ( ) ;
y = Math . min ( Math . max ( y , clampWindow . getMinY ( ) ) , clampWindow . getMaxY ( ) ) ;
PagedMemory m = ( ( RAM128k ) computer . getMemory ( ) ) . getMainMemory ( ) ;
int s = getSlot ( ) ;
/ *
* $0478 + slot Low byte of absolute X position
* $04F8 + slot Low byte of absolute Y position
* /
m . writeByte ( 0x0478 + s , ( byte ) ( ( int ) x & 0x0ff ) ) ;
m . writeByte ( 0x04F8 + s , ( byte ) ( ( int ) y & 0x0ff ) ) ;
/ *
* $0578 + slot High byte of absolute X position
* $05F8 + slot High byte of absolute Y position
* /
m . writeByte ( 0x0578 + s , ( byte ) ( ( ( int ) x & 0x0ff00 ) > > 8 ) ) ;
m . writeByte ( 0x05F8 + s , ( byte ) ( ( ( int ) y & 0x0ff00 ) > > 8 ) ) ;
/ *
* $0678 + slot Reserved and used by the firmware
* $06F8 + slot Reserved and used by the firmware
*
* Interrupt status byte :
* Set by READMOUSE
* Bit 7 6 5 4 3 2 1 0
* | | | | | | | |
* | | | | | | | ` - - - Previously , button 1 was up ( 0 ) or down ( 1 )
* | | | | | | ` - - - - - Movement interrupt
* | | | | | ` - - - - - - - Button 0 / 1 interrupt
* | | | | ` - - - - - - - - - VBL interrupt
* | | | ` - - - - - - - - - - - Currently , button 1 is up ( 0 ) or down ( 1 )
* | | ` - - - - - - - - - - - - - X / Y moved since last READMOUSE
* | ` - - - - - - - - - - - - - - - Previously , button 0 was up ( 0 ) or down ( 1 )
* ` - - - - - - - - - - - - - - - - - Currently , button 0 is up ( 0 ) or down ( 1 )
* /
int status = 0 ;
if ( button1pressLast ) {
status | = 1 ;
}
if ( interruptOnMove & & movedSinceLastRead ) {
status | = 2 ;
}
if ( interruptOnPress & & ( button0press ! = button0pressLast | | button1press ! = button1pressLast ) ) {
status | = 4 ;
}
if ( isVBL ) {
status | = 8 ;
}
if ( button1press ) {
status | = 16 ;
}
if ( movedSinceLastRead ) {
status | = 32 ;
}
if ( button0pressLast ) {
status | = 64 ;
}
if ( button0press ) {
status | = 128 ;
}
/ *
* $0778 + slot Button 0 / 1 interrupt status byte
* /
m . writeByte ( 0x0778 + s , ( byte ) ( status ) ) ;
/ *
* $07F8 + slot Mode byte
* /
m . writeByte ( 0x07F8 + s , ( byte ) ( mode ) ) ;
button0pressLast = button0press ;
button1pressLast = button1press ;
movedSinceLastRead = false ;
}
public void mousePressed ( MouseEvent me ) {
MouseButton button = me . getButton ( ) ;
if ( button = = MouseButton . PRIMARY | | button = = MouseButton . MIDDLE ) {
button0press = true ;
}
if ( button = = MouseButton . SECONDARY | | button = = MouseButton . MIDDLE ) {
button1press = true ;
}
}
public void mouseReleased ( MouseEvent me ) {
MouseButton button = me . getButton ( ) ;
if ( button = = MouseButton . PRIMARY | | button = = MouseButton . MIDDLE ) {
button0press = false ;
}
if ( button = = MouseButton . SECONDARY | | button = = MouseButton . MIDDLE ) {
button1press = false ;
}
}
private void setClampWindowX ( int min , int max ) {
// Fix for GEOS clamping funkiness
if ( max = = 32767 ) {
max = 560 ;
}
clampWindow = new Rectangle2D ( min , clampWindow . getMinY ( ) , max , clampWindow . getMaxY ( ) ) ;
}
private void setClampWindowY ( int min , int max ) {
// Fix for GEOS clamping funkiness
if ( max = = 32767 ) {
max = 192 ;
}
clampWindow = new Rectangle2D ( clampWindow . getMinX ( ) , min , clampWindow . getMaxX ( ) , max ) ;
}
@Override
protected void handleC8FirmwareAccess ( int register , TYPE type , int value , RAMEvent e ) {
// Do nothing, there is no need to emulate c8 rom
}
}