view src/gui.c @ 79:8f90d5addda9 v2.0

*: refactor... basically everything! The Win32 GUI version is now unicode-friendly. HOWEVER, ANSI still very much works. you can configure which version to use through `-DUNICODE=0/1` in CFLAGS. the CLI is also friendlier and uses a more sane interface as well. note: the command line flags (which were optional before) are now required. Unicode filenames will not work on Windows because Windows sucks.
author Paper <paper@paper.us.eu.org>
date Wed, 20 Mar 2024 17:06:26 -0400
parents 79a35af2cb56
children c06dcab17923
line wrap: on
line source

/**
 * msvpvf GUI for Windows 
 * Copyright (c) Paper 2022-2024
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
**/

/* change these macros to configure the window size */
#define WINDOW_WIDTH  225
#define WINDOW_HEIGHT 200

/* mingw */
#ifdef UNICODE
#define NTDDI_VERSION 0x06000000
#define _WIN32_WINNT  0x0600
#else
#define _WIN32_WINNT  0x0400
#endif

#include <windef.h>
#include <winbase.h>
#include <shlwapi.h>
#include <shobjidl.h>

#include <stdint.h>
#include <stdio.h>

#include "common.h"

/* we use COM when `UNICODE=1` to avoid file paths being cut off */
#define COM_INITFLAGS (COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)

#define OPEN_FILE_BUTTON	0
#define COMBOBOX		1
#define LISTBOX			2
#define SAVE_FILE_BUTTON	3
#define VERSION			4

/* adjust for these functions for Unicode */
#if UNICODE
#define _tstrncpy wcsncpy
#define _tstrdup _wcsdup
#define _tfopen _wfopen
#define _sntprintf _snwprintf
#else
#define _tstrncpy strncpy
#define _tstrdup _strdup
#define _tfopen fopen
#define _sntprintf _snprintf
#endif

/* enumerate over this */
static const enum types types[] = {TYPES_VF, TYPES_VEG};

static LPTSTR file_path = NULL;
static uint8_t version = 13;
static enum types type = TYPES_VEG;

/* we edit this from display_file() */
static HWND hwnd_version = NULL;

static inline LPCTSTR type_to_prefix(enum types type) {
	switch (type) {
		case TYPES_VF: return TEXT("MS");
		case TYPES_VEG: return TEXT("PRO");
		case TYPES_UNKNOWN:
		default: return TEXT("UNK");
	}
}

static inline LPCTSTR type_to_string(enum types type) {
	switch (type) {
		case TYPES_VF: return TEXT("Movie Studio");
		case TYPES_VEG: return TEXT("Vegas Pro");
		case TYPES_UNKNOWN:
		default: return TEXT("Unknown");
	}
}

static inline LPCTSTR type_to_extension(enum types type) {
	switch (type) {
		case TYPES_VF: return TEXT("vf");
		case TYPES_VEG:
		case TYPES_UNKNOWN:
		default: return TEXT("veg");
	}
}

/* these functions are designed to *not* use global variables,
 * to make everything a bit more simple... */
static int display_file(LPCTSTR path) {
	/* Read the file to memory */
	FILE* file = _tfopen(path, TEXT("rb"));

	uint8_t file_version = 0;
	enum types file_type = TYPES_UNKNOWN;

	get_file_information(file, &file_version, &file_type);

	int needed = _sntprintf(NULL, 0, TEXT("File version: %s %u"), type_to_string(file_type), file_version);
	LPTSTR text = calloc(needed + 1, sizeof(TCHAR));
	if (!text) /* out of memory... lol */
		exit(1);
	_sntprintf(text, needed + 1, TEXT("File version: %s %u"), type_to_string(file_type), file_version);

	if (!SendMessage(hwnd_version, WM_SETTEXT, (WPARAM)0, (LPARAM)text)) {
		free(text);
		return -1;
	}

	free(text);
	fclose(file);

	return 0;
}

static int open_file(HWND hWnd, LPTSTR* filepath) {
#if UNICODE
	if (CoInitializeEx(NULL, COM_INITFLAGS) != S_OK)
		return -1; /* what */

	COMDLG_FILTERSPEC filters[] = {{L"Project files", L"*.veg;*.vf"}, {L"All files", L"*.*"}};

	IFileDialog* pfd = NULL;
	IShellItem* result = NULL;

	if (SUCCEEDED(CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, (LPVOID*)&pfd))) {
		pfd->lpVtbl->SetFileTypes(pfd, ARRAYSIZE(filters), filters);
		pfd->lpVtbl->SetFileTypeIndex(pfd, 1);
		pfd->lpVtbl->Show(pfd, hWnd);

		if (!SUCCEEDED(pfd->lpVtbl->GetResult(pfd, &result)) || !result) {
			pfd->lpVtbl->Release(pfd);
			return -1;
		}

		if (!SUCCEEDED(result->lpVtbl->GetDisplayName(result, SIGDN_FILESYSPATH, filepath))) {
			pfd->lpVtbl->Release(pfd);
			result->lpVtbl->Release(result);
			*filepath = NULL; /* might memleak? */
			return -1;
		}

		result->lpVtbl->Release(result);
		pfd->lpVtbl->Release(pfd);
	} else {
#endif
		/* initialize our buffer */
		*filepath = calloc(MAX_PATH + 1, sizeof(TCHAR));

		OPENFILENAME ofn = {0};

		/* NT 4.0 compat */
		ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400;
		ofn.hwndOwner = hWnd;
		ofn.lpstrFile = *filepath;
		ofn.nMaxFile = MAX_PATH;
		ofn.lpstrFilter = TEXT("Project files\0*.veg;*.vf\0All files\0*.*\0");

		if (!GetOpenFileName(&ofn))
			return -1;
#if UNICODE
	}

	CoUninitialize();
#endif

	return display_file(*filepath);;
}

static int save_file(HWND hWnd, LPCTSTR input, uint8_t version, enum types type) {
	if (!input) {
		MessageBox(hWnd,
					TEXT("Please open a file first!"),
					TEXT("Invalid input file!"),
					MB_ICONEXCLAMATION);
		return -1;
	}

	LPTSTR output_template = NULL;
	LPTSTR output = NULL;

	{
		LPTSTR input_basename = PathFindFileName(input);
		int input_basename_len = PathFindExtension(input_basename) - input_basename;

		int needed = _sntprintf(NULL, 0, TEXT("%s_V%u_%.*s.%s"), type_to_prefix(type), version, input_basename_len, input_basename, type_to_extension(type));
		output_template = calloc(needed + 1, sizeof(TCHAR));
		_sntprintf(output_template, needed + 1, TEXT("%s_V%u_%.*s.%s"), type_to_prefix(type), version, input_basename_len, input_basename, type_to_extension(type));

		free(input_basename);
	}

	{
		/* File dialog */
#if UNICODE
		int com_initialized = SUCCEEDED(CoInitializeEx(NULL, COM_INITFLAGS));

		COMDLG_FILTERSPEC filters[] = {{L"Movie Studio project files", L"*.vf"}, {L"Vegas Pro project files", L"*.veg"}, {L"All files", L"*.*"}};

		IFileDialog* pfd = NULL;
		IShellItem* result = NULL;
		if (com_initialized && SUCCEEDED(CoCreateInstance(&CLSID_FileSaveDialog, NULL, CLSCTX_INPROC_SERVER, &IID_IFileSaveDialog, (LPVOID*)&pfd))) {
			pfd->lpVtbl->SetFileTypes(pfd, ARRAYSIZE(filters), filters);
			pfd->lpVtbl->SetFileTypeIndex(pfd, (type == TYPES_UNKNOWN) ? ARRAYSIZE(filters) : type - TYPES_UNKNOWN);
			pfd->lpVtbl->SetFileName(pfd, output_template);
			pfd->lpVtbl->Show(pfd, hWnd);

			if (!SUCCEEDED(pfd->lpVtbl->GetResult(pfd, &result))) {
				pfd->lpVtbl->Release(pfd);
				free(output_template);
				return -1;
			}

			if (!SUCCEEDED(result->lpVtbl->GetDisplayName(result, SIGDN_FILESYSPATH, &output))) {
				pfd->lpVtbl->Release(pfd);
				result->lpVtbl->Release(result);
				free(output_template);
				output = NULL; /* might memleak ? */
				return -1;
			}

			result->lpVtbl->Release(result);
			pfd->lpVtbl->Release(pfd);
			free(output_template);
		} else {
#endif
			/* fallback to OPENFILENAME if COM fucks up for whatever reason (or we're on ANSI)... */
			output = calloc(MAX_PATH + 1, sizeof(TCHAR));
			_tstrncpy(output, output_template, MAX_PATH);
			free(output_template);

			OPENFILENAME ofn = {0};

			ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400;
			ofn.hwndOwner = hWnd;
			ofn.lpstrFile = output;
			ofn.nMaxFile = MAX_PATH;
			ofn.lpstrFilter = TEXT("Movie Studio project files\0*.vf\0Vegas Pro project files\0*.veg\0All files\0*.*\0");
			ofn.nFilterIndex = (type == TYPES_UNKNOWN) ? 3 : type - TYPES_UNKNOWN;

			if (!GetSaveFileName(&ofn))
				return -1;
#if UNICODE
		}

		if (com_initialized)
			CoUninitialize();
#endif
	}

	if (!CopyFile(input, output, 0)) {
		MessageBox(hWnd, TEXT("Failed to copy original project file! Does the destination file already exist?"), TEXT("Saving project failed!"), MB_ICONEXCLAMATION | MB_OK);
		free(output);
		return -1;
	}

	FILE* output_file = _tfopen(output, TEXT("ab"));
	if (!output_file) {
		MessageBox(hWnd, TEXT("Failed to save project file!"), TEXT("Saving project failed!"), MB_ICONEXCLAMATION | MB_OK);
		free(output);
		return -1;
	}

	free(output);

	set_file_information(output_file, version, type);

	fclose(output_file);

	return 0;
}

/* TODO: use a resource file instead... */
void AddControls(HWND hWnd) {
	/* Open File */
	HWND open_button = CreateWindow(TEXT("Button"), TEXT("Open"), WS_VISIBLE | WS_CHILD, WINDOW_WIDTH * 7 / 18, WINDOW_HEIGHT / 40, WINDOW_WIDTH * 2 / 9, WINDOW_HEIGHT / 10, hWnd, (HMENU)OPEN_FILE_BUTTON, NULL, NULL);

	/* Versions */
	HWND combobox = CreateWindow(TEXT("ComboBox"), NULL,
								 CBS_DROPDOWNLIST | CBS_HASSTRINGS | WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_VSCROLL,
								 WINDOW_WIDTH * 7 / 18, WINDOW_HEIGHT * 3 / 20, WINDOW_WIDTH * 2 / 9, WINDOW_HEIGHT, 
								 hWnd, (HMENU)COMBOBOX, NULL, NULL);

	static LPCTSTR versions[] = {TEXT("8"),  TEXT("9"),  TEXT("10"), 
	                             TEXT("11"), TEXT("12"), TEXT("13"), 
	                             TEXT("14"), TEXT("15"), TEXT("16"), 
	                             TEXT("17"), TEXT("18"), TEXT("19"),
	                             TEXT("20"), TEXT("21")}; /* wuss 9+10 */

	for (size_t i = 0; i < ARRAYSIZE(versions); i++)
		SendMessage(combobox, (UINT)CB_ADDSTRING, (WPARAM)0, (LPARAM)versions[i]);

	SendMessage(combobox, CB_SETCURSEL, (WPARAM)3, (LPARAM)0);

	/* Type */
	HWND listbox = CreateWindow(TEXT("Listbox"), NULL, WS_VISIBLE | WS_CHILD | LBS_STANDARD | LBS_NOTIFY, WINDOW_WIDTH * 5 / 18, WINDOW_HEIGHT * 11 / 40, WINDOW_WIDTH * 4 / 9, WINDOW_HEIGHT / 5, hWnd, (HMENU)LISTBOX, NULL, NULL);

	for (size_t i = 0; i < ARRAYSIZE(types); i++) {
		LRESULT pos = SendMessage(listbox, LB_ADDSTRING, i, (LPARAM)type_to_string(types[i]));
		SendMessage(listbox, LB_SETITEMDATA, pos, (LPARAM)types[i]);
		if (types[i] == type)
			SendMessage(listbox, LB_SETCURSEL, (WPARAM)pos, (LPARAM)0);
	}

	/* Save File */
	HWND save_button = CreateWindow(TEXT("Button"), TEXT("Save"), WS_VISIBLE | WS_CHILD, WINDOW_WIDTH * 7 / 18, WINDOW_HEIGHT * 9 / 20, WINDOW_WIDTH * 2 / 9, WINDOW_HEIGHT / 10, hWnd, (HMENU)SAVE_FILE_BUTTON, NULL, NULL);

	/* Version and Type display */
	hwnd_version = CreateWindow(TEXT("Edit"), TEXT(""), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_READONLY | ES_CENTER | ES_MULTILINE | SS_CENTER, WINDOW_WIDTH / 6, WINDOW_HEIGHT * 3 / 5, WINDOW_WIDTH * 2 / 3, WINDOW_HEIGHT / 5, hWnd, (HMENU)VERSION, NULL, NULL);
	if (!open_button || !save_button || !listbox || !combobox || !hwnd_version)
		MessageBox(hWnd, TEXT("how did you even trigger this"), TEXT("GUI could not be initialized!"), MB_ICONEXCLAMATION); 
}

int CALLBACK SetFont(HWND child, LPARAM font) {
	SendMessage(child, WM_SETFONT, font, 1);
	return 1;
}

LRESULT CALLBACK WindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
	switch(msg) {
		case WM_COMMAND:
			if (HIWORD(wParam) == CBN_SELCHANGE) {
				switch (LOWORD(wParam)) {
					case COMBOBOX:
						version = (uint8_t)(8+SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0));
						break;
					case LISTBOX: {
						LRESULT i = SendMessage((HWND)lParam, (UINT)LB_GETCURSEL, (WPARAM)0, (LPARAM)0);
						type = (enum types)SendMessage((HWND)lParam, (UINT)LB_GETITEMDATA, (WPARAM)i, (LPARAM)0);
						break;
					}
					default:
						break;
				}
			}

			switch (wParam) {
				case OPEN_FILE_BUTTON:
					/* free(NULL) == no-op */
					free(file_path);
					open_file(hWnd, &file_path);
					break;
				case SAVE_FILE_BUTTON:
					save_file(hWnd, file_path, version, type);
					break;
				default:
					break;
			}
			break;
		case WM_CREATE:
			AddControls(hWnd);
			EnumChildWindows(hWnd, (WNDENUMPROC)SetFont, (LPARAM)GetStockObject(DEFAULT_GUI_FONT));
			break;
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
		case WM_DROPFILES: {
			/* Drag and drop support */
			free(file_path);

			HDROP drop = (HDROP)wParam;

			int needed = DragQueryFile(drop, 0, NULL, 0);
			file_path = malloc((needed + 1) * sizeof(TCHAR));
			DragQueryFile(drop, 0, file_path, needed + 1);
			file_path[needed] = L'\0';

			display_file(file_path);
			break;
		}
		default:
			return DefWindowProc(hWnd, msg, wParam, lParam);
	}
	return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR args, int ncmdshow) {
	WNDCLASS wc = {0};

	wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	wc.hInstance = hInstance;
	wc.lpszClassName = TEXT("msvpvf");
	wc.lpfnWndProc = WindowProcedure;

	if (!RegisterClass(&wc)) return -1;

	CreateWindowEx(WS_EX_ACCEPTFILES, TEXT("msvpvf"), TEXT("Movie Studio / Vegas Pro version spoofer"), WS_OVERLAPPED | WS_VISIBLE | WS_MINIMIZEBOX | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, NULL, NULL, hInstance, NULL);

	MSG msg = {0};

	while (GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return 0;
}