1029 lines
39 KiB
C
1029 lines
39 KiB
C
// QuickNote: type directly on the Windows desktop.
|
|
// Double-click an empty spot on the wallpaper and start typing. The text stays
|
|
// on the desktop (behind your windows, like desktop icons) and is saved automatically.
|
|
// Drag across text to select it, Ctrl+C to copy. Drag the grip on the left edge to move a note.
|
|
|
|
#ifndef UNICODE
|
|
#define UNICODE
|
|
#endif
|
|
#ifndef _UNICODE
|
|
#define _UNICODE
|
|
#endif
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define _WIN32_WINNT 0x0A00
|
|
#define COBJMACROS
|
|
#include <windows.h>
|
|
#include "desktools.h"
|
|
#include <shellapi.h>
|
|
#include <shlobj.h>
|
|
#include <oleacc.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <wchar.h>
|
|
|
|
#define APP_NAME L"QuickNote"
|
|
#define WM_TRAY (WM_APP + 1)
|
|
#define WM_DESKDBL (WM_APP + 2)
|
|
#define WM_KILLNOTE (WM_APP + 3)
|
|
#define MAX_NOTES 256
|
|
#define MAX_LINES 2000
|
|
#define FONT_PT 10
|
|
|
|
#define TIMER_BLINK 1
|
|
#define TIMER_SAVE 2
|
|
#define TIMER_CHECK 3
|
|
|
|
#define IDM_NEW 100
|
|
#define IDM_HIDE 101
|
|
#define IDM_STARTUP 102
|
|
#define IDM_HELP 103
|
|
#define IDM_EXIT 104
|
|
#define IDM_COLOR0 200
|
|
#define IDM_COPY 400
|
|
#define IDM_COPYALL 401
|
|
#define IDM_PASTE 402
|
|
#define IDM_SELALL 403
|
|
#define IDM_DELETE 404
|
|
|
|
typedef struct {
|
|
BOOL used;
|
|
HWND hwnd;
|
|
WCHAR *text;
|
|
int len, cap;
|
|
int caret; // cursor position
|
|
int anchor; // other end of the selection (== caret when nothing is selected)
|
|
COLORREF color;
|
|
BOOL active, caretOn, hover;
|
|
} Note;
|
|
|
|
static const COLORREF COLORS[] = {
|
|
RGB(0, 0, 0), RGB(255, 255, 255), RGB(255, 214, 10), RGB(255, 140, 40),
|
|
RGB(240, 70, 70), RGB(70, 200, 110), RGB(80, 160, 255), RGB(235, 110, 200)
|
|
};
|
|
static const WCHAR *COLOR_NAMES[] = {
|
|
L"Black", L"White", L"Yellow", L"Orange", L"Red", L"Green", L"Blue", L"Pink"
|
|
};
|
|
#define NCOLORS 8
|
|
|
|
static Note g_notes[MAX_NOTES];
|
|
static BOOL g_enabled = TRUE;
|
|
static HINSTANCE g_inst;
|
|
static HWND g_main;
|
|
static HHOOK g_hook;
|
|
static NOTIFYICONDATAW g_nid;
|
|
static UINT g_taskbarCreated;
|
|
static int g_dpi = 96;
|
|
static HFONT g_font;
|
|
static WCHAR g_file[MAX_PATH];
|
|
static BOOL g_dirty, g_hidden;
|
|
static HWND g_lastFg;
|
|
static int g_lastColor = 0;
|
|
|
|
// mouse state inside a note
|
|
static BOOL g_moving, g_selecting;
|
|
static POINT g_dragStart, g_dragOrig;
|
|
|
|
// double-click detection in the hook
|
|
static DWORD g_lastDownTime;
|
|
static POINT g_lastDownPt;
|
|
|
|
static int S(int v) { return MulDiv(v, g_dpi, 96); }
|
|
static int GripW(void) { return S(8); } // grip square size
|
|
|
|
/* ---------------- settings ---------------- */
|
|
|
|
static const WCHAR *RUN_KEY = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
static BOOL StartupEnabled(void) {
|
|
WCHAR buf[MAX_PATH * 2]; DWORD sz = sizeof(buf);
|
|
return RegGetValueW(HKEY_CURRENT_USER, RUN_KEY, APP_NAME, RRF_RT_REG_SZ, NULL, buf, &sz) == ERROR_SUCCESS;
|
|
}
|
|
static void SetStartup(BOOL on) {
|
|
HKEY k;
|
|
if (RegOpenKeyExW(HKEY_CURRENT_USER, RUN_KEY, 0, KEY_WRITE, &k) != ERROR_SUCCESS) return;
|
|
if (on) {
|
|
WCHAR exe[MAX_PATH], val[MAX_PATH + 4];
|
|
GetModuleFileNameW(NULL, exe, MAX_PATH);
|
|
swprintf(val, MAX_PATH + 4, L"\"%ls\"", exe);
|
|
RegSetValueExW(k, APP_NAME, 0, REG_SZ, (BYTE *)val, (DWORD)((wcslen(val) + 1) * sizeof(WCHAR)));
|
|
} else {
|
|
RegDeleteValueW(k, APP_NAME);
|
|
}
|
|
RegCloseKey(k);
|
|
}
|
|
static DWORD RegGetFlag(const WCHAR *name) {
|
|
DWORD v = 0, sz = sizeof(v);
|
|
RegGetValueW(HKEY_CURRENT_USER, L"Software\\QuickNote", name, RRF_RT_REG_DWORD, NULL, &v, &sz);
|
|
return v;
|
|
}
|
|
static void RegSetFlag(const WCHAR *name, DWORD v) {
|
|
HKEY k;
|
|
if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\QuickNote", 0, NULL, 0, KEY_WRITE, NULL, &k, NULL) == ERROR_SUCCESS) {
|
|
RegSetValueExW(k, name, 0, REG_DWORD, (BYTE *)&v, sizeof(v));
|
|
RegCloseKey(k);
|
|
}
|
|
}
|
|
|
|
/* ---------------- text buffer ---------------- */
|
|
|
|
static void EnsureCap(Note *n, int need) {
|
|
if (n->cap >= need + 1) return;
|
|
int c = n->cap ? n->cap : 64;
|
|
while (c < need + 1) c *= 2;
|
|
n->text = (WCHAR *)realloc(n->text, c * sizeof(WCHAR));
|
|
n->cap = c;
|
|
}
|
|
static BOOL HasSel(Note *n) { return n->anchor != n->caret; }
|
|
static void SelRange(Note *n, int *a, int *b) {
|
|
*a = n->anchor < n->caret ? n->anchor : n->caret;
|
|
*b = n->anchor < n->caret ? n->caret : n->anchor;
|
|
}
|
|
static void DeleteRange(Note *n, int a, int b) {
|
|
if (a < 0) a = 0;
|
|
if (b > n->len) b = n->len;
|
|
if (b <= a) return;
|
|
memmove(n->text + a, n->text + b, (n->len - b) * sizeof(WCHAR));
|
|
n->len -= (b - a);
|
|
n->text[n->len] = 0;
|
|
n->caret = n->anchor = a;
|
|
}
|
|
static void DeleteSel(Note *n) {
|
|
if (!HasSel(n)) return;
|
|
int a, b; SelRange(n, &a, &b);
|
|
DeleteRange(n, a, b);
|
|
}
|
|
static void InsertText(Note *n, const WCHAR *s, int cnt) {
|
|
DeleteSel(n);
|
|
EnsureCap(n, n->len + cnt);
|
|
memmove(n->text + n->caret + cnt, n->text + n->caret, (n->len - n->caret) * sizeof(WCHAR));
|
|
memcpy(n->text + n->caret, s, cnt * sizeof(WCHAR));
|
|
n->len += cnt; n->caret += cnt;
|
|
n->anchor = n->caret;
|
|
n->text[n->len] = 0;
|
|
}
|
|
|
|
static int LineStarts(Note *n, int *starts) {
|
|
int c = 0;
|
|
starts[c++] = 0;
|
|
for (int i = 0; i < n->len && c < MAX_LINES; i++) if (n->text[i] == L'\n') starts[c++] = i + 1;
|
|
return c;
|
|
}
|
|
static int LineEnd(Note *n, int *starts, int count, int line) {
|
|
return (line + 1 < count) ? starts[line + 1] - 1 : n->len;
|
|
}
|
|
static int LineOfIndex(int *starts, int count, int idx) {
|
|
int l = 0;
|
|
while (l + 1 < count && starts[l + 1] <= idx) l++;
|
|
return l;
|
|
}
|
|
static BOOL IsWordChar(WCHAR c) { return c != L' ' && c != L'\t' && c != L'\n'; }
|
|
|
|
/* ---------------- layout ---------------- */
|
|
|
|
typedef struct { int left, pad, lineH, w, h, lines; } Layout;
|
|
|
|
static void NoteLayout(Note *n, HDC dc, int *starts, Layout *L) {
|
|
TEXTMETRICW tm; GetTextMetricsW(dc, &tm);
|
|
L->pad = S(4);
|
|
L->left = GripW() + S(3);
|
|
L->lineH = tm.tmHeight;
|
|
L->lines = LineStarts(n, starts);
|
|
int maxw = 0;
|
|
for (int i = 0; i < L->lines; i++) {
|
|
int a = starts[i], b = LineEnd(n, starts, L->lines, i);
|
|
SIZE sz = { 0, 0 };
|
|
if (b > a) GetTextExtentPoint32W(dc, n->text + a, b - a, &sz);
|
|
if (sz.cx > maxw) maxw = sz.cx;
|
|
}
|
|
if (maxw < S(20)) maxw = S(20);
|
|
L->w = L->left + maxw + L->pad + S(4);
|
|
L->h = L->lines * L->lineH + L->pad * 2;
|
|
}
|
|
|
|
static int TextX(HDC dc, Note *n, int lineStart, int idx) {
|
|
SIZE sz = { 0, 0 };
|
|
if (idx > lineStart) GetTextExtentPoint32W(dc, n->text + lineStart, idx - lineStart, &sz);
|
|
return sz.cx;
|
|
}
|
|
|
|
static int CaretFromPoint(Note *n, int x, int y) {
|
|
HDC dc = CreateCompatibleDC(NULL);
|
|
SelectObject(dc, g_font);
|
|
static int starts[MAX_LINES];
|
|
Layout L; NoteLayout(n, dc, starts, &L);
|
|
int line = (y < L.pad) ? 0 : (y - L.pad) / L.lineH;
|
|
if (line >= L.lines) line = L.lines - 1;
|
|
int a = starts[line], b = LineEnd(n, starts, L.lines, line), cnt = b - a;
|
|
int lx = x - L.left, result = a;
|
|
if (y < 0) result = 0; // dragged above the note: start of text
|
|
else if (y >= L.h) result = n->len; // dragged below: end of text
|
|
else if (cnt > 0 && lx > 0) {
|
|
int *dx = (int *)malloc(cnt * sizeof(int));
|
|
SIZE sz;
|
|
GetTextExtentExPointW(dc, n->text + a, cnt, 0, NULL, dx, &sz);
|
|
result = b;
|
|
for (int i = 0; i < cnt; i++) {
|
|
int left = i ? dx[i - 1] : 0, right = dx[i];
|
|
if (lx < (left + right) / 2) { result = a + i; break; }
|
|
}
|
|
free(dx);
|
|
}
|
|
DeleteDC(dc);
|
|
return result;
|
|
}
|
|
|
|
/* ---------------- desktop z-order ---------------- */
|
|
|
|
static HWND FindDesktopHost(void) {
|
|
HWND p = FindWindowW(L"Progman", NULL);
|
|
if (p && FindWindowExW(p, NULL, L"SHELLDLL_DefView", NULL)) return p;
|
|
HWND w = NULL;
|
|
while ((w = FindWindowExW(NULL, w, L"WorkerW", NULL)) != NULL)
|
|
if (FindWindowExW(w, NULL, L"SHELLDLL_DefView", NULL)) return w;
|
|
return p;
|
|
}
|
|
|
|
static void PushToDesktop(HWND h) {
|
|
SetWindowPos(h, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); // adjusted in WM_WINDOWPOSCHANGING
|
|
}
|
|
|
|
static BOOL IsDesktopClass(HWND h) {
|
|
WCHAR cls[64];
|
|
if (!h || !GetClassNameW(h, cls, 64)) return FALSE;
|
|
return !wcscmp(cls, L"Progman") || !wcscmp(cls, L"WorkerW");
|
|
}
|
|
|
|
/* ---------------- rendering ---------------- */
|
|
|
|
static void DrawTextLayer(HDC dc, Note *n, int *starts, Layout *L) {
|
|
SetBkMode(dc, TRANSPARENT);
|
|
SelectObject(dc, g_font);
|
|
int lum = (GetRValue(n->color) * 3 + GetGValue(n->color) * 6 + GetBValue(n->color)) / 10;
|
|
for (int pass = (lum > 128 ? 0 : 1); pass < 2; pass++) { // light text gets a dark shadow; dark text none
|
|
SetTextColor(dc, pass == 0 ? RGB(0, 0, 0) : n->color);
|
|
int off = pass == 0 ? 1 : 0;
|
|
for (int i = 0; i < L->lines; i++) {
|
|
int a = starts[i], b = LineEnd(n, starts, L->lines, i);
|
|
if (b <= a) continue;
|
|
TextOutW(dc, L->left + off, L->pad + i * L->lineH + off, n->text + a, b - a);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void RenderNote(Note *n) {
|
|
if (!n->used || !n->hwnd) return;
|
|
static int starts[MAX_LINES];
|
|
HDC screen = GetDC(NULL);
|
|
HDC mdc = CreateCompatibleDC(screen);
|
|
SelectObject(mdc, g_font);
|
|
Layout L; NoteLayout(n, mdc, starts, &L);
|
|
int W = L.w, H = L.h;
|
|
|
|
// caret position
|
|
int cline = LineOfIndex(starts, L.lines, n->caret);
|
|
int caretX = L.left + TextX(mdc, n, starts[cline], n->caret), caretY = L.pad + cline * L.lineH;
|
|
|
|
// selection: per line pixel span [x1, x2)
|
|
int *selX1 = NULL, *selX2 = NULL, sa = 0, sb = 0, la = 0, lb = -1;
|
|
if (HasSel(n)) {
|
|
SelRange(n, &sa, &sb);
|
|
la = LineOfIndex(starts, L.lines, sa);
|
|
lb = LineOfIndex(starts, L.lines, sb);
|
|
selX1 = (int *)calloc(L.lines, sizeof(int));
|
|
selX2 = (int *)calloc(L.lines, sizeof(int));
|
|
for (int i = la; i <= lb; i++) {
|
|
int ls = starts[i], le = LineEnd(n, starts, L.lines, i);
|
|
selX1[i] = L.left + (i == la ? TextX(mdc, n, ls, sa) : 0);
|
|
selX2[i] = L.left + (i == lb ? TextX(mdc, n, ls, sb) : TextX(mdc, n, ls, le) + S(4));
|
|
}
|
|
}
|
|
DeleteDC(mdc);
|
|
|
|
BITMAPINFO bi; ZeroMemory(&bi, sizeof(bi));
|
|
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
bi.bmiHeader.biWidth = W; bi.bmiHeader.biHeight = -H;
|
|
bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32; bi.bmiHeader.biCompression = BI_RGB;
|
|
BYTE *pb = NULL, *pw = NULL, *po = NULL;
|
|
HDC db = CreateCompatibleDC(screen), dw = CreateCompatibleDC(screen), dout = CreateCompatibleDC(screen);
|
|
HBITMAP bb = CreateDIBSection(screen, &bi, DIB_RGB_COLORS, (void **)&pb, NULL, 0);
|
|
HBITMAP bw = CreateDIBSection(screen, &bi, DIB_RGB_COLORS, (void **)&pw, NULL, 0);
|
|
HBITMAP bo = CreateDIBSection(screen, &bi, DIB_RGB_COLORS, (void **)&po, NULL, 0);
|
|
if (bb && bw && bo) {
|
|
HGDIOBJ ob = SelectObject(db, bb), ow = SelectObject(dw, bw), oo = SelectObject(dout, bo);
|
|
RECT all = { 0, 0, W, H };
|
|
FillRect(db, &all, (HBRUSH)GetStockObject(BLACK_BRUSH));
|
|
FillRect(dw, &all, (HBRUSH)GetStockObject(WHITE_BRUSH));
|
|
DrawTextLayer(db, n, starts, &L);
|
|
DrawTextLayer(dw, n, starts, &L);
|
|
GdiFlush();
|
|
|
|
// the only extra mark is a small grip square, shown while pointing at or editing the note
|
|
BOOL showGrip = n->active || n->hover;
|
|
int gw = GripW(), gy = L.pad + (L.lineH - gw) / 2;
|
|
int cw = S(1) > 1 ? S(1) : 1;
|
|
BYTE tr = GetRValue(n->color), tg = GetGValue(n->color), tb = GetBValue(n->color);
|
|
|
|
for (int y = 0; y < H; y++) {
|
|
int line = (y >= L.pad) ? (y - L.pad) / L.lineH : -1;
|
|
BOOL lineSel = selX1 && line >= la && line <= lb && line < L.lines;
|
|
for (int x = 0; x < W; x++) {
|
|
int i = (y * W + x) * 4;
|
|
int d = ((pw[i] - pb[i]) + (pw[i + 1] - pb[i + 1]) + (pw[i + 2] - pb[i + 2])) / 3;
|
|
int a = 255 - d; if (a < 0) a = 0; if (a > 255) a = 255;
|
|
int c0 = pb[i], c1 = pb[i + 1], c2 = pb[i + 2];
|
|
if (c0 > a) c0 = a; if (c1 > a) c1 = a; if (c2 > a) c2 = a;
|
|
|
|
// layers under the text: selection highlight and the grip square
|
|
int ua = 0, ur = 0, ug = 0, ub = 0;
|
|
if (lineSel && x >= selX1[line] && x < selX2[line]) { ur = 51; ug = 153; ub = 255; ua = 130; }
|
|
else if (showGrip && x < gw && y >= gy && y < gy + gw) { ur = ug = ub = 120; ua = 210; }
|
|
if (ua) {
|
|
int inv = 255 - a;
|
|
c0 += (ub * ua / 255) * inv / 255;
|
|
c1 += (ug * ua / 255) * inv / 255;
|
|
c2 += (ur * ua / 255) * inv / 255;
|
|
a += ua * inv / 255;
|
|
}
|
|
if (n->active && n->caretOn && !HasSel(n) && x >= caretX && x < caretX + cw &&
|
|
y >= caretY && y < caretY + L.lineH) {
|
|
c0 = tb; c1 = tg; c2 = tr; a = 255;
|
|
}
|
|
if (a < 1) a = 1; // whole box stays clickable
|
|
po[i] = (BYTE)c0; po[i + 1] = (BYTE)c1; po[i + 2] = (BYTE)c2; po[i + 3] = (BYTE)a;
|
|
}
|
|
}
|
|
SIZE sz = { W, H }; POINT src = { 0, 0 };
|
|
BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
|
|
UpdateLayeredWindow(n->hwnd, screen, NULL, &sz, dout, &src, 0, &bf, ULW_ALPHA);
|
|
SelectObject(db, ob); SelectObject(dw, ow); SelectObject(dout, oo);
|
|
}
|
|
free(selX1); free(selX2);
|
|
if (bb) DeleteObject(bb);
|
|
if (bw) DeleteObject(bw);
|
|
if (bo) DeleteObject(bo);
|
|
DeleteDC(db); DeleteDC(dw); DeleteDC(dout);
|
|
ReleaseDC(NULL, screen);
|
|
}
|
|
|
|
/* ---------------- saving ---------------- */
|
|
|
|
static void MarkDirty(void) { g_dirty = TRUE; SetTimer(g_main, TIMER_SAVE, 1200, NULL); }
|
|
|
|
static void SaveNotes(void) {
|
|
g_dirty = FALSE;
|
|
size_t cap = 4096, len = 0;
|
|
WCHAR *buf = (WCHAR *)malloc(cap * sizeof(WCHAR));
|
|
for (int k = 0; k < MAX_NOTES; k++) {
|
|
Note *n = &g_notes[k];
|
|
if (!n->used || n->len == 0) continue;
|
|
RECT r; GetWindowRect(n->hwnd, &r);
|
|
WCHAR head[96];
|
|
int hl = swprintf(head, 96, L"%ld\t%ld\t%d\t%06lX\t", r.left, r.top, FONT_PT, (unsigned long)n->color);
|
|
size_t need = len + hl + n->len * 2 + 4;
|
|
if (need > cap) { while (need > cap) cap *= 2; buf = (WCHAR *)realloc(buf, cap * sizeof(WCHAR)); }
|
|
memcpy(buf + len, head, hl * sizeof(WCHAR)); len += hl;
|
|
for (int i = 0; i < n->len; i++) {
|
|
WCHAR c = n->text[i];
|
|
if (c == L'\\') { buf[len++] = L'\\'; buf[len++] = L'\\'; }
|
|
else if (c == L'\n') { buf[len++] = L'\\'; buf[len++] = L'n'; }
|
|
else if (c == L'\t') { buf[len++] = L'\\'; buf[len++] = L't'; }
|
|
else if (c == L'\r') { }
|
|
else buf[len++] = c;
|
|
}
|
|
buf[len++] = L'\n';
|
|
}
|
|
int bytes = WideCharToMultiByte(CP_UTF8, 0, buf, (int)len, NULL, 0, NULL, NULL);
|
|
char *utf8 = (char *)malloc(bytes + 1);
|
|
WideCharToMultiByte(CP_UTF8, 0, buf, (int)len, utf8, bytes, NULL, NULL);
|
|
WCHAR tmp[MAX_PATH + 8]; swprintf(tmp, MAX_PATH + 8, L"%ls.tmp", g_file);
|
|
HANDLE f = CreateFileW(tmp, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (f != INVALID_HANDLE_VALUE) {
|
|
DWORD wr; WriteFile(f, utf8, bytes, &wr, NULL);
|
|
CloseHandle(f);
|
|
MoveFileExW(tmp, g_file, MOVEFILE_REPLACE_EXISTING);
|
|
}
|
|
free(utf8); free(buf);
|
|
}
|
|
|
|
/* ---------------- notes ---------------- */
|
|
|
|
static Note *NewNoteSlot(void) {
|
|
for (int i = 0; i < MAX_NOTES; i++) if (!g_notes[i].used) {
|
|
ZeroMemory(&g_notes[i], sizeof(Note));
|
|
return &g_notes[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void ForceForeground(HWND h) {
|
|
HWND fg = GetForegroundWindow();
|
|
DWORD ft = fg ? GetWindowThreadProcessId(fg, NULL) : 0, me = GetCurrentThreadId();
|
|
if (ft && ft != me) AttachThreadInput(me, ft, TRUE);
|
|
BringWindowToTop(h);
|
|
SetForegroundWindow(h);
|
|
SetFocus(h);
|
|
if (ft && ft != me) AttachThreadInput(me, ft, FALSE);
|
|
}
|
|
|
|
static Note *CreateNote(int x, int y, const WCHAR *text, int tlen, COLORREF color, BOOL activate) {
|
|
Note *n = NewNoteSlot();
|
|
if (!n) return NULL;
|
|
n->used = TRUE;
|
|
n->color = color;
|
|
EnsureCap(n, tlen);
|
|
if (tlen) memcpy(n->text, text, tlen * sizeof(WCHAR));
|
|
n->len = tlen; n->text[n->len] = 0; n->caret = n->anchor = tlen;
|
|
n->active = activate;
|
|
n->caretOn = TRUE;
|
|
n->hwnd = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOOLWINDOW, L"QuickNoteText", L"QuickNote", WS_POPUP,
|
|
x, y, 10, 10, NULL, NULL, g_inst, n);
|
|
if (!n->hwnd) { free(n->text); n->used = FALSE; return NULL; }
|
|
RenderNote(n);
|
|
if (g_hidden) return n;
|
|
if (activate) {
|
|
ShowWindow(n->hwnd, SW_SHOW);
|
|
ForceForeground(n->hwnd);
|
|
} else {
|
|
ShowWindow(n->hwnd, SW_SHOWNA);
|
|
PushToDesktop(n->hwnd);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static void DeleteNote(Note *n) {
|
|
if (!n->used) return;
|
|
n->used = FALSE;
|
|
if (n->hwnd) DestroyWindow(n->hwnd);
|
|
n->hwnd = NULL;
|
|
free(n->text); n->text = NULL; n->len = n->cap = 0;
|
|
MarkDirty();
|
|
}
|
|
|
|
static void LoadNotes(void) {
|
|
HANDLE f = CreateFileW(g_file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (f == INVALID_HANDLE_VALUE) return;
|
|
DWORD size = GetFileSize(f, NULL), rd = 0;
|
|
char *utf8 = (char *)malloc(size + 1);
|
|
ReadFile(f, utf8, size, &rd, NULL);
|
|
CloseHandle(f);
|
|
int wl = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)rd, NULL, 0);
|
|
WCHAR *w = (WCHAR *)malloc((wl + 1) * sizeof(WCHAR));
|
|
MultiByteToWideChar(CP_UTF8, 0, utf8, (int)rd, w, wl);
|
|
w[wl] = 0;
|
|
free(utf8);
|
|
|
|
WCHAR *line = w;
|
|
while (line && *line) {
|
|
WCHAR *nl = wcschr(line, L'\n');
|
|
if (nl) *nl = 0;
|
|
long x, y; int sz; unsigned long col; int consumed = 0;
|
|
if (swscanf(line, L"%ld\t%ld\t%d\t%lx\t%n", &x, &y, &sz, &col, &consumed) >= 4 && consumed > 0) {
|
|
WCHAR *src = line + consumed;
|
|
int sl = (int)wcslen(src);
|
|
WCHAR *txt = (WCHAR *)malloc((sl + 1) * sizeof(WCHAR));
|
|
int tl = 0;
|
|
for (int i = 0; i < sl; i++) {
|
|
if (src[i] == L'\\' && i + 1 < sl) {
|
|
WCHAR e = src[++i];
|
|
txt[tl++] = e == L'n' ? L'\n' : e == L't' ? L'\t' : e;
|
|
} else if (src[i] != L'\r') txt[tl++] = src[i];
|
|
}
|
|
POINT p = { x + 5, y + 5 };
|
|
if (!MonitorFromPoint(p, MONITOR_DEFAULTTONULL)) {
|
|
RECT wa; SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0);
|
|
x = wa.left + S(40); y = wa.top + S(40);
|
|
}
|
|
if (tl > 0) CreateNote((int)x, (int)y, txt, tl, (COLORREF)col, FALSE);
|
|
free(txt);
|
|
}
|
|
line = nl ? nl + 1 : NULL;
|
|
}
|
|
free(w);
|
|
}
|
|
|
|
static void SetAllHidden(BOOL hide) {
|
|
g_hidden = hide;
|
|
for (int i = 0; i < MAX_NOTES; i++) {
|
|
Note *n = &g_notes[i];
|
|
if (!n->used) continue;
|
|
if (hide) ShowWindow(n->hwnd, SW_HIDE);
|
|
else { ShowWindow(n->hwnd, SW_SHOWNA); PushToDesktop(n->hwnd); }
|
|
}
|
|
}
|
|
|
|
/* ---------------- clipboard ---------------- */
|
|
|
|
static void CopyToClipboard(HWND h, const WCHAR *s, int len) {
|
|
if (len <= 0 || !OpenClipboard(h)) return;
|
|
EmptyClipboard();
|
|
int extra = 0;
|
|
for (int i = 0; i < len; i++) if (s[i] == L'\n') extra++;
|
|
HGLOBAL g = GlobalAlloc(GMEM_MOVEABLE, (len + extra + 1) * sizeof(WCHAR));
|
|
if (g) {
|
|
WCHAR *p = (WCHAR *)GlobalLock(g); int k = 0;
|
|
for (int i = 0; i < len; i++) { if (s[i] == L'\n') p[k++] = L'\r'; p[k++] = s[i]; }
|
|
p[k] = 0;
|
|
GlobalUnlock(g);
|
|
SetClipboardData(CF_UNICODETEXT, g);
|
|
}
|
|
CloseClipboard();
|
|
}
|
|
|
|
// copies the selection, or the whole note when nothing is selected
|
|
static void CopyNote(HWND h, Note *n, BOOL whole) {
|
|
if (!whole && HasSel(n)) {
|
|
int a, b; SelRange(n, &a, &b);
|
|
CopyToClipboard(h, n->text + a, b - a);
|
|
} else {
|
|
CopyToClipboard(h, n->text, n->len);
|
|
}
|
|
}
|
|
|
|
static void PasteFromClipboard(HWND h, Note *n) {
|
|
if (!OpenClipboard(h)) return;
|
|
HANDLE g = GetClipboardData(CF_UNICODETEXT);
|
|
if (g) {
|
|
const WCHAR *p = (const WCHAR *)GlobalLock(g);
|
|
if (p) {
|
|
int l = (int)wcslen(p);
|
|
WCHAR *clean = (WCHAR *)malloc((l + 1) * sizeof(WCHAR)); int k = 0;
|
|
for (int i = 0; i < l; i++) if (p[i] != L'\r') clean[k++] = p[i];
|
|
InsertText(n, clean, k);
|
|
free(clean);
|
|
GlobalUnlock(g);
|
|
}
|
|
}
|
|
CloseClipboard();
|
|
}
|
|
|
|
/* ---------------- note window ---------------- */
|
|
|
|
static void MoveCaretVertical(Note *n, int dir) {
|
|
static int starts[MAX_LINES];
|
|
HDC dc = CreateCompatibleDC(NULL);
|
|
SelectObject(dc, g_font);
|
|
Layout L; NoteLayout(n, dc, starts, &L);
|
|
int line = LineOfIndex(starts, L.lines, n->caret);
|
|
int x = L.left + TextX(dc, n, starts[line], n->caret);
|
|
DeleteDC(dc);
|
|
int target = line + dir;
|
|
if (target < 0) { n->caret = 0; return; }
|
|
if (target >= L.lines) { n->caret = n->len; return; }
|
|
n->caret = CaretFromPoint(n, x, L.pad + target * L.lineH + L.lineH / 2);
|
|
}
|
|
|
|
static void SelectWordAt(Note *n, int idx) {
|
|
int a = idx, b = idx;
|
|
if (a < n->len && !IsWordChar(n->text[a]) && a > 0 && IsWordChar(n->text[a - 1])) { a--; b--; }
|
|
while (a > 0 && IsWordChar(n->text[a - 1])) a--;
|
|
while (b < n->len && IsWordChar(n->text[b])) b++;
|
|
n->anchor = a; n->caret = b;
|
|
}
|
|
|
|
static void ShowNoteMenu(HWND h, Note *n) {
|
|
HMENU m = CreatePopupMenu(), mc = CreatePopupMenu();
|
|
for (int i = 0; i < NCOLORS; i++)
|
|
AppendMenuW(mc, MF_STRING | (n->color == COLORS[i] ? MF_CHECKED : 0), IDM_COLOR0 + i, COLOR_NAMES[i]);
|
|
AppendMenuW(m, MF_STRING | (HasSel(n) ? 0 : MF_GRAYED), IDM_COPY, L"Copy\tCtrl+C");
|
|
AppendMenuW(m, MF_STRING, IDM_COPYALL, L"Copy all text");
|
|
AppendMenuW(m, MF_STRING | (IsClipboardFormatAvailable(CF_UNICODETEXT) ? 0 : MF_GRAYED), IDM_PASTE, L"Paste\tCtrl+V");
|
|
AppendMenuW(m, MF_STRING, IDM_SELALL, L"Select all\tCtrl+A");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, NULL);
|
|
AppendMenuW(m, MF_POPUP, (UINT_PTR)mc, L"Color");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, NULL);
|
|
AppendMenuW(m, MF_STRING, IDM_DELETE, L"Delete this note");
|
|
POINT p; GetCursorPos(&p);
|
|
int cmd = TrackPopupMenu(m, TPM_RIGHTBUTTON | TPM_RETURNCMD, p.x, p.y, 0, h, NULL);
|
|
DestroyMenu(m);
|
|
if (cmd >= IDM_COLOR0 && cmd < IDM_COLOR0 + NCOLORS) {
|
|
n->color = COLORS[cmd - IDM_COLOR0]; g_lastColor = cmd - IDM_COLOR0;
|
|
RegSetFlag(L"LastColor", g_lastColor);
|
|
RenderNote(n); MarkDirty();
|
|
} else if (cmd == IDM_COPY) {
|
|
CopyNote(h, n, FALSE);
|
|
} else if (cmd == IDM_COPYALL) {
|
|
CopyNote(h, n, TRUE);
|
|
} else if (cmd == IDM_PASTE) {
|
|
PasteFromClipboard(h, n); RenderNote(n); MarkDirty();
|
|
} else if (cmd == IDM_SELALL) {
|
|
n->anchor = 0; n->caret = n->len; RenderNote(n);
|
|
} else if (cmd == IDM_DELETE) {
|
|
PostMessageW(g_main, WM_KILLNOTE, (WPARAM)(n - g_notes), 0);
|
|
}
|
|
}
|
|
|
|
static BOOL InGrip(Note *n, int x, int y) {
|
|
// the grip square sits beside the first line of text
|
|
HDC dc = CreateCompatibleDC(NULL);
|
|
SelectObject(dc, g_font);
|
|
TEXTMETRICW tm; GetTextMetricsW(dc, &tm);
|
|
DeleteDC(dc);
|
|
int g = GripW(), gy = S(4) + (tm.tmHeight - g) / 2;
|
|
(void)n;
|
|
return x >= 0 && x < g + S(2) && y >= gy - S(2) && y < gy + g + S(2);
|
|
}
|
|
|
|
static LRESULT CALLBACK NoteProc(HWND h, UINT msg, WPARAM wp, LPARAM lp) {
|
|
Note *n = (Note *)GetWindowLongPtrW(h, GWLP_USERDATA);
|
|
switch (msg) {
|
|
case WM_NCCREATE: {
|
|
CREATESTRUCTW *cs = (CREATESTRUCTW *)lp;
|
|
SetWindowLongPtrW(h, GWLP_USERDATA, (LONG_PTR)cs->lpCreateParams);
|
|
break;
|
|
}
|
|
case WM_SETCURSOR: {
|
|
POINT p; GetCursorPos(&p); ScreenToClient(h, &p);
|
|
BOOL move = g_moving || (!g_selecting && (InGrip(n, p.x, p.y) || (GetKeyState(VK_MENU) & 0x8000)));
|
|
SetCursor(LoadCursorW(NULL, move ? IDC_SIZEALL : IDC_IBEAM));
|
|
return TRUE;
|
|
}
|
|
case WM_MOUSEACTIVATE:
|
|
if (n) n->active = TRUE;
|
|
return MA_ACTIVATE;
|
|
case WM_ACTIVATE:
|
|
if (!n) break;
|
|
if (LOWORD(wp) != WA_INACTIVE) {
|
|
n->active = TRUE; n->caretOn = TRUE;
|
|
SetTimer(h, TIMER_BLINK, GetCaretBlinkTime(), NULL);
|
|
RenderNote(n);
|
|
} else {
|
|
n->active = FALSE;
|
|
n->anchor = n->caret;
|
|
KillTimer(h, TIMER_BLINK);
|
|
if (n->len == 0) { PostMessageW(g_main, WM_KILLNOTE, (WPARAM)(n - g_notes), 0); return 0; }
|
|
RenderNote(n);
|
|
PushToDesktop(h);
|
|
MarkDirty();
|
|
}
|
|
return 0;
|
|
case WM_WINDOWPOSCHANGING:
|
|
if (n && !n->active) {
|
|
WINDOWPOS *pos = (WINDOWPOS *)lp;
|
|
if (!(pos->flags & SWP_NOZORDER)) {
|
|
HWND host = FindDesktopHost();
|
|
if (host) {
|
|
HWND above = GetWindow(host, GW_HWNDPREV);
|
|
if (above == h) pos->flags |= SWP_NOZORDER;
|
|
else pos->hwndInsertAfter = above ? above : HWND_TOP;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case WM_TIMER:
|
|
if (wp == TIMER_BLINK && n) { n->caretOn = !n->caretOn; RenderNote(n); }
|
|
return 0;
|
|
case WM_LBUTTONDOWN: {
|
|
if (!n) return 0;
|
|
int cx = (short)LOWORD(lp), cy = (short)HIWORD(lp);
|
|
if (InGrip(n, cx, cy) || (GetKeyState(VK_MENU) & 0x8000)) {
|
|
// move the note
|
|
RECT r; GetWindowRect(h, &r);
|
|
g_dragOrig.x = r.left; g_dragOrig.y = r.top;
|
|
GetCursorPos(&g_dragStart);
|
|
g_moving = TRUE;
|
|
SetCursor(LoadCursorW(NULL, IDC_SIZEALL));
|
|
} else {
|
|
// place the cursor / start selecting text
|
|
n->caret = CaretFromPoint(n, cx, cy);
|
|
if (!(wp & MK_SHIFT)) n->anchor = n->caret;
|
|
n->caretOn = TRUE;
|
|
g_selecting = TRUE;
|
|
RenderNote(n);
|
|
}
|
|
SetCapture(h);
|
|
return 0;
|
|
}
|
|
case WM_MOUSEMOVE:
|
|
if (n && !n->hover) {
|
|
n->hover = TRUE;
|
|
TRACKMOUSEEVENT t = { sizeof(t), TME_LEAVE, h, 0 };
|
|
TrackMouseEvent(&t);
|
|
RenderNote(n);
|
|
}
|
|
if (GetCapture() == h && n) {
|
|
if (g_moving) {
|
|
POINT p; GetCursorPos(&p);
|
|
SetWindowPos(h, NULL, g_dragOrig.x + p.x - g_dragStart.x, g_dragOrig.y + p.y - g_dragStart.y,
|
|
0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
} else if (g_selecting) {
|
|
int c = CaretFromPoint(n, (short)LOWORD(lp), (short)HIWORD(lp));
|
|
if (c != n->caret) { n->caret = c; RenderNote(n); }
|
|
}
|
|
}
|
|
return 0;
|
|
case WM_LBUTTONUP: {
|
|
BOOL wasMove = g_moving;
|
|
g_moving = FALSE; g_selecting = FALSE;
|
|
if (GetCapture() == h) ReleaseCapture();
|
|
if (wasMove) MarkDirty();
|
|
return 0;
|
|
}
|
|
case WM_CAPTURECHANGED:
|
|
g_moving = FALSE; g_selecting = FALSE;
|
|
return 0;
|
|
case WM_LBUTTONDBLCLK:
|
|
if (n && !InGrip(n, (short)LOWORD(lp), (short)HIWORD(lp))) {
|
|
SelectWordAt(n, CaretFromPoint(n, (short)LOWORD(lp), (short)HIWORD(lp)));
|
|
RenderNote(n);
|
|
}
|
|
return 0;
|
|
case WM_MOUSELEAVE:
|
|
if (n && n->hover) { n->hover = FALSE; RenderNote(n); }
|
|
return 0;
|
|
case WM_RBUTTONUP:
|
|
if (n) ShowNoteMenu(h, n);
|
|
return 0;
|
|
case WM_CHAR: {
|
|
if (!n) return 0;
|
|
WCHAR c = (WCHAR)wp;
|
|
if (c == 8) { // backspace
|
|
if (HasSel(n)) DeleteSel(n);
|
|
else if (n->caret > 0) DeleteRange(n, n->caret - 1, n->caret);
|
|
} else if (c == 13) {
|
|
WCHAR nl = L'\n'; InsertText(n, &nl, 1);
|
|
} else if (c == 27) {
|
|
SetForegroundWindow(GetShellWindow());
|
|
return 0;
|
|
} else if (c == 9 || (c >= 32 && c != 127)) {
|
|
InsertText(n, &c, 1);
|
|
} else {
|
|
return 0;
|
|
}
|
|
n->caretOn = TRUE;
|
|
RenderNote(n);
|
|
MarkDirty();
|
|
return 0;
|
|
}
|
|
case WM_KEYDOWN: {
|
|
if (!n) return 0;
|
|
BOOL ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
|
BOOL shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
|
BOOL changed = FALSE, moved = FALSE;
|
|
int a, b; SelRange(n, &a, &b);
|
|
switch (wp) {
|
|
case VK_LEFT:
|
|
if (HasSel(n) && !shift) n->caret = a;
|
|
else if (n->caret > 0) n->caret--;
|
|
moved = TRUE; break;
|
|
case VK_RIGHT:
|
|
if (HasSel(n) && !shift) n->caret = b;
|
|
else if (n->caret < n->len) n->caret++;
|
|
moved = TRUE; break;
|
|
case VK_UP: MoveCaretVertical(n, -1); moved = TRUE; break;
|
|
case VK_DOWN: MoveCaretVertical(n, +1); moved = TRUE; break;
|
|
case VK_HOME:
|
|
if (ctrl) n->caret = 0;
|
|
else while (n->caret > 0 && n->text[n->caret - 1] != L'\n') n->caret--;
|
|
moved = TRUE; break;
|
|
case VK_END:
|
|
if (ctrl) n->caret = n->len;
|
|
else while (n->caret < n->len && n->text[n->caret] != L'\n') n->caret++;
|
|
moved = TRUE; break;
|
|
case VK_DELETE:
|
|
if (HasSel(n)) DeleteSel(n);
|
|
else if (n->caret < n->len) DeleteRange(n, n->caret, n->caret + 1);
|
|
changed = TRUE; break;
|
|
case 'A': if (!ctrl) return 0; n->anchor = 0; n->caret = n->len; break;
|
|
case 'C': if (ctrl) CopyNote(h, n, FALSE); return 0;
|
|
case 'X':
|
|
if (!ctrl) return 0;
|
|
CopyNote(h, n, FALSE);
|
|
if (HasSel(n)) DeleteSel(n); else { n->anchor = 0; n->caret = n->len; DeleteSel(n); }
|
|
changed = TRUE; break;
|
|
case 'V': if (!ctrl) return 0; PasteFromClipboard(h, n); changed = TRUE; break;
|
|
default: return 0;
|
|
}
|
|
if (moved && !shift) n->anchor = n->caret;
|
|
n->caretOn = TRUE;
|
|
RenderNote(n);
|
|
if (changed) MarkDirty();
|
|
return 0;
|
|
}
|
|
}
|
|
return DefWindowProcW(h, msg, wp, lp);
|
|
}
|
|
|
|
/* ---------------- desktop double-click detection ---------------- */
|
|
|
|
static BOOL IsDesktopSurface(HWND w) {
|
|
if (!w) return FALSE;
|
|
WCHAR cls[64], pcls[64] = L"";
|
|
GetClassNameW(w, cls, 64);
|
|
HWND p = GetParent(w);
|
|
if (p) GetClassNameW(p, pcls, 64);
|
|
if (!wcscmp(cls, L"SysListView32") && !wcscmp(pcls, L"SHELLDLL_DefView")) return TRUE;
|
|
if (!wcscmp(cls, L"SHELLDLL_DefView") || !wcscmp(cls, L"Progman") || !wcscmp(cls, L"WorkerW")) return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
// TRUE if a desktop icon is under the point (so a double-click should open it, not start a note)
|
|
static BOOL DesktopIconAt(POINT pt) {
|
|
IAccessible *acc = NULL;
|
|
VARIANT child; VariantInit(&child);
|
|
if (FAILED(AccessibleObjectFromPoint(pt, &acc, &child)) || !acc) return FALSE;
|
|
BOOL icon = FALSE;
|
|
VARIANT role; VariantInit(&role);
|
|
if (SUCCEEDED(IAccessible_get_accRole(acc, child, &role)) && role.vt == VT_I4) {
|
|
long r = role.lVal;
|
|
icon = (r == ROLE_SYSTEM_LISTITEM || r == ROLE_SYSTEM_STATICTEXT || r == ROLE_SYSTEM_GRAPHIC ||
|
|
r == ROLE_SYSTEM_TEXT || r == ROLE_SYSTEM_PUSHBUTTON);
|
|
}
|
|
VariantClear(&role);
|
|
VariantClear(&child);
|
|
IAccessible_Release(acc);
|
|
return icon;
|
|
}
|
|
|
|
int QN_Mouse(WPARAM wp, MSLLHOOKSTRUCT *m) {
|
|
if (!g_enabled || wp != WM_LBUTTONDOWN) return 0;
|
|
HWND w = WindowFromPoint(m->pt);
|
|
if (IsDesktopSurface(w)) {
|
|
DWORD dt = m->time - g_lastDownTime;
|
|
if (g_lastDownTime && dt <= GetDoubleClickTime() &&
|
|
abs(m->pt.x - g_lastDownPt.x) <= GetSystemMetrics(SM_CXDOUBLECLK) &&
|
|
abs(m->pt.y - g_lastDownPt.y) <= GetSystemMetrics(SM_CYDOUBLECLK)) {
|
|
PostMessageW(g_main, WM_DESKDBL, 0, MAKELPARAM((WORD)(SHORT)m->pt.x, (WORD)(SHORT)m->pt.y));
|
|
g_lastDownTime = 0;
|
|
} else {
|
|
g_lastDownTime = m->time ? m->time : 1;
|
|
g_lastDownPt = m->pt;
|
|
}
|
|
} else {
|
|
g_lastDownTime = 0;
|
|
}
|
|
return 0; // never swallows: desktop keeps working normally
|
|
}
|
|
|
|
/* ---------------- tray + main ---------------- */
|
|
|
|
static void Balloon(const WCHAR *title, const WCHAR *text) { DT_Balloon(title, text); }
|
|
|
|
static void AddTrayIcon(void) { }
|
|
|
|
static void ShowHelp(void) {
|
|
MessageBoxW(NULL,
|
|
L"Write on your desktop:\n"
|
|
L" Double-click any empty spot on the wallpaper and start typing.\n"
|
|
L" Click anywhere else when you're done. The text stays on the desktop,\n"
|
|
L" behind your windows, just like desktop icons.\n\n"
|
|
L"Copy text out of a note:\n"
|
|
L" Drag across the text to select it, then press Ctrl+C.\n"
|
|
L" Double-click a word to select it. Ctrl+A selects everything.\n"
|
|
L" Or right-click the note and choose Copy all text.\n\n"
|
|
L"Move a note:\n"
|
|
L" Drag the small gray square at its top left corner (it shows when\n"
|
|
L" you point at the note), or hold Alt and drag anywhere on it.\n\n"
|
|
L"Right-click a note to change its color or delete it.\n"
|
|
L"Enter starts a new line. Esc finishes editing.\n"
|
|
L"A note you empty out disappears on its own.\n"
|
|
L"Notes save automatically and come back after a restart.",
|
|
L"How to use QuickNote", MB_OK | MB_ICONINFORMATION);
|
|
}
|
|
|
|
static void ShowTrayMenu(void) {
|
|
HMENU m = CreatePopupMenu();
|
|
AppendMenuW(m, MF_STRING | MF_GRAYED, 0, L"QuickNote");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, NULL);
|
|
AppendMenuW(m, MF_STRING, IDM_NEW, L"New note");
|
|
AppendMenuW(m, MF_STRING | (g_hidden ? MF_CHECKED : 0), IDM_HIDE, L"Hide all notes");
|
|
AppendMenuW(m, MF_STRING | (StartupEnabled() ? MF_CHECKED : 0), IDM_STARTUP, L"Start with Windows");
|
|
AppendMenuW(m, MF_STRING, IDM_HELP, L"How to use");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, NULL);
|
|
AppendMenuW(m, MF_STRING, IDM_EXIT, L"Exit");
|
|
POINT p; GetCursorPos(&p);
|
|
SetForegroundWindow(g_main);
|
|
TrackPopupMenu(m, TPM_RIGHTBUTTON | TPM_BOTTOMALIGN, p.x, p.y, 0, g_main, NULL);
|
|
PostMessageW(g_main, WM_NULL, 0, 0);
|
|
DestroyMenu(m);
|
|
}
|
|
|
|
static void NewNoteAt(int x, int y) {
|
|
if (g_hidden) SetAllHidden(FALSE);
|
|
HDC dc = CreateCompatibleDC(NULL);
|
|
SelectObject(dc, g_font);
|
|
TEXTMETRICW tm; GetTextMetricsW(dc, &tm);
|
|
DeleteDC(dc);
|
|
// place it so the cursor lands right where you clicked
|
|
CreateNote(x - GripW() - S(3), y - S(4) - tm.tmHeight / 2, L"", 0, COLORS[g_lastColor], TRUE);
|
|
}
|
|
|
|
static LRESULT CALLBACK MainProc(HWND h, UINT msg, WPARAM wp, LPARAM lp) {
|
|
if (msg == g_taskbarCreated && g_taskbarCreated) { AddTrayIcon(); return 0; }
|
|
switch (msg) {
|
|
case WM_TRAY:
|
|
if (LOWORD(lp) == WM_RBUTTONUP || LOWORD(lp) == WM_LBUTTONUP) ShowTrayMenu();
|
|
return 0;
|
|
case WM_DESKDBL: {
|
|
POINT pt = { (short)LOWORD(lp), (short)HIWORD(lp) };
|
|
if (!DesktopIconAt(pt)) NewNoteAt(pt.x, pt.y);
|
|
return 0;
|
|
}
|
|
case WM_KILLNOTE:
|
|
if (wp < MAX_NOTES) DeleteNote(&g_notes[wp]);
|
|
return 0;
|
|
case WM_TIMER:
|
|
if (wp == TIMER_SAVE) { KillTimer(h, TIMER_SAVE); if (g_dirty) SaveNotes(); }
|
|
else if (wp == TIMER_CHECK && !g_hidden) {
|
|
// keep notes visible after Win+D and sitting just above the wallpaper
|
|
for (int i = 0; i < MAX_NOTES; i++) {
|
|
Note *n = &g_notes[i];
|
|
if (!n->used) continue;
|
|
if (IsIconic(n->hwnd) || !IsWindowVisible(n->hwnd)) ShowWindow(n->hwnd, SW_SHOWNA);
|
|
}
|
|
HWND fg = GetForegroundWindow();
|
|
if (fg != g_lastFg) {
|
|
g_lastFg = fg;
|
|
if (IsDesktopClass(fg))
|
|
for (int i = 0; i < MAX_NOTES; i++)
|
|
if (g_notes[i].used && !g_notes[i].active) PushToDesktop(g_notes[i].hwnd);
|
|
}
|
|
}
|
|
return 0;
|
|
case WM_COMMAND:
|
|
switch (LOWORD(wp)) {
|
|
case IDM_NEW: {
|
|
RECT wa; SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0);
|
|
NewNoteAt((wa.left + wa.right) / 2 - S(60), (wa.top + wa.bottom) / 2);
|
|
break;
|
|
}
|
|
case IDM_HIDE: SetAllHidden(!g_hidden); break;
|
|
case IDM_STARTUP: SetStartup(!StartupEnabled()); break;
|
|
case IDM_HELP: ShowHelp(); break;
|
|
case IDM_EXIT: DestroyWindow(h); break;
|
|
}
|
|
return 0;
|
|
case WM_QUERYENDSESSION:
|
|
SaveNotes();
|
|
return TRUE;
|
|
case WM_DESTROY:
|
|
SaveNotes();
|
|
if (g_hook) UnhookWindowsHookEx(g_hook);
|
|
Shell_NotifyIconW(NIM_DELETE, &g_nid);
|
|
|
|
return 0;
|
|
}
|
|
return DefWindowProcW(h, msg, wp, lp);
|
|
}
|
|
|
|
/* ---------------- DeskTools module interface ---------------- */
|
|
|
|
void QN_SetEnabled(BOOL on) {
|
|
g_enabled = on;
|
|
SetAllHidden(!on);
|
|
}
|
|
BOOL QN_IsEnabled(void) { return g_enabled; }
|
|
|
|
void QN_Init(HINSTANCE inst, BOOL enabled) {
|
|
g_inst = inst;
|
|
HDC sdc = GetDC(NULL); g_dpi = GetDeviceCaps(sdc, LOGPIXELSY); ReleaseDC(NULL, sdc);
|
|
if (g_dpi < 96) g_dpi = 96;
|
|
g_font = CreateFontW(-MulDiv(FONT_PT, g_dpi, 72), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
|
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
|
|
DEFAULT_PITCH, L"Segoe UI");
|
|
WCHAR dir[MAX_PATH];
|
|
if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, dir))) {
|
|
wcscat(dir, L"\\QuickNote");
|
|
CreateDirectoryW(dir, NULL);
|
|
swprintf(g_file, MAX_PATH, L"%ls\\notes.txt", dir);
|
|
} else {
|
|
GetTempPathW(MAX_PATH, dir);
|
|
swprintf(g_file, MAX_PATH, L"%lsQuickNote_notes.txt", dir);
|
|
}
|
|
DWORD lc = RegGetFlag(L"LastColor");
|
|
if (lc < NCOLORS) g_lastColor = (int)lc;
|
|
|
|
WNDCLASSEXW wc; ZeroMemory(&wc, sizeof(wc));
|
|
wc.cbSize = sizeof(wc); wc.lpfnWndProc = MainProc; wc.hInstance = inst; wc.lpszClassName = L"QuickNoteMain";
|
|
RegisterClassExW(&wc);
|
|
WNDCLASSEXW nc; ZeroMemory(&nc, sizeof(nc));
|
|
nc.cbSize = sizeof(nc); nc.style = CS_DBLCLKS; nc.lpfnWndProc = NoteProc; nc.hInstance = inst;
|
|
nc.hCursor = LoadCursorW(NULL, IDC_IBEAM); nc.lpszClassName = L"QuickNoteText";
|
|
RegisterClassExW(&nc);
|
|
|
|
g_main = CreateWindowExW(0, L"QuickNoteMain", APP_NAME, 0, 0, 0, 0, 0, NULL, NULL, inst, NULL);
|
|
g_hidden = !enabled;
|
|
LoadNotes();
|
|
SetTimer(g_main, TIMER_CHECK, 1000, NULL);
|
|
g_enabled = enabled;
|
|
}
|
|
|
|
void QN_Shutdown(void) {
|
|
if (g_main) { DestroyWindow(g_main); g_main = NULL; } // saves the notes
|
|
for (int i = 0; i < MAX_NOTES; i++) if (g_notes[i].used && g_notes[i].hwnd) DestroyWindow(g_notes[i].hwnd);
|
|
if (g_font) { DeleteObject(g_font); g_font = NULL; }
|
|
}
|
|
|
|
void QN_AppendMenu(HMENU parent, UINT base) {
|
|
HMENU m = CreatePopupMenu();
|
|
UINT dis = g_enabled ? 0 : MF_GRAYED;
|
|
AppendMenuW(m, MF_STRING | (g_enabled ? MF_CHECKED : 0), base + DT_ID_ENABLE, L"Turned on");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, NULL);
|
|
AppendMenuW(m, MF_STRING | dis, base + IDM_NEW, L"New note");
|
|
AppendMenuW(m, MF_STRING | dis | (g_hidden && g_enabled ? MF_CHECKED : 0), base + IDM_HIDE, L"Hide all notes");
|
|
AppendMenuW(parent, MF_POPUP, (UINT_PTR)m, L"QuickNote");
|
|
}
|
|
|
|
void QN_Command(UINT local) {
|
|
if (local == DT_ID_ENABLE) { QN_SetEnabled(!g_enabled); return; }
|
|
SendMessageW(g_main, WM_COMMAND, local, 0);
|
|
}
|