shithub: ft²

Download patch

ref: 2247f4c8984d43c50550fbd4444e2eed321007d2
parent: 108c333ecdfd8287e12f39f858347db64d33f2f7
author: Olav Sørensen <[email protected]>
date: Thu Apr 2 17:59:03 EDT 2020

Pushed v1.16 code

- The mouse system has been rewritten so that we don't need to do mouse
  capturing while interacting with GUI widgets, which could be buggy sometimes.
- The different Disk Op. item paths (mod./instr./smp./pat./trk.) now behave
  exactly like real FT2. I.e. if they haven't been initialized before, they
  will be set to the current working directory.
- Scrollbar thumbs are now limited to minimum 9 pixels in length/height, to
  prevent them from being difficult to click on.

--- a/src/ft2_diskop.c
+++ b/src/ft2_diskop.c
@@ -75,7 +75,7 @@
 static char *modTmpFName, *insTmpFName, *smpTmpFName, *patTmpFName, *trkTmpFName;
 static char *modTmpFNameUTF8; // for window title
 static uint8_t FReq_Item;
-static bool FReq_ShowAllFiles;
+static bool FReq_ShowAllFiles, modPathSet, insPathSet, smpPathSet, patPathSet, trkPathSet;
 static int32_t FReq_EntrySelected = -1, FReq_FileCount, FReq_DirPos, lastMouseY;
 static UNICHAR *FReq_CurPathU, *FReq_ModCurPathU, *FReq_InsCurPathU, *FReq_SmpCurPathU, *FReq_PatCurPathU, *FReq_TrkCurPathU;
 static DirRec *FReq_Buffer;
@@ -176,20 +176,70 @@
 	UNICHAR pathU[PATH_MAX + 2];
 #endif
 
+	UNICHAR_GETCWD(FReq_ModCurPathU, PATH_MAX);
+
 	// the UNICHAR paths are already zeroed out
 
 #ifdef _WIN32
-	if (config.modulesPath[0]  != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.modulesPath, -1, FReq_ModCurPathU, 80);
-	if (config.instrPath[0]    != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.instrPath, -1, FReq_InsCurPathU, 80);
-	if (config.samplesPath[0]  != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.samplesPath, -1, FReq_SmpCurPathU, 80);
-	if (config.patternsPath[0] != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.patternsPath, -1, FReq_PatCurPathU, 80);
-	if (config.tracksPath[0]   != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.tracksPath, -1, FReq_TrkCurPathU, 80);
+	if (config.modulesPath[0] != '\0')
+	{
+		MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.modulesPath, -1, FReq_ModCurPathU, 80);
+		modPathSet = true;
+	}
+
+	if (config.instrPath[0] != '\0')
+	{
+		MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.instrPath, -1, FReq_InsCurPathU, 80);
+		insPathSet = true;
+	}
+
+	if (config.samplesPath[0] != '\0')
+	{
+		MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.samplesPath, -1, FReq_SmpCurPathU, 80);
+		smpPathSet = true;
+	}
+
+	if (config.patternsPath[0] != '\0')
+	{
+		MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.patternsPath, -1, FReq_PatCurPathU, 80);
+		patPathSet = true;
+	}
+
+	if (config.tracksPath[0] != '\0')
+	{
+		MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.tracksPath, -1, FReq_TrkCurPathU, 80);
+		trkPathSet = true;
+	}
 #else
-	if (config.modulesPath[0]  != '\0') strncpy(FReq_ModCurPathU, config.modulesPath, 80);
-	if (config.instrPath[0]    != '\0') strncpy(FReq_InsCurPathU, config.instrPath, 80);
-	if (config.samplesPath[0]  != '\0') strncpy(FReq_SmpCurPathU, config.samplesPath, 80);
-	if (config.patternsPath[0] != '\0') strncpy(FReq_PatCurPathU, config.patternsPath, 80);
-	if (config.tracksPath[0]   != '\0') strncpy(FReq_TrkCurPathU, config.tracksPath, 80);
+	if (config.modulesPath[0] != '\0')
+	{
+		strncpy(FReq_ModCurPathU, config.modulesPath, 80);
+		modPathSet = true;
+	}
+
+	if (config.instrPath[0] != '\0')
+	{
+		strncpy(FReq_InsCurPathU, config.instrPath, 80);
+		insPathSet = true;
+	}
+
+	if (config.samplesPath[0] != '\0')
+	{
+		strncpy(FReq_SmpCurPathU, config.samplesPath, 80);
+		smpPathSet = true;
+	}
+
+	if (config.patternsPath[0] != '\0')
+	{
+		strncpy(FReq_PatCurPathU, config.patternsPath, 80);
+		patPathSet = true;
+	}
+
+	if (config.tracksPath[0] != '\0')
+	{
+		strncpy(FReq_TrkCurPathU, config.tracksPath, 80);
+		trkPathSet = true;
+	}
 #endif
 
 	// set initial path to user directory
@@ -200,16 +250,12 @@
 	UNICHAR_CHDIR(getenv("HOME"));
 #endif
 
-	// if present in config, set custom "modules" path
-	if (UNICHAR_CHDIR(FReq_ModCurPathU) != 0)
+	// if we couldn't set present in config, set custom "modules" path
+	if (modPathSet && UNICHAR_CHDIR(FReq_ModCurPathU) != 0)
+	{
 		UNICHAR_GETCWD(FReq_ModCurPathU, PATH_MAX);
-
-	if (UNICHAR_CHDIR(FReq_InsCurPathU) != 0) UNICHAR_STRCPY(FReq_InsCurPathU, FReq_ModCurPathU);
-	if (UNICHAR_CHDIR(FReq_SmpCurPathU) != 0) UNICHAR_STRCPY(FReq_SmpCurPathU, FReq_ModCurPathU);
-	if (UNICHAR_CHDIR(FReq_PatCurPathU) != 0) UNICHAR_STRCPY(FReq_PatCurPathU, FReq_ModCurPathU);
-	if (UNICHAR_CHDIR(FReq_TrkCurPathU) != 0) UNICHAR_STRCPY(FReq_TrkCurPathU, FReq_ModCurPathU);
-
-	UNICHAR_CHDIR(FReq_ModCurPathU); // set back after testing
+		modPathSet = true;
+	}
 }
 
 static void freeDirRecBuffer(void)
@@ -1969,7 +2015,7 @@
 	return true;
 }
 
-void startDiskOpFillThread(void)
+void diskOp_StartDirReadThread(void)
 {
 	editor.diskOpReadDone = false;
 
@@ -2076,11 +2122,69 @@
 	FReq_Item = item;
 	switch (FReq_Item)
 	{
-		default: case DISKOP_ITEM_MODULE:  FReq_FileName = modTmpFName; FReq_CurPathU = FReq_ModCurPathU; break;
-		         case DISKOP_ITEM_INSTR:   FReq_FileName = insTmpFName; FReq_CurPathU = FReq_InsCurPathU; break;
-		         case DISKOP_ITEM_SAMPLE:  FReq_FileName = smpTmpFName; FReq_CurPathU = FReq_SmpCurPathU; break;
-		         case DISKOP_ITEM_PATTERN: FReq_FileName = patTmpFName; FReq_CurPathU = FReq_PatCurPathU; break;
-		         case DISKOP_ITEM_TRACK:   FReq_FileName = trkTmpFName; FReq_CurPathU = FReq_TrkCurPathU; break;
+		default:
+		case DISKOP_ITEM_MODULE:
+		{
+			FReq_FileName = modTmpFName;
+			FReq_CurPathU = FReq_ModCurPathU;
+		}
+		break;
+
+		case DISKOP_ITEM_INSTR:
+		{
+			FReq_FileName = insTmpFName;
+
+			if (!insPathSet)
+			{
+				UNICHAR_STRCPY(FReq_InsCurPathU, FReq_CurPathU);
+				insPathSet = true;
+			}
+
+			FReq_CurPathU = FReq_InsCurPathU;
+		}
+		break;
+
+		case DISKOP_ITEM_SAMPLE:
+		{
+			FReq_FileName = smpTmpFName;
+
+			if (!smpPathSet)
+			{
+				UNICHAR_STRCPY(FReq_SmpCurPathU, FReq_CurPathU);
+				smpPathSet = true;
+			}
+
+			FReq_CurPathU = FReq_SmpCurPathU;
+		}
+		break;
+
+		case DISKOP_ITEM_PATTERN:
+		{
+			FReq_FileName = patTmpFName;
+
+			if (!patPathSet)
+			{
+				UNICHAR_STRCPY(FReq_SmpCurPathU, FReq_CurPathU);
+				patPathSet = true;
+			}
+
+			FReq_CurPathU = FReq_PatCurPathU;
+		}
+		break;
+
+		case DISKOP_ITEM_TRACK:
+		{
+			FReq_FileName = trkTmpFName;
+
+			if (!trkPathSet)
+			{
+				UNICHAR_STRCPY(FReq_TrkCurPathU, FReq_CurPathU);
+				trkPathSet = true;
+			}
+
+			FReq_CurPathU = FReq_TrkCurPathU;
+		}
+		break;
 	}
 
 	pathLen = (int32_t)UNICHAR_STRLEN(FReq_CurPathU);
@@ -2189,7 +2293,7 @@
 	if (editor.diskOpReadOnOpen)
 	{
 		editor.diskOpReadOnOpen = false;
-		startDiskOpFillThread();
+		diskOp_StartDirReadThread();
 	}
 }
 
--- a/src/ft2_diskop.h
+++ b/src/ft2_diskop.h
@@ -40,7 +40,7 @@
 int32_t getExtOffset(char *s, int32_t stringLen); // get byte offset of file extension (last '.')
 bool testDiskOpMouseDown(bool mouseHeldDown);
 void testDiskOpMouseRelease(void);
-void startDiskOpFillThread(void);
+void diskOp_StartDirReadThread(void);
 void diskOp_DrawDirectory(void);
 void showDiskOpScreen(void);
 void hideDiskOpScreen(void);
--- a/src/ft2_events.c
+++ b/src/ft2_events.c
@@ -152,7 +152,7 @@
 	if (editor.diskOpReadDir)
 	{
 		editor.diskOpReadDir = false;
-		startDiskOpFillThread();
+		diskOp_StartDirReadThread();
 	}
 
 	if (editor.diskOpReadDone)
@@ -407,14 +407,23 @@
 
 	while (SDL_PollEvent(&event))
 	{
-		if (video.vsync60HzPresent)
+		if (event.type == SDL_WINDOWEVENT)
 		{
-			/* if we minimize the window and vsync is present, vsync is temporarily turned off.
-			** recalc waitVBL() vars so that it can sleep properly in said mode. */
-			if (event.type == SDL_WINDOWEVENT &&
-				(event.window.event == SDL_WINDOWEVENT_MINIMIZED || event.window.event == SDL_WINDOWEVENT_FOCUS_LOST))
+			if (event.window.event == SDL_WINDOWEVENT_HIDDEN)
+				video.windowHidden = true;
+			else if (event.window.event == SDL_WINDOWEVENT_SHOWN)
+				video.windowHidden = false;
+
+			if (video.vsync60HzPresent)
 			{
-				setupWaitVBL();
+				/* if we minimize the window and vsync is present, vsync is temporarily turned off.
+				** recalc waitVBL() vars so that it can sleep properly in said mode.
+				*/
+				if (event.window.event == SDL_WINDOWEVENT_MINIMIZED ||
+					event.window.event == SDL_WINDOWEVENT_FOCUS_LOST)
+				{
+					setupWaitVBL();
+				}
 			}
 		}
 
--- a/src/ft2_header.h
+++ b/src/ft2_header.h
@@ -12,7 +12,7 @@
 #endif
 #include "ft2_replayer.h"
 
-#define PROG_VER_STR "1.15"
+#define PROG_VER_STR "1.16"
 
 // do NOT change these! It will only mess things up...
 
--- a/src/ft2_inst_ed.c
+++ b/src/ft2_inst_ed.c
@@ -3146,7 +3146,7 @@
 			ins->vibDepth = ih.vibDepth;
 			ins->vibRate = ih.vibRate;
 			ins->fadeOut = ih.fadeOut;
-			ins->midiOn = (ih.midiOn > 0) ? true : false;
+			ins->midiOn = (ih.midiOn == 1) ? true : false;
 			ins->midiChannel = ih.midiChannel;
 			ins->midiProgram = ih.midiProgram;
 			ins->midiBend = ih.midiBend;
--- a/src/ft2_module_loader.c
+++ b/src/ft2_module_loader.c
@@ -622,17 +622,7 @@
 		s = &instrTmp[1+a]->samp[0];
 
 		s->len = 2 * SWAP16(h_MOD31.instr[a].len);
-		
-		s->pek = NULL;
-		s->origPek = (int8_t *)malloc(s->len + LOOP_FIX_LEN);
-		if (s->origPek == NULL)
-		{
-			showMsg(0, "System message", "Not enough memory!");
-			goto modLoadError;
-		}
 
-		s->pek = s->origPek + SMP_DAT_OFFSET;
-
 		if (modFormat != FORMAT_HMNT) // most of "His Master's Noisetracker" songs have junk sample names, so let's not load them
 			memcpy(s->name, songTmp.instrName[1+a], 22);
 
@@ -655,11 +645,11 @@
 			s->repL = 2;
 
 		// in The Ultimate SoundTracker, sample loop start is in bytes, not words
-		if (mightBeSTK)
+		if (modFormat == FORMAT_STK)
 			s->repS >>= 1;
 
 		// fix for poorly converted STK (< v2.5) -> PT/NT modules (FIXME: Worth keeping or not?)
-		if (!mightBeSTK && s->repL > 2 && s->repS+s->repL > s->len)
+		if (modFormat != FORMAT_STK && s->repL > 2 && s->repS+s->repL > s->len)
 		{
 			if ((s->repS>>1)+s->repL <= s->len)
 				s->repS >>= 1;
@@ -692,6 +682,16 @@
 			s->repS = 0;
 		}
 
+		s->pek = NULL;
+		s->origPek = (int8_t *)malloc(s->len + LOOP_FIX_LEN);
+		if (s->origPek == NULL)
+		{
+			showMsg(0, "System message", "Not enough memory!");
+			goto modLoadError;
+		}
+
+		s->pek = s->origPek + SMP_DAT_OFFSET;
+
 		int32_t bytesRead = (int32_t)fread(s->pek, 1, s->len, f);
 		if (bytesRead < s->len)
 		{
@@ -737,7 +737,7 @@
 static bool loadMusicSTM(FILE *f, uint32_t fileLength, bool fromExternalThread)
 {
 	uint8_t typ, tempo;
-	int16_t i, j, k, ai, ap, tmp;
+	int16_t i, j, k, ap, tmp;
 	uint16_t a;
 	tonTyp *ton;
 	sampleTyp *s;
@@ -899,7 +899,6 @@
 		}
 	}
 
-	ai = 31;
 	for (i = 0; i < 31; i++)
 	{
 		// trim off spaces at end of name
@@ -1010,7 +1009,6 @@
 static bool loadMusicS3M(FILE *f, uint32_t dataLength, bool fromExternalThread)
 {
 	int8_t *tmpSmp;
-	bool illegalUxx;
 	uint8_t ha[2048];
 	uint8_t alastnfo[32], alastefx[32], alastvibnfo[32], s3mLastGInstr[32];
 	uint8_t typ;
@@ -1129,8 +1127,6 @@
 
 	// *** PATTERNS ***
 
-	illegalUxx = false;
-
 	k = 0;
 	for (i = 0; i < ap; i++)
 	{
@@ -2012,7 +2008,7 @@
 			ins->vibDepth = ih.vibDepth;
 			ins->vibRate = ih.vibRate;
 			ins->fadeOut = ih.fadeOut;
-			ins->midiOn = (ih.midiOn > 0) ? true : false;
+			ins->midiOn = (ih.midiOn == 1) ? true : false;
 			ins->midiChannel = ih.midiChannel;
 			ins->midiProgram = ih.midiProgram;
 			ins->midiBend = ih.midiBend;
--- a/src/ft2_mouse.c
+++ b/src/ft2_mouse.c
@@ -59,9 +59,9 @@
 	const uint8_t *cursorsSrc = bmp.mouseCursors;
 	switch (config.mouseType)
 	{
-		case MOUSE_IDLE_SHAPE_NICE:   cursorsSrc += 0 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
-		case MOUSE_IDLE_SHAPE_UGLY:   cursorsSrc += 1 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
-		case MOUSE_IDLE_SHAPE_AWFUL:  cursorsSrc += 2 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+		case MOUSE_IDLE_SHAPE_NICE: cursorsSrc += 0 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+		case MOUSE_IDLE_SHAPE_UGLY: cursorsSrc += 1 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+		case MOUSE_IDLE_SHAPE_AWFUL: cursorsSrc += 2 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
 		case MOUSE_IDLE_SHAPE_USABLE: cursorsSrc += 3 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
 		default: break;
 	}
@@ -68,7 +68,9 @@
 
 	for (uint32_t i = 0; i < NUM_CURSORS; i++)
 	{
-		SDL_Surface *surface = SDL_CreateRGBSurface(0, MOUSE_CURSOR_W*video.yScale, MOUSE_CURSOR_H*video.yScale, 32, 0, 0, 0, 0);
+		int32_t scaleFactor = video.yScale;
+
+		SDL_Surface *surface = SDL_CreateRGBSurface(0, MOUSE_CURSOR_W*scaleFactor, MOUSE_CURSOR_H*scaleFactor, 32, 0, 0, 0, 0);
 		if (surface == NULL)
 		{
 			freeMouseCursors();
@@ -104,10 +106,10 @@
 		// blit upscaled cursor to surface
 		for (uint32_t y = 0; y < MOUSE_CURSOR_H; y++)
 		{
-			uint32_t *outX = &dstPixels32[(y * video.yScale) * surface->w];
-			for (uint32_t yScale = 0; yScale < video.yScale; yScale++)
+			uint32_t *outX = &dstPixels32[(y * scaleFactor) * surface->w];
+			for (int32_t yScale = 0; yScale < scaleFactor; yScale++)
 			{
-				for (uint32_t x = 0; x < MOUSE_CURSOR_W; x++)
+				for (int32_t x = 0; x < MOUSE_CURSOR_W; x++)
 				{
 					uint8_t srcPix = srcPixels8[(y * MOUSE_CURSOR_W) + x];
 					if (srcPix != PAL_TRANSPR)
@@ -118,7 +120,7 @@
 						else if (srcPix == PAL_BCKGRND)
 							pixel = border;
 
-						for (uint32_t xScale = 0; xScale < video.yScale; xScale++)
+						for (int32_t xScale = 0; xScale < scaleFactor; xScale++)
 							outX[xScale] = pixel;
 					}
 
@@ -163,13 +165,13 @@
 {
 	if (video.fullscreen)
 	{
-		mouse.setPosX = video.displayW / 2;
-		mouse.setPosY = video.displayH / 2;
+		mouse.setPosX = video.displayW >> 1;
+		mouse.setPosY = video.displayH >> 1;
 	}
 	else
 	{
-		mouse.setPosX = video.renderW / 2;
-		mouse.setPosY = video.renderH / 2;
+		mouse.setPosX = video.renderW >> 1;
+		mouse.setPosY = video.renderH >> 1;
 	}
 
 	mouse.setPosFlag = true;
@@ -242,11 +244,11 @@
 		gfxPtr = &bmp.mouseCursors[mouseModeGfxOffs];
 		switch (shape)
 		{
-			case MOUSE_IDLE_SHAPE_NICE:   gfxPtr += 0  * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
-			case MOUSE_IDLE_SHAPE_UGLY:   gfxPtr += 1  * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
-			case MOUSE_IDLE_SHAPE_AWFUL:  gfxPtr += 2  * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
-			case MOUSE_IDLE_SHAPE_USABLE: gfxPtr += 3  * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
-			case MOUSE_IDLE_TEXT_EDIT:    gfxPtr += 12 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+			case MOUSE_IDLE_SHAPE_NICE: gfxPtr += 0 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+			case MOUSE_IDLE_SHAPE_UGLY: gfxPtr += 1 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+			case MOUSE_IDLE_SHAPE_AWFUL: gfxPtr += 2 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+			case MOUSE_IDLE_SHAPE_USABLE: gfxPtr += 3 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
+			case MOUSE_IDLE_TEXT_EDIT: gfxPtr += 12 * (MOUSE_CURSOR_W * MOUSE_CURSOR_H); break;
 			default: return;
 		}
 	}
@@ -284,7 +286,7 @@
 
 static void changeCursorIfOverTextBoxes(void)
 {
-	int16_t i, mx, my;
+	int32_t i, mx, my;
 	textBox_t *t;
 
 	mouse.mouseOverTextBox = false;
@@ -542,11 +544,6 @@
 
 void mouseButtonUpHandler(uint8_t mouseButton)
 {
-#ifndef __APPLE__
-	if (!video.fullscreen) // release mouse button trap
-		SDL_SetWindowGrab(video.window, SDL_FALSE);
-#endif
-
 	if (mouseButton == SDL_BUTTON_LEFT)
 	{
 		mouse.leftButtonPressed = false;
@@ -569,7 +566,6 @@
 		mouse.rightButtonPressed = false;
 		mouse.rightButtonReleased = true;
 
-		
 		if (editor.editSampleFlag)
 		{
 			// right mouse button released after hand-editing sample data
@@ -582,9 +578,9 @@
 				writeSample(true);
 
 			setSongModifiedFlag();
+
 			editor.editSampleFlag = false;
 		}
-
 	}
 
 	mouse.firstTimePressingButton = false;
@@ -611,9 +607,9 @@
 	testRadioButtonMouseRelease();
 
 	// revert "delete/rename" mouse modes (disk op.)
-	if (mouse.lastUsedObjectID != PB_DISKOP_DELETE && mouse.lastUsedObjectID != PB_DISKOP_RENAME)
+	if (mouse.mode != MOUSE_MODE_NORMAL)
 	{
-		if (mouse.mode != MOUSE_MODE_NORMAL)
+		if (mouse.lastUsedObjectID != PB_DISKOP_DELETE && mouse.lastUsedObjectID != PB_DISKOP_RENAME)
 			setMouseMode(MOUSE_MODE_NORMAL);
 	}
 
@@ -623,11 +619,6 @@
 
 void mouseButtonDownHandler(uint8_t mouseButton)
 {
-#ifndef __APPLE__
-	if (!video.fullscreen) // trap mouse pointer while holding down left and/or right button
-		SDL_SetWindowGrab(video.window, SDL_TRUE);
-#endif
-
 	// if already holding left button and clicking right, don't do mouse down handling
 	if (mouseButton == SDL_BUTTON_RIGHT && mouse.leftButtonPressed)
 	{
@@ -691,7 +682,7 @@
 	if (mouse.lastUsedObjectType != OBJECT_PUSHBUTTON && mouse.lastUsedObjectID != OBJECT_ID_NONE)
 		return;
 
-	// kludge #3
+	// kludge #3 :(
 	if (!mouse.rightButtonPressed)
 		mouse.lastUsedObjectID = OBJECT_ID_NONE;
 
@@ -698,27 +689,28 @@
 	// check if we pressed a GUI object
 
 	/* test objects like this - clickable things *never* overlap, so no need to test all
-	** other objects if we clicked on one already */
+	** other objects if we clicked on one already
+	*/
 
 	testInstrSwitcherMouseDown(); // kludge: allow right click to both change ins. and edit text
 
-	if (testTextBoxMouseDown())     return;
-	if (testPushButtonMouseDown())  return;
-	if (testCheckBoxMouseDown())    return;
-	if (testScrollBarMouseDown())   return;
+	if (testTextBoxMouseDown()) return;
+	if (testPushButtonMouseDown()) return;
+	if (testCheckBoxMouseDown()) return;
+	if (testScrollBarMouseDown()) return;
 	if (testRadioButtonMouseDown()) return;
 
-	// from this point, we don't need to test more widgets if a system request box is shown
+	// at this point, we don't need to test more widgets if a system request is shown
 	if (editor.ui.sysReqShown)
 		return;
 
 	if (testInstrVolEnvMouseDown(false)) return;
 	if (testInstrPanEnvMouseDown(false)) return;
-	if (testDiskOpMouseDown(false))      return;
-	if (testPianoKeysMouseDown(false))   return;
-	if (testSamplerDataMouseDown())      return;
-	if (testPatternDataMouseDown())      return;
-	if (testScopesMouseDown())           return;
+	if (testDiskOpMouseDown(false)) return;
+	if (testPianoKeysMouseDown(false)) return;
+	if (testSamplerDataMouseDown()) return;
+	if (testPatternDataMouseDown()) return;
+	if (testScopesMouseDown()) return;
 	if (testAudioDeviceListsMouseDown()) return;
 
 #ifdef HAS_MIDI
@@ -737,11 +729,11 @@
 		{
 			switch (mouse.lastUsedObjectType)
 			{
-				case OBJECT_PUSHBUTTON:  handlePushButtonsWhileMouseDown();  break;
+				case OBJECT_PUSHBUTTON: handlePushButtonsWhileMouseDown(); break;
 				case OBJECT_RADIOBUTTON: handleRadioButtonsWhileMouseDown(); break;
-				case OBJECT_CHECKBOX:    handleCheckBoxesWhileMouseDown();   break;
-				case OBJECT_SCROLLBAR:   handleScrollBarsWhileMouseDown();   break;
-				case OBJECT_TEXTBOX:     handleTextBoxWhileMouseDown();      break;
+				case OBJECT_CHECKBOX: handleCheckBoxesWhileMouseDown(); break;
+				case OBJECT_SCROLLBAR: handleScrollBarsWhileMouseDown(); break;
+				case OBJECT_TEXTBOX: handleTextBoxWhileMouseDown(); break;
 				default: break;
 			}
 		}
@@ -750,16 +742,28 @@
 			// test non-standard GUI elements
 			switch (mouse.lastUsedObjectType)
 			{
-				case OBJECT_INSTRSWITCH: testInstrSwitcherMouseDown();       break;
-				case OBJECT_PATTERNMARK: handlePatternDataMouseDown(true);   break;
-				case OBJECT_DISKOPLIST:  testDiskOpMouseDown(true);          break;
-				case OBJECT_SMPDATA:     handleSampleDataMouseDown(true);    break;
-				case OBJECT_PIANO:       testPianoKeysMouseDown(true);       break;
-				case OBJECT_INSVOLENV:   testInstrVolEnvMouseDown(true);     break;
-				case OBJECT_INSPANENV:   testInstrPanEnvMouseDown(true);     break;
+				case OBJECT_INSTRSWITCH: testInstrSwitcherMouseDown(); break;
+				case OBJECT_PATTERNMARK: handlePatternDataMouseDown(true); break;
+				case OBJECT_DISKOPLIST: testDiskOpMouseDown(true); break;
+				case OBJECT_SMPDATA: handleSampleDataMouseDown(true); break;
+				case OBJECT_PIANO: testPianoKeysMouseDown(true); break;
+				case OBJECT_INSVOLENV: testInstrVolEnvMouseDown(true); break;
+				case OBJECT_INSPANENV: testInstrPanEnvMouseDown(true); break;
 				default: break;
 			}
 		}
+
+		/* Hack to send "mouse button up" events if we released the mouse button(s)
+		** outside of the window...
+		*/
+		if (mouse.x < 0 || mouse.x >= SCREEN_W || mouse.y < 0 || mouse.y >= SCREEN_H)
+		{
+			if (mouse.leftButtonPressed && !(mouse.buttonState & SDL_BUTTON_LMASK))
+				sendMouseButtonUpEvent(SDL_BUTTON_LEFT);
+
+			if (mouse.rightButtonPressed && !(mouse.buttonState & SDL_BUTTON_RMASK))
+				sendMouseButtonUpEvent(SDL_BUTTON_RIGHT);
+		}
 	}
 }
 
@@ -769,88 +773,102 @@
 	if (video.renderH > 0.0) video.dMouseYMul = (double)SCREEN_H / video.renderH;
 }
 
+void sendMouseButtonUpEvent(uint8_t button)
+{
+	SDL_Event event;
+
+	memset(&event, 0, sizeof (event));
+
+	event.type = SDL_MOUSEBUTTONUP;
+	event.button.button = button;
+
+	SDL_PushEvent(&event);
+}
+
 void readMouseXY(void)
 {
-	int16_t x, y;
-	int32_t mx, my;
+	int32_t mx, my, windowX, windowY;
 
 	if (mouse.setPosFlag)
 	{
-		mouse.setPosFlag = false;
-
-		if (SDL_GetWindowFlags(video.window) & SDL_WINDOW_SHOWN)
+		if (!video.windowHidden)
 			SDL_WarpMouseInWindow(video.window, mouse.setPosX, mouse.setPosY);
 
+		mouse.setPosFlag = false;
 		return;
 	}
 
-	SDL_PumpEvents(); // gathers all pending input from devices into the event queue (less mouse lag)
-	SDL_GetMouseState(&mx, &my);
+	mouse.buttonState = SDL_GetGlobalMouseState(&mx, &my);
 
-	/* in centered fullscreen mode, trap the mouse inside the framed image
-	** and subtract the coords to match the OS mouse position (fixes touch from touchscreens) */
-	if (video.fullscreen && !(config.windowFlags & FILTERING))
+	if (video.fullscreen)
 	{
-		if (mx < video.renderX)
-		{
-			mx = video.renderX;
-			SDL_WarpMouseInWindow(video.window, mx, my);
-		}
-		else if (mx >= video.renderX+video.renderW)
-		{
-			mx = (video.renderX + video.renderW) - 1;
-			SDL_WarpMouseInWindow(video.window, mx, my);
-		}
+		/* If fullscreen without filtering mode, translate coords and warp mouse
+		** inside the render space.
+		** Fullscreen + filtering mode takes up 100% of the screen area used, so no
+		** need to translate coords in that mode.
+		*/
 
-		if (my < video.renderY)
+		if (!(config.windowFlags & FILTERING))
 		{
-			my = video.renderY;
-			SDL_WarpMouseInWindow(video.window, mx, my);
-		}
-		else if (my >= video.renderY+video.renderH)
-		{
-			my = (video.renderY + video.renderH) - 1;
-			SDL_WarpMouseInWindow(video.window, mx, my);
-		}
+			if (!(config.specialFlags2 & HARDWARE_MOUSE))
+			{
+				bool warpMouse = false;
 
-		mx -= video.renderX;
-		my -= video.renderY;
+				if (mx < video.renderX)
+				{
+					mx = video.renderX;
+					warpMouse = true;
+				}
+				else if (mx >= video.renderX+video.renderW)
+				{
+					mx = (video.renderX + video.renderW) - 1;
+					warpMouse = true;
+				}
+
+				if (my < video.renderY)
+				{
+					my = video.renderY;
+					warpMouse = true;
+				}
+				else if (my >= video.renderY+video.renderH)
+				{
+					my = (video.renderY + video.renderH) - 1;
+					warpMouse = true;
+				}
+
+				if (warpMouse)
+					SDL_WarpMouseInWindow(video.window, mx, my);
+			}
+
+			// convert fullscreen coords to window (centered image) coords
+			mx -= video.renderX;
+			my -= video.renderY;
+		}
 	}
+	else
+	{
+		// convert desktop coords to window coords
 
-	if (mx < 0) mx = 0;
-	if (my < 0) mx = 0;
+		// (a call to this function is really fast in windowed mode)
+		SDL_GetWindowPosition(video.window, &windowX, &windowY);
 
+		mx -= windowX;
+		my -= windowY;
+	}
+
 	// multiply coords by video upscaling factors (don't round)
-	mx = (int32_t)(mx * video.dMouseXMul);
-	my = (int32_t)(my * video.dMouseYMul);
+	mouse.x = (int32_t)(mx * video.dMouseXMul);
+	mouse.y = (int32_t)(my * video.dMouseYMul);
 
-	if (mx >= SCREEN_W) mx = SCREEN_W - 1;
-	if (my >= SCREEN_H) my = SCREEN_H - 1;
-
 	if (config.specialFlags2 & HARDWARE_MOUSE)
 	{
-		// hardware mouse (OS)
-		mouse.x = (int16_t)mx;
-		mouse.y = (int16_t)my;
+		// hardware mouse mode (OS)
 		hideSprite(SPRITE_MOUSE_POINTER);
 	}
 	else
 	{
-		// software mouse (FT2 mouse)
-		x = (int16_t)mx;
-		y = (int16_t)my;
-
-		mouse.x = x;
-		mouse.y = y;
-
-		// for text editing cursor (do this after clamp)
-		x += mouse.xBias;
-		y += mouse.yBias;
-
-		if (x < 0) x = 0;
-		if (y < 0) y = 0;
-
-		setSpritePos(SPRITE_MOUSE_POINTER, x, y);
+		// software mouse mode (FT2 mouse)
+		setSpritePos(SPRITE_MOUSE_POINTER, mouse.x + mouse.xBias, mouse.y + mouse.yBias);
 	}
 
 	changeCursorIfOverTextBoxes();
--- a/src/ft2_mouse.h
+++ b/src/ft2_mouse.h
@@ -19,9 +19,10 @@
 	bool leftButtonPressed, rightButtonPressed, leftButtonReleased, rightButtonReleased;
 	bool firstTimePressingButton, mouseOverTextBox;
 	int8_t buttonCounter, mode;
-	int16_t lastUsedObjectID, lastUsedObjectType, lastEditBox, x, y, lastX, lastY, xBias, yBias;
-	int16_t lastScrollX, lastScrollXTmp, lastScrollY, saveMouseX, saveMouseY;
-	int32_t setPosX, setPosY;
+	int16_t lastUsedObjectID, lastUsedObjectType, lastEditBox;
+	int32_t x, y, lastX, lastY, xBias, yBias, setPosX, setPosY;
+	int32_t lastScrollX, lastScrollXTmp, lastScrollY, saveMouseX, saveMouseY;
+	uint32_t buttonState;
 } mouse;
 
 // do not change these!
@@ -30,6 +31,7 @@
 #define MOUSE_GLASS_ANI_FRAMES 22
 #define MOUSE_CLOCK_ANI_FRAMES 5
 
+void sendMouseButtonUpEvent(uint8_t button);
 void freeMouseCursors(void);
 bool createMouseCursors(void);
 void setMousePosToCenter(void);
--- a/src/ft2_sample_ed.c
+++ b/src/ft2_sample_ed.c
@@ -1440,7 +1440,7 @@
 	smpEd_Rx2 = smpEd_ScrPos + smpEd_ViewSize;
 }
 
-static void zoomSampleDataIn(int32_t step, int16_t x)
+static void zoomSampleDataIn(int32_t step, int32_t x)
 {
 	int32_t tmp32, minViewSize;
 	int64_t newScrPos64;
@@ -1482,7 +1482,7 @@
 	updateScrPos();
 }
 
-static void zoomSampleDataOut(int32_t step, int16_t x)
+static void zoomSampleDataOut(int32_t step, int32_t x)
 {
 	int32_t tmp32;
 	int64_t newViewSize64;
--- a/src/ft2_sample_ed_features.c
+++ b/src/ft2_sample_ed_features.c
@@ -43,11 +43,6 @@
 	editor.ui.sysReqShown = true;
 	editor.ui.sysReqEnterPressed = false;
 
-#ifndef __APPLE__
-	if (!video.fullscreen) // release mouse button trap
-		SDL_SetWindowGrab(video.window, SDL_FALSE);
-#endif
-
 	unstuckLastUsedGUIElement();
 	SDL_EventState(SDL_DROPFILE, SDL_DISABLE);
 }
--- a/src/ft2_scrollbars.c
+++ b/src/ft2_scrollbars.c
@@ -20,6 +20,12 @@
 #include "ft2_video.h"
 #include "ft2_palette.h"
 
+/* Prevent the scrollbar thumbs from being so small that
+** it's difficult to use them. In units of pixels.
+** Shouldn't be higher than 9!
+*/
+#define MIN_THUMB_LENGTH 9
+
 scrollBar_t scrollBars[NUM_SCROLLBARS] =
 {
 	// ------ RESERVED SCROLLBARS ------
@@ -168,7 +174,9 @@
 
 static void setScrollBarThumbCoords(uint16_t scrollBarID)
 {
-	int16_t tmp16, thumbX, thumbY, thumbW, thumbH, scrollEnd;
+	int16_t thumbX, thumbY, thumbW, thumbH, scrollEnd, realThumbLength;
+	int32_t tmp32, length, end;
+	double dTmp;
 	scrollBar_t *scrollBar;
 
 	assert(scrollBarID < NUM_SCROLLBARS);
@@ -196,11 +204,18 @@
 
 		if (scrollBar->thumbType == SCROLLBAR_THUMB_NOFLAT)
 		{
-			thumbW = 15;
+			realThumbLength = 15;
+
+			thumbW = realThumbLength;
+			if (thumbW < MIN_THUMB_LENGTH)
+				thumbW = MIN_THUMB_LENGTH;
+
 			if (scrollBar->end > 0)
 			{
-				tmp16 = (int16_t)round(((scrollBar->w - thumbW) / (double)scrollBar->end) * scrollBar->pos);
-				thumbX = scrollBar->x + tmp16;
+				length = scrollBar->w - realThumbLength;
+				dTmp = (length / (double)scrollBar->end) * scrollBar->pos;
+				tmp32 = (int32_t)(dTmp + 0.5);
+				thumbX = (int16_t)(scrollBar->x + tmp32);
 			}
 			else
 			{
@@ -211,18 +226,27 @@
 		{
 			if (scrollBar->end > 0)
 			{
-				tmp16 = (int16_t)round((scrollBar->w / (double)scrollBar->end) * scrollBar->page);
-				thumbW = CLAMP(tmp16, 1, scrollBar->w);
+				dTmp = (scrollBar->w / (double)scrollBar->end) * scrollBar->page;
+				tmp32 = (int32_t)(dTmp + 0.5);
+				realThumbLength = (int16_t)CLAMP(tmp32, 1, scrollBar->w);
 			}
 			else
 			{
-				thumbW = 1;
+				realThumbLength = 1;
 			}
 
+			thumbW = realThumbLength;
+			if (thumbW < MIN_THUMB_LENGTH)
+				thumbW = MIN_THUMB_LENGTH;
+
 			if (scrollBar->end > scrollBar->page)
 			{
-				tmp16 = (int16_t)round(((scrollBar->w - thumbW) / (double)(scrollBar->end - scrollBar->page)) * scrollBar->pos);
-				thumbX = scrollBar->x + tmp16;
+				length = scrollBar->w - thumbW;
+				end = scrollBar->end - scrollBar->page;
+
+				dTmp = (length / (double)end) * scrollBar->pos;
+				tmp32 = (int32_t)(dTmp + 0.5);
+				thumbX = (int16_t)(scrollBar->x + tmp32);
 			}
 			else
 			{
@@ -231,7 +255,7 @@
 		}
 
 		// prevent scrollbar thumb coords from being outside of the scrollbar area
-		thumbX = CLAMP(thumbX, scrollBar->x, scrollEnd - 1);
+		thumbX = CLAMP(thumbX, scrollBar->x, scrollEnd-1);
 		if (thumbX+thumbW > scrollEnd)
 			thumbW = scrollEnd - thumbX;
 	}
@@ -245,18 +269,27 @@
 
 		if (scrollBar->end > 0)
 		{
-			tmp16 = (int16_t)round((scrollBar->h / (double)scrollBar->end) * scrollBar->page);
-			thumbH = CLAMP(tmp16, 1, scrollBar->h);
+			dTmp = (scrollBar->h / (double)scrollBar->end) * scrollBar->page;
+			tmp32 = (int32_t)(dTmp + 0.5);
+			realThumbLength = (int16_t)CLAMP(tmp32, MIN_THUMB_LENGTH, scrollBar->h);
 		}
 		else
 		{
-			thumbH = 1;
+			realThumbLength = 1;
 		}
 
+		thumbH = realThumbLength;
+		if (thumbW < MIN_THUMB_LENGTH)
+			thumbW = MIN_THUMB_LENGTH;
+
 		if (scrollBar->end > scrollBar->page)
 		{
-			tmp16 = (int16_t)round(((scrollBar->h - thumbH) / (double)(scrollBar->end - scrollBar->page)) * scrollBar->pos);
-			thumbY = scrollBar->y + tmp16;
+			length = scrollBar->h - thumbH;
+			end = scrollBar->end - scrollBar->page;
+
+			dTmp = (length / (double)end) * scrollBar->pos;
+			tmp32 = (int32_t)(dTmp + 0.5);
+			thumbY = (int16_t)(scrollBar->y + tmp32);
 		}
 		else
 		{
@@ -270,6 +303,7 @@
 	}
 
 	// set values now
+	scrollBar->realThumbLength = realThumbLength;
 	scrollBar->thumbX = thumbX;
 	scrollBar->thumbY = thumbY;
 	scrollBar->thumbW = thumbW;
@@ -463,7 +497,7 @@
 bool testScrollBarMouseDown(void)
 {
 	uint16_t start, end;
-	int32_t scrollPos;
+	int32_t scrollPos, length;
 	double dTmp;
 	scrollBar_t *scrollBar;
 
@@ -506,18 +540,26 @@
 				}
 				else
 				{
-					mouse.saveMouseX = (int16_t)round(scrollBar->thumbW / 2.0);
+					mouse.saveMouseX = scrollBar->thumbW >> 1;
 
 					scrollPos = mouse.lastScrollX - scrollBar->x - mouse.saveMouseX;
 					if (scrollBar->thumbType == SCROLLBAR_THUMB_NOFLAT)
-						scrollPos = (int32_t)round(scrollPos * (scrollBar->w / (double)(scrollBar->w - 15)));
+					{
+						dTmp = scrollPos * (scrollBar->w / (double)(scrollBar->w - scrollBar->thumbW));
+						scrollPos = (int32_t)(dTmp + 0.5);
+					}
 
+					assert(scrollBar->w > 0);
 					scrollPos = CLAMP(scrollPos, 0, scrollBar->w);
 
-					assert(scrollBar->w > 0);
+					length = scrollBar->w + (scrollBar->realThumbLength - scrollBar->thumbW);
+					if (length < 1)
+						length = 1;
 
-					dTmp = round(((double)scrollPos * scrollBar->end) / scrollBar->w);
-					setScrollBarPos(mouse.lastUsedObjectID, (int32_t)dTmp, true);
+					dTmp = ((double)scrollPos * scrollBar->end) / length;
+					scrollPos = (int32_t)(dTmp + 0.5);
+
+					setScrollBarPos(mouse.lastUsedObjectID, scrollPos, true);
 				}
 			}
 			else
@@ -529,15 +571,21 @@
 				}
 				else
 				{
-					mouse.saveMouseY = (int16_t)(scrollBar->thumbH / 2.0); // truncate here
+					mouse.saveMouseY = scrollBar->thumbH >> 1;
 
 					scrollPos = mouse.lastScrollY - scrollBar->y - mouse.saveMouseY;
-					scrollPos = CLAMP(scrollPos, 0, scrollBar->h);
 
 					assert(scrollBar->h > 0);
+					scrollPos = CLAMP(scrollPos, 0, scrollBar->h);
 
-					dTmp = round(((double)scrollPos * scrollBar->end) / scrollBar->h);
-					setScrollBarPos(mouse.lastUsedObjectID, (int32_t)dTmp, true);
+					length = scrollBar->h + (scrollBar->realThumbLength - scrollBar->thumbW);
+					if (length < 1)
+						length = 1;
+
+					dTmp = ((double)scrollPos * scrollBar->end) / length;
+					scrollPos = (int32_t)(dTmp + 0.5);
+
+					setScrollBarPos(mouse.lastUsedObjectID, scrollPos, true);
 				}
 			}
 
@@ -575,7 +623,7 @@
 
 void handleScrollBarsWhileMouseDown(void)
 {
-	int16_t scrollX, scrollY;
+	int32_t scrollX, scrollY, length;
 	double dTmp;
 	scrollBar_t *scrollBar;
 
@@ -594,16 +642,22 @@
 			if (scrollBar->thumbType == SCROLLBAR_THUMB_NOFLAT)
 			{
 				assert(scrollBar->w >= 16);
-				scrollX = (int16_t)round(scrollX * (scrollBar->w / (double)(scrollBar->w - 15)));
+				dTmp = scrollX * (scrollBar->w / (double)(scrollBar->w - scrollBar->thumbW));
+				scrollX = (int32_t)(dTmp + 0.5);
 			}
 
+			assert(scrollBar->w > 0);
 			scrollX = CLAMP(scrollX, 0, scrollBar->w);
 
-			assert(scrollBar->w > 0);
+			length = scrollBar->w + (scrollBar->realThumbLength - scrollBar->thumbW);
+			if (length < 1)
+				length = 1;
 
-			dTmp = round(((double)scrollX * scrollBar->end) / scrollBar->w);
-			setScrollBarPos(mouse.lastUsedObjectID, (int32_t)dTmp, true);
+			dTmp = ((double)scrollX * scrollBar->end) / length;
+			scrollX = (int32_t)(dTmp + 0.5);
 
+			setScrollBarPos(mouse.lastUsedObjectID, scrollX, true);
+
 			if (mouse.lastUsedObjectID != OBJECT_ID_NONE) // this can change in the callback in setScrollBarPos()
 				drawScrollBar(mouse.lastUsedObjectID);
 		}
@@ -617,10 +671,14 @@
 			scrollY = mouse.lastScrollY - mouse.saveMouseY - scrollBar->y;
 			scrollY = CLAMP(scrollY, 0, scrollBar->h);
 
-			assert(scrollBar->h > 0);
+			length = scrollBar->h + (scrollBar->realThumbLength - scrollBar->thumbH);
+			if (length < 1)
+				length = 1;
 
-			dTmp = round(((double)scrollY * scrollBar->end) / scrollBar->h);
-			setScrollBarPos(mouse.lastUsedObjectID, (int32_t)dTmp, true);
+			dTmp = ((double)scrollY * scrollBar->end) / length;
+			scrollY = (int32_t)(dTmp + 0.5);
+
+			setScrollBarPos(mouse.lastUsedObjectID, scrollY, true);
 
 			if (mouse.lastUsedObjectID != OBJECT_ID_NONE) // this can change in the callback in setScrollBarPos()
 				drawScrollBar(mouse.lastUsedObjectID);
--- a/src/ft2_scrollbars.h
+++ b/src/ft2_scrollbars.h
@@ -76,7 +76,7 @@
 	uint8_t state;
 	uint32_t pos, page, end;
 	uint32_t oldPos, oldPage, oldEnd;
-	uint16_t thumbX, thumbY, thumbW, thumbH; 
+	uint16_t thumbX, thumbY, thumbW, thumbH, realThumbLength;
 } scrollBar_t;
 
 void drawScrollBar(uint16_t scrollBarID);
--- a/src/ft2_sysreqs.c
+++ b/src/ft2_sysreqs.c
@@ -161,11 +161,6 @@
 	pushButton_t *p;
 	checkBox_t *c;
 
-#ifndef __APPLE__
-	if (!video.fullscreen) // release mouse button trap
-		SDL_SetWindowGrab(video.window, SDL_FALSE);
-#endif
-
 	if (editor.editTextFlag)
 		exitTextEditing();
 
@@ -410,11 +405,6 @@
 		okBox(0, "System message", "Not enough memory!");
 		return 0;
 	}
-
-#ifndef __APPLE__
-	if (!video.fullscreen) // release mouse button trap
-		SDL_SetWindowGrab(video.window, SDL_FALSE);
-#endif
 
 	SDL_EventState(SDL_DROPFILE, SDL_DISABLE);
 
--- a/src/ft2_textboxes.c
+++ b/src/ft2_textboxes.c
@@ -63,7 +63,8 @@
 };
 
 static int16_t markX1, markX2;
-static uint16_t oldCursorPos, oldMouseX;
+static uint16_t oldCursorPos;
+static int32_t oldMouseX;
 
 static void moveTextCursorLeft(int16_t i, bool updateTextBox);
 static void moveTextCursorRight(int16_t i, bool updateTextBox);
--- a/src/ft2_video.c
+++ b/src/ft2_video.c
@@ -376,7 +376,7 @@
 	return true;
 }
 
-void changeSpriteData(uint8_t sprite, const uint8_t *data)
+void changeSpriteData(int32_t sprite, const uint8_t *data)
 {
 	sprites[sprite].data = data;
 	memset(sprites[sprite].refreshBuffer, 0, sprites[sprite].w * sprites[sprite].h * sizeof (int32_t));
@@ -384,7 +384,7 @@
 
 void freeSprites(void)
 {
-	for (uint32_t i = 0; i < SPRITE_NUM; i++)
+	for (int32_t i = 0; i < SPRITE_NUM; i++)
 	{
 		if (sprites[i].refreshBuffer != NULL)
 		{
@@ -404,18 +404,18 @@
 	changeSpriteData(SPRITE_RIGHT_LOOP_PIN, clicked ? &bmp.loopPins[3*(154*16)] : &bmp.loopPins[2*(154*16)]);
 }
 
-int32_t getSpritePosX(uint8_t sprite)
+int32_t getSpritePosX(int32_t sprite)
 {
 	return sprites[sprite].x;
 }
 
-void setSpritePos(uint8_t sprite, int16_t x, int16_t y)
+void setSpritePos(int32_t sprite, int32_t x, int32_t y)
 {
-	sprites[sprite].newX = x;
-	sprites[sprite].newY = y;
+	sprites[sprite].newX = (int16_t)x;
+	sprites[sprite].newY = (int16_t)y;
 }
 
-void hideSprite(uint8_t sprite)
+void hideSprite(int32_t sprite)
 {
 	sprites[sprite].newX = SCREEN_W;
 }
@@ -423,7 +423,7 @@
 void eraseSprites(void)
 {
 	int8_t i;
-	register int32_t x, y, sw, sh, srcPitch, dstPitch;
+	int32_t sx, sy, x, y, sw, sh, srcPitch, dstPitch;
 	const uint32_t *src32;
 	uint32_t *dst32;
 	sprite_t *s;
@@ -431,28 +431,36 @@
 	for (i = SPRITE_NUM-1; i >= 0; i--) // erasing must be done in reverse order
 	{
 		s = &sprites[i];
-		if (s->x >= SCREEN_W) // sprite is hidden, don't erase
+		if (s->x >= SCREEN_W || s->y >= SCREEN_H) // sprite is hidden, don't draw nor fill clear buffer
 			continue;
 
-		assert(s->y >= 0 && s->refreshBuffer != NULL);
+		assert(s->refreshBuffer != NULL);
 
 		sw = s->w;
 		sh = s->h;
-		x  = s->x;
-		y  = s->y;
+		sx = s->x;
+		sy = s->y;
 
-		// if x is negative, adjust variables (can only happen on loop pins in smp. ed.)
-		if (x < 0)
+		// if x is negative, adjust variables
+		if (sx < 0)
 		{
-			sw += x; // subtraction
-			x = 0;
+			sw += sx; // subtraction
+			sx = 0;
 		}
 
+		// if y is negative, adjust variables
+		if (sy < 0)
+		{
+			sh += sy; // subtraction
+			sy = 0;
+		}
+
 		src32 = s->refreshBuffer;
-		dst32 = &video.frameBuffer[(y * SCREEN_W) + x];
+		dst32 = &video.frameBuffer[(sy * SCREEN_W) + sx];
 
-		if (y+sh >= SCREEN_H) sh = SCREEN_H - y;
-		if (x+sw >= SCREEN_W) sw = SCREEN_W - x;
+		// handle x/y clipping
+		if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx;
+		if (sy+sh >= SCREEN_H) sh = SCREEN_H - sy;
 
 		srcPitch = s->w - sw;
 		dstPitch = SCREEN_W - sw;
@@ -471,7 +479,7 @@
 void renderSprites(void)
 {
 	const uint8_t *src8;
-	register int32_t x, y, sw, sh, srcPitch, dstPitch;
+	int32_t sx, sy, x, y, sw, sh, srcPitch, dstPitch;
 	uint32_t i, *clr32, *dst32, windowFlags;
 	sprite_t *s;
 
@@ -495,20 +503,42 @@
 		s->x = s->newX;
 		s->y = s->newY;
 
-		if (s->x >= SCREEN_W) // sprite is hidden, don't draw nor fill clear buffer
+		if (s->x >= SCREEN_W || s->y >= SCREEN_H) // sprite is hidden, don't draw nor fill clear buffer
 			continue;
 
-		assert(s->x >= 0 && s->y >= 0 && s->data != NULL && s->refreshBuffer != NULL);
+		assert(s->data != NULL && s->refreshBuffer != NULL);
 
 		sw = s->w;
 		sh = s->h;
+		sx = s->x;
+		sy = s->y;
 		src8 = s->data;
-		dst32 = &video.frameBuffer[(s->y * SCREEN_W) + s->x];
+
+		// if x is negative, adjust variables
+		if (sx < 0)
+		{
+			sw += sx; // subtraction
+			src8 -= sx; // addition
+			sx = 0;
+		}
+
+		// if y is negative, adjust variables
+		if (sy < 0)
+		{
+			sh += sy; // subtraction
+			src8 += (-sy * s->w); // addition
+			sy = 0;
+		}
+
+		if (sw <= 0 || sh <= 0) // sprite is hidden, don't draw nor fill clear buffer
+			continue;
+
+		dst32 = &video.frameBuffer[(sy * SCREEN_W) + sx];
 		clr32 = s->refreshBuffer;
 
-		// handle xy clipping
-		if (s->y+sh >= SCREEN_H) sh = SCREEN_H - s->y;
-		if (s->x+sw >= SCREEN_W) sw = SCREEN_W - s->x;
+		// handle x/y clipping
+		if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx;
+		if (sy+sh >= SCREEN_H) sh = SCREEN_H - sy;
 
 		srcPitch = s->w - sw;
 		dstPitch = SCREEN_W - sw;
@@ -570,8 +600,7 @@
 {
 	uint8_t pal;
 	const uint8_t *src8;
-	int32_t sx;
-	register int32_t x, y, sw, sh, srcPitch, dstPitch;
+	int32_t sx, x, y, sw, sh, srcPitch, dstPitch;
 	uint32_t *clr32, *dst32;
 	sprite_t *s;
 
@@ -604,7 +633,7 @@
 		dst32 = &video.frameBuffer[(s->y * SCREEN_W) + sx];
 
 		// handle x clipping
-		if (s->x+sw >= SCREEN_W) sw = SCREEN_W - s->x;
+		if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx;
 
 		srcPitch = s->w - sw;
 		dstPitch = SCREEN_W - sw;
@@ -663,7 +692,7 @@
 		dst32 = &video.frameBuffer[(s->y * SCREEN_W) + sx];
 
 		// handle x clipping
-		if (s->x+sw >= SCREEN_W) sw = SCREEN_W - s->x;
+		if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx;
 
 		srcPitch = s->w - sw;
 		dstPitch = SCREEN_W - sw;
--- a/src/ft2_video.h
+++ b/src/ft2_video.h
@@ -27,7 +27,7 @@
 	SDL_Window *window;
 	double dMonitorRefreshRate, dMouseXMul, dMouseYMul;
 	uint8_t upscaleFactor;
-	bool vsync60HzPresent;
+	bool vsync60HzPresent, windowHidden;
 	int32_t renderX, renderY, renderW, renderH, displayW, displayH;
 	uint32_t *frameBufferUnaligned;
 	SDL_Renderer *renderer;
@@ -56,15 +56,15 @@
 void closeVideo(void);
 void setLeftLoopPinState(bool clicked);
 void setRightLoopPinState(bool clicked);
-int32_t getSpritePosX(uint8_t sprite);
+int32_t getSpritePosX(int32_t sprite);
 void eraseSprites(void);
 void renderLoopPins(void);
 void renderSprites(void);
 bool setupSprites(void);
 void freeSprites(void);
-void setSpritePos(uint8_t sprite, int16_t x, int16_t y);
-void changeSpriteData(uint8_t sprite, const uint8_t *data);
-void hideSprite(uint8_t sprite);
+void setSpritePos(int32_t sprite, int32_t x, int32_t y);
+void changeSpriteData(int32_t sprite, const uint8_t *data);
+void hideSprite(int32_t sprite);
 void handleRedrawing(void);
 void enterFullscreen(void);
 void leaveFullScreen(void);