Compare commits

...

10 Commits

Author SHA1 Message Date
1b2ef4240d Remember last time file 2025-01-21 04:16:51 -06:00
844b32a6fd Add version info and about summary 2025-01-21 03:39:39 -06:00
c1f798be7a Add saving and loading 2025-01-21 03:08:49 -06:00
8b0efde52a Minor UI changes 2025-01-21 01:58:37 -06:00
489824b1f9 Add accelerators 2025-01-21 01:48:01 -06:00
ecca252027 Move to update 2025-01-21 01:46:44 -06:00
ab198d6778 Add icon and resume 2025-01-21 00:42:05 -06:00
d60cedf10d Add times together 2025-01-20 23:23:35 -06:00
fd6a14675b Basic timing 2025-01-20 23:06:16 -06:00
bf96e3bc9c Add menu 2025-01-20 21:41:36 -06:00
9 changed files with 353 additions and 19 deletions

View File

@ -1,5 +1,5 @@
# Mainspring app CMakeLists.txt # Mainspring app CMakeLists.txt
add_executable(Mainspring WIN32 "src/main.cpp" "src/pch.h" "src/Application.h" "src/Application.cpp" "resource.rc" "src/resources.h") add_executable(Mainspring WIN32 "src/main.cpp" "src/pch.h" "src/Application.h" "src/Application.cpp" "resource.rc" "src/resources.h" "src/utility.h" "src/utility.cpp")
set_property(TARGET Mainspring PROPERTY CXX_STANDARD 20) set_property(TARGET Mainspring PROPERTY CXX_STANDARD 20)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -2,12 +2,61 @@
#include "windows.h" #include "windows.h"
#include "winres.h" #include "winres.h"
#include "src/resources.h" #include "src/resources.h"
#include "version.h"
IDD_DIALOGMAIN DIALOGEX 0, 0, 175, 75 IDI_ICONMAIN ICON "res/mainspring.ico"
IDR_MENUMAIN MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&Reset\tCtrl+R" ID_FILE_RESET
MENUITEM "&Save Time\tCtrl+S" ID_FILE_SAVE
MENUITEM "Save Time &As...\tCtrl+Shift+S" ID_FILE_SAVEAS
MENUITEM "&Open Time\tCtrl+O" ID_FILE_OPEN
MENUITEM SEPARATOR
MENUITEM "&Exit\tAlt+F4" ID_FILE_EXIT
END
POPUP "&Help"
BEGIN
MENUITEM "&About" ID_HELP_ABOUT
END
END
IDD_DIALOGMAIN DIALOGEX 0, 0, 200, 70
STYLE WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT STYLE WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT
FONT 8, "MS Shell Dlg", 400, 0, 0x1 FONT 8, "MS Shell Dlg", 400, 0, 0x1
MENU IDR_MENUMAIN
BEGIN BEGIN
CTEXT "APPNAME", IDC_STATICTITLE, 30, 5, 115, 20, SS_CENTERIMAGE CTEXT "APPNAME", IDC_STATICTITLE, 0, 0, 200, 20, SS_CENTERIMAGE
CTEXT "TIME", IDC_STATICTIME, 10, 25, 155, 25, SS_CENTERIMAGE CTEXT "TIME", IDC_STATICTIME, 8, 20, 184, 28, SS_CENTERIMAGE | SS_SUNKEN
DEFPUSHBUTTON "STARTPAUSE" IDC_BUTTONSTARTPAUSE, 63, 55, 50, 15 DEFPUSHBUTTON "STARTPAUSE" IDC_BUTTONSTARTPAUSE, 75, 53, 50, 15
END
IDA_ACCELMAIN ACCELERATORS
BEGIN
"R", ID_FILE_RESET, CONTROL, VIRTKEY
"S", ID_FILE_SAVE, CONTROL, VIRTKEY
"S", ID_FILE_SAVEAS, CONTROL, SHIFT, VIRTKEY
"O", ID_FILE_OPEN, CONTROL, VIRTKEY
END
VS_VERSION_INFO VERSIONINFO
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Grayson Riffe"
VALUE "FileDescription", "Mainspring"
VALUE "ProductName", "Mainspring - A simple timekeeper"
VALUE "ProductVersion", APPVERSION
VALUE "LegalCopyright", "Copyright (C) 2025"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END END

View File

@ -2,6 +2,14 @@
#include "pch.h" #include "pch.h"
#include "Application.h" #include "Application.h"
#include "resources.h" #include "resources.h"
#include "utility.h"
// Define timer IDs
#define IDT_UPDATEAPP 1
#define IDT_SAVEDTEXT 2
// Define window message to update the timer display
#define WM_UPDATEAPP WM_USER + 1
// Enable visual styles // Enable visual styles
#pragma comment(linker, "\"/manifestdependency:type='win32' \ #pragma comment(linker, "\"/manifestdependency:type='win32' \
@ -12,12 +20,31 @@ namespace mainspring {
Application::Application(const char* appName, const char* appVersion) Application::Application(const char* appName, const char* appVersion)
: m_appName(appName) : m_appName(appName)
, m_appVersion(appVersion) , m_appVersion(appVersion)
, m_dlg()
, m_timing(false)
, m_startpauseButtonPressed(false)
, m_elapsedSaved()
, m_startTime()
, m_file()
, m_showSavedText(false)
{ {
std::cout << std::format("{} {}\n", m_appName, m_appVersion); std::cout << std::format("{} {}\n", m_appName, m_appVersion);
} }
void Application::run() { void Application::run() {
DialogBoxParam(nullptr, MAKEINTRESOURCE(IDD_DIALOGMAIN), nullptr, reinterpret_cast<DLGPROC>(mainDlgProc), reinterpret_cast<LPARAM>(this)); std::string configPath = getConfigPath();
std::string recentFile;
if (readFile(configPath.c_str(), recentFile) && !recentFile.empty())
open(recentFile);
m_dlg = CreateDialogParam(nullptr, MAKEINTRESOURCE(IDD_DIALOGMAIN), nullptr, reinterpret_cast<DLGPROC>(mainDlgProc), reinterpret_cast<LPARAM>(this));
HACCEL hAccel = LoadAccelerators(GetModuleHandle(NULL), MAKEINTRESOURCE(IDA_ACCELMAIN));
MSG msg = {};
ShowWindow(m_dlg, SW_SHOW);
while (GetMessage(&msg, nullptr, 0, 0))
if (!TranslateAccelerator(m_dlg, hAccel, &msg))
IsDialogMessage(m_dlg, &msg);
} }
BOOL CALLBACK Application::mainDlgProc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam) { BOOL CALLBACK Application::mainDlgProc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam) {
@ -28,6 +55,7 @@ namespace mainspring {
app = reinterpret_cast<Application*>(lParam); app = reinterpret_cast<Application*>(lParam);
SetWindowText(dlg, app->m_appName); SetWindowText(dlg, app->m_appName);
SendMessage(dlg, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICONMAIN))));
POINT cursor = {}; POINT cursor = {};
GetCursorPos(&cursor); GetCursorPos(&cursor);
@ -36,30 +64,206 @@ namespace mainspring {
GetMonitorInfo(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &mi); GetMonitorInfo(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &mi);
SetWindowPos(dlg, nullptr, mi.rcMonitor.left + 100, mi.rcMonitor.top + 100, 0, 0, SWP_NOZORDER | SWP_NOSIZE); SetWindowPos(dlg, nullptr, mi.rcMonitor.left + 100, mi.rcMonitor.top + 100, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
SetDlgItemText(dlg, IDC_STATICTITLE, app->m_appName);
LOGFONT lFont = {}; LOGFONT lFont = {};
lFont.lfHeight = 30; lFont.lfHeight = 30;
HFONT titleFont = CreateFontIndirect(&lFont); HFONT titleFont = CreateFontIndirect(&lFont);
SendMessage(GetDlgItem(dlg, IDC_STATICTITLE), WM_SETFONT, reinterpret_cast<WPARAM>(titleFont), NULL); SendMessage(GetDlgItem(dlg, IDC_STATICTITLE), WM_SETFONT, reinterpret_cast<WPARAM>(titleFont), NULL);
lFont.lfHeight = 40; std::memcpy(lFont.lfFaceName, "Consolas", 9);
lFont.lfHeight = 60;
HFONT timeFont = CreateFontIndirect(&lFont); HFONT timeFont = CreateFontIndirect(&lFont);
SendMessage(GetDlgItem(dlg, IDC_STATICTIME), WM_SETFONT, reinterpret_cast<WPARAM>(timeFont), NULL); SendMessage(GetDlgItem(dlg, IDC_STATICTIME), WM_SETFONT, reinterpret_cast<WPARAM>(timeFont), NULL);
SetDlgItemText(dlg, IDC_BUTTONSTARTPAUSE, "Start"); SendMessage(dlg, WM_UPDATEAPP, NULL, NULL);
SetTimer(dlg, IDT_UPDATEAPP, 50, nullptr);
return TRUE; return TRUE;
} }
case WM_CLOSE: case WM_COMMAND:
EndDialog(dlg, 0); switch (LOWORD(wParam)) {
case IDC_BUTTONSTARTPAUSE:
app->m_startpauseButtonPressed = true;
return TRUE;
case ID_FILE_RESET: {
if (!app->m_file.empty())
app->save();
app->reset();
return TRUE;
}
case ID_FILE_SAVE: {
app->save();
return TRUE;
}
case ID_FILE_SAVEAS: {
app->save(true);
return TRUE;
}
case ID_FILE_OPEN: {
if (!app->m_file.empty())
app->save();
app->open();
return TRUE;
}
case ID_FILE_EXIT: {
SendMessage(dlg, WM_CLOSE, NULL, NULL);
return TRUE;
}
case ID_HELP_ABOUT:
MessageBox(dlg, std::format("{} {}\nCopyright Grayson Riffe 2025\ngraysonriffe.com\n\n"
"\tMainspring is a simple timekeeper. It can be used as a simple stopwatch for short-term activities, or "
"it can log hours on a longer-term project. Times can be saved to a file, and any subsequent uses will autosave to that file. The most recent "
"time will be loaded when Mainspring starts.", app->m_appName, app->m_appVersion).c_str(), std::format("About {}", app->m_appName).c_str(), MB_OK);
return TRUE;
}
return FALSE;
case WM_TIMER:
switch (wParam) {
case IDT_UPDATEAPP:
SendMessage(dlg, WM_UPDATEAPP, NULL, NULL);
return TRUE;
case IDT_SAVEDTEXT:
app->m_showSavedText = false;
return TRUE;
}
return FALSE;
return TRUE;
case WM_UPDATEAPP: {
if (app->m_startpauseButtonPressed) {
app->m_startpauseButtonPressed = false;
if (!app->m_timing)
app->m_startTime = Clock::now();
else
app->m_elapsedSaved += app->getElapsed();
app->m_timing = !app->m_timing;
}
if (app->m_showSavedText)
SetDlgItemText(dlg, IDC_STATICTITLE, "Saved!");
else if (!app->m_file.empty()) {
std::string filename = app->m_file.substr(app->m_file.find_last_of('\\') + 1);
SetDlgItemText(dlg, IDC_STATICTITLE, filename.c_str());
}
else
SetDlgItemText(dlg, IDC_STATICTITLE, app->m_appName);
if (app->m_timing)
SetDlgItemText(dlg, IDC_BUTTONSTARTPAUSE, "Pause");
else
SetDlgItemText(dlg, IDC_BUTTONSTARTPAUSE, !app->m_elapsedSaved ? "Start" : "Resume");
Seconds totalTime = app->m_elapsedSaved + app->getElapsed();
uint32_t hr = totalTime / (3600), min = totalTime % (3600) / 60, sec = totalTime % 60;
SetDlgItemText(dlg, IDC_STATICTIME, std::format("{:03}:{:02}:{:02}", hr, min, sec).c_str());
return TRUE;
}
case WM_CLOSE: {
if (!app->m_file.empty())
app->save();
std::string configPath = app->getConfigPath();
writeFile(configPath.c_str(), app->m_file);
KillTimer(dlg, 1);
DestroyWindow(dlg);
return TRUE;
}
case WM_DESTROY:
PostQuitMessage(0);
return TRUE; return TRUE;
} }
return FALSE; return FALSE;
} }
Application::~Application() { std::string Application::getConfigPath() {
char appDataPath[MAX_PATH] = {};
SHGetFolderPath(nullptr, CSIDL_APPDATA, nullptr, NULL, appDataPath);
return std::string(appDataPath) + "\\" + m_appName + "\\last";
}
Seconds Application::getElapsed() {
if (m_timing)
return Duration(Clock::now() - m_startTime).count();
else
return 0;
}
void Application::reset() {
m_timing = false;
m_elapsedSaved = 0;
m_file.clear();
}
void Application::save(bool saveas) {
if (m_file.empty() || saveas) {
OPENFILENAME ofn = {};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = m_dlg;
ofn.lpstrFilter = "Mainspring Time File (*.mspring)\0*.mspring\0\0";
ofn.lpstrDefExt = "mspring";
char fileBuf[MAX_PATH] = {};
ofn.lpstrFile = fileBuf;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_OVERWRITEPROMPT;
if (GetSaveFileName(&ofn))
m_file = fileBuf;
else
return;
}
Seconds totalTime = m_elapsedSaved + getElapsed();
std::string saveData = std::to_string(totalTime);
writeFile(m_file.c_str(), saveData);
m_showSavedText = true;
SetTimer(m_dlg, IDT_SAVEDTEXT, 3000, nullptr);
}
void Application::open(std::string path) {
if (path.empty()) {
OPENFILENAME ofn = {};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = m_dlg;
ofn.lpstrFilter = "Mainspring Time File (*.mspring)\0*.mspring\0\0";
ofn.lpstrDefExt = "mspring";
char fileBuf[MAX_PATH] = {};
ofn.lpstrFile = fileBuf;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_FILEMUSTEXIST;
if (GetOpenFileName(&ofn))
m_file = fileBuf;
else
return;
}
else
m_file = path;
std::string openData;
if (readFile(m_file.c_str(), openData))
// Update status text;
m_elapsedSaved = std::stoull(openData);
} }
} }

View File

@ -1,17 +1,39 @@
// Application class header // Application class header
namespace mainspring { namespace mainspring {
using Clock = std::chrono::high_resolution_clock;
using TimePoint = std::chrono::time_point<Clock>;
using Duration = std::chrono::duration<double, std::ratio<1, 1>>;
using Seconds = uint64_t;
class Application { class Application {
public: public:
Application(const char* appName, const char* appVersion); Application(const char* appName, const char* appVersion);
void run(); void run();
~Application();
private: private:
static BOOL CALLBACK mainDlgProc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam); static BOOL CALLBACK mainDlgProc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam);
std::string getConfigPath();
Seconds getElapsed(); // Gets the current timing session time
void reset();
void save(bool saveas = false);
void open(std::string path = "");
const char* m_appName; const char* m_appName;
const char* m_appVersion; const char* m_appVersion;
HWND m_dlg;
// Current state of the app
bool m_timing; // Currently timing?
bool m_startpauseButtonPressed; // Was the start / pause button pressed?
Seconds m_elapsedSaved; // Time saved
TimePoint m_startTime; // When the current timing session was started
std::string m_file; // Current time file path
bool m_showSavedText; // Save confirmation
}; };
} }

View File

@ -3,6 +3,7 @@
// IO and strings // IO and strings
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <filesystem>
#include <string> #include <string>
#include <format> #include <format>
@ -12,3 +13,5 @@
// Windows // Windows
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <Windows.h> #include <Windows.h>
#include <commdlg.h>
#include <ShlObj.h>

View File

@ -1,6 +1,26 @@
// Win32 resources // Win32 resources
#define IDD_DIALOGMAIN 101
// Icon
#define IDI_ICONMAIN 101
// Dialog
#define IDD_DIALOGMAIN 201
// Menu
#define IDR_MENUMAIN 301
#define ID_FILE_RESET 302
#define ID_FILE_SAVE 303
#define ID_FILE_SAVEAS 304
#define ID_FILE_OPEN 305
#define ID_FILE_EXIT 306
#define ID_HELP_ABOUT 311
// Accelerators
#define IDA_ACCELMAIN 401
// Controls (text and buttons)
#define IDC_BUTTONSTARTPAUSE 1001 #define IDC_BUTTONSTARTPAUSE 1001
#define IDC_STATICTITLE 1002 #define IDC_STATICTITLE 1002
#define IDC_STATICTIME 1003 #define IDC_STATICTIME 1003
#define IDC_STATICICON 1004

View File

@ -0,0 +1,30 @@
// Utility implementation
#include "pch.h"
#include "utility.h"
namespace mainspring {
bool readFile(const char* filename, std::string& out) {
std::ifstream fileStream(filename, std::ios::binary | std::ios::ate);
if (!fileStream.is_open())
return false;
size_t fileSize = fileStream.tellg();
fileStream.seekg(0);
out.resize(fileSize);
fileStream.read(out.data(), fileSize);
return true;
}
bool writeFile(const char* filename, const std::string& in) {
std::string folder = filename;
folder.resize(folder.find_last_of('\\'));
if (!std::filesystem::exists(folder))
std::filesystem::create_directories(folder);
std::ofstream fileStream(filename, std::ios::binary | std::ios::trunc);
if (!fileStream.is_open())
return false;
fileStream << in;
return true;
}
}

6
Mainspring/src/utility.h Normal file
View File

@ -0,0 +1,6 @@
// Utility header
namespace mainspring {
bool readFile(const char* filename, std::string& out);
bool writeFile(const char* filename, const std::string& in);
}