Files
2026-05-14 19:58:13 -07:00

739 lines
18 KiB
C++

/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
IceTech Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
IceTech Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the IceTech Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the IceTech Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing Justin Marshall, justinmarshall20@gmail.com
===========================================================================
*/
#include "precompiled.h"
#pragma hdrstop
#include "QE3.H"
#include "TabsDlg.h"
/*
========================
Tabs dialog top caption
Draws a non-layout-changing caption at the top of the tab dialog. This is an
overlay child window, so it does not move the tab control, docked children, or
floating/docking logic. The caption text follows the currently selected tab.
========================
*/
static const char* TABSDLG_TOP_CAPTION_CLASS = "QER_TabsDlgTopCaption";
static const char* TABSDLG_TOP_CAPTION_STATE_PROP = "QER_TabsDlgTopCaptionState";
static const char* TABSDLG_TAB_NOTIFY_STATE_PROP = "QER_TabsDlgTabNotifyState";
static const int TABSDLG_TOP_CAPTION_HEIGHT = 28;
static const int TABSDLG_TOP_CAPTION_CHILD_ID = 64010;
struct tabsDlgTopCaptionState_t {
WNDPROC oldProc;
HWND captionWnd;
};
struct tabsDlgTabNotifyState_t {
WNDPROC oldProc;
HWND parentWnd;
};
static LRESULT CALLBACK TabsDlg_TopCaptionProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK TabsDlg_ParentProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK TabsDlg_TabNotifyProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
static WNDPROC TabsDlg_GetWindowProc(HWND hWnd) {
#ifdef _WIN64
return (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC);
#else
return (WNDPROC)GetWindowLong(hWnd, GWL_WNDPROC);
#endif
}
static void TabsDlg_SetWindowProc(HWND hWnd, WNDPROC proc) {
#ifdef _WIN64
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)proc);
#else
SetWindowLong(hWnd, GWL_WNDPROC, (LONG)proc);
#endif
}
static COLORREF TabsDlg_LerpColor(COLORREF a, COLORREF b, float t) {
const int ar = GetRValue(a);
const int ag = GetGValue(a);
const int ab = GetBValue(a);
const int br = GetRValue(b);
const int bg = GetGValue(b);
const int bb = GetBValue(b);
const int r = ar + (int)((br - ar) * t);
const int g = ag + (int)((bg - ag) * t);
const int bl = ab + (int)((bb - ab) * t);
return RGB(r, g, bl);
}
static void TabsDlg_FillHorizontalGradient(HDC hDC, const RECT& rect, COLORREF leftColor, COLORREF rightColor) {
const int width = rect.right - rect.left;
if (width <= 0) {
return;
}
for (int x = 0; x < width; x++) {
const float t = (width > 1) ? ((float)x / (float)(width - 1)) : 0.0f;
const COLORREF color = TabsDlg_LerpColor(leftColor, rightColor, t);
HPEN pen = CreatePen(PS_SOLID, 1, color);
HPEN oldPen = (HPEN)SelectObject(hDC, pen);
MoveToEx(hDC, rect.left + x, rect.top, NULL);
LineTo(hDC, rect.left + x, rect.bottom);
SelectObject(hDC, oldPen);
DeleteObject(pen);
}
}
static tabsDlgTopCaptionState_t* TabsDlg_GetTopCaptionState(HWND hWnd) {
return (tabsDlgTopCaptionState_t*)GetProp(hWnd, TABSDLG_TOP_CAPTION_STATE_PROP);
}
static tabsDlgTabNotifyState_t* TabsDlg_GetTabNotifyState(HWND hTabs) {
return (tabsDlgTabNotifyState_t*)GetProp(hTabs, TABSDLG_TAB_NOTIFY_STATE_PROP);
}
static HWND TabsDlg_GetTabControl(HWND hWnd) {
HWND hTabs = GetDlgItem(hWnd, IDC_TAB_INSPECTOR);
return IsWindow(hTabs) ? hTabs : NULL;
}
static CString TabsDlg_GetCurrentTopCaption(HWND hWnd) {
HWND hTabs = TabsDlg_GetTabControl(hWnd);
if (!hTabs) {
return "Viewport";
}
int curSel = TabCtrl_GetCurSel(hTabs);
if (curSel < 0 && TabCtrl_GetItemCount(hTabs) > 0) {
curSel = 0;
}
if (curSel >= 0) {
char text[256];
text[0] = '\0';
TCITEM item;
memset(&item, 0, sizeof(item));
item.mask = TCIF_TEXT;
item.pszText = text;
item.cchTextMax = sizeof(text);
if (TabCtrl_GetItem(hTabs, curSel, &item) && text[0] != '\0') {
return text;
}
}
return "Viewport";
}
static void TabsDlg_RegisterTopCaptionClass() {
static bool registered = false;
if (registered) {
return;
}
WNDCLASS wc;
memset(&wc, 0, sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = TabsDlg_TopCaptionProc;
wc.hInstance = AfxGetInstanceHandle();
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszClassName = TABSDLG_TOP_CAPTION_CLASS;
AfxRegisterClass(&wc);
registered = true;
}
static void TabsDlg_DrawTopCaption(HWND hCaption, HDC hDC) {
HWND hParent = GetParent(hCaption);
RECT rect;
GetClientRect(hCaption, &rect);
TabsDlg_FillHorizontalGradient(
hDC,
rect,
RGB(24, 82, 150),
RGB(92, 96, 104)
);
RECT dividerRect = rect;
dividerRect.top = rect.bottom - 1;
dividerRect.bottom = rect.bottom;
FillRect(hDC, &dividerRect, (HBRUSH)(COLOR_3DSHADOW + 1));
HFONT font = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
HFONT oldFont = NULL;
if (font) {
oldFont = (HFONT)SelectObject(hDC, font);
}
SetBkMode(hDC, TRANSPARENT);
SetTextColor(hDC, RGB(235, 242, 255));
RECT textRect = rect;
textRect.left += 8;
textRect.right -= 8;
CString caption = TabsDlg_GetCurrentTopCaption(hParent);
DrawText(
hDC,
caption,
-1,
&textRect,
DT_SINGLELINE | DT_LEFT | DT_VCENTER | DT_END_ELLIPSIS
);
if (oldFont) {
SelectObject(hDC, oldFont);
}
}
static LRESULT CALLBACK TabsDlg_TopCaptionProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_ERASEBKGND:
return 1;
case WM_SETCURSOR:
SetCursor(LoadCursor(NULL, IDC_ARROW));
return TRUE;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hDC = BeginPaint(hWnd, &ps);
TabsDlg_DrawTopCaption(hWnd, hDC);
EndPaint(hWnd, &ps);
return 0;
}
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
static void TabsDlg_LayoutTopCaption(HWND hWnd) {
tabsDlgTopCaptionState_t* state = TabsDlg_GetTopCaptionState(hWnd);
if (!state || !IsWindow(state->captionWnd)) {
return;
}
RECT clientRect;
GetClientRect(hWnd, &clientRect);
int width = clientRect.right - clientRect.left;
if (width < 1) {
width = 1;
}
SetWindowPos(
state->captionWnd,
HWND_TOP,
0,
0,
width,
TABSDLG_TOP_CAPTION_HEIGHT,
SWP_NOACTIVATE | SWP_SHOWWINDOW
);
}
static void TabsDlg_RefreshTopCaption(HWND hWnd) {
tabsDlgTopCaptionState_t* state = TabsDlg_GetTopCaptionState(hWnd);
if (!state || !IsWindow(state->captionWnd)) {
return;
}
TabsDlg_LayoutTopCaption(hWnd);
InvalidateRect(state->captionWnd, NULL, FALSE);
UpdateWindow(state->captionWnd);
}
static void TabsDlg_UninstallTabNotify(HWND hTabs) {
tabsDlgTabNotifyState_t* state = TabsDlg_GetTabNotifyState(hTabs);
if (!state) {
return;
}
TabsDlg_SetWindowProc(hTabs, state->oldProc);
RemoveProp(hTabs, TABSDLG_TAB_NOTIFY_STATE_PROP);
delete state;
}
static LRESULT CALLBACK TabsDlg_TabNotifyProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
tabsDlgTabNotifyState_t* state = TabsDlg_GetTabNotifyState(hWnd);
WNDPROC oldProc = state ? state->oldProc : NULL;
HWND parentWnd = state ? state->parentWnd : GetParent(hWnd);
switch (uMsg) {
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_KEYDOWN:
case WM_KEYUP:
case TCM_SETCURSEL:
case TCM_SETCURFOCUS:
{
LRESULT result = oldProc ? CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam) : DefWindowProc(hWnd, uMsg, wParam, lParam);
TabsDlg_RefreshTopCaption(parentWnd);
return result;
}
case WM_NCDESTROY:
{
TabsDlg_UninstallTabNotify(hWnd);
return oldProc ? CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam) : DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return oldProc ? CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam) : DefWindowProc(hWnd, uMsg, wParam, lParam);
}
static void TabsDlg_InstallTabNotify(HWND hWnd) {
HWND hTabs = TabsDlg_GetTabControl(hWnd);
if (!hTabs || TabsDlg_GetTabNotifyState(hTabs)) {
return;
}
tabsDlgTabNotifyState_t* state = new tabsDlgTabNotifyState_t;
state->oldProc = TabsDlg_GetWindowProc(hTabs);
state->parentWnd = hWnd;
SetProp(hTabs, TABSDLG_TAB_NOTIFY_STATE_PROP, (HANDLE)state);
TabsDlg_SetWindowProc(hTabs, TabsDlg_TabNotifyProc);
}
static LRESULT CALLBACK TabsDlg_ParentProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
tabsDlgTopCaptionState_t* state = TabsDlg_GetTopCaptionState(hWnd);
WNDPROC oldProc = state ? state->oldProc : NULL;
switch (uMsg) {
case WM_SIZE:
case WM_WINDOWPOSCHANGED:
{
LRESULT result = oldProc ? CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam) : DefWindowProc(hWnd, uMsg, wParam, lParam);
TabsDlg_LayoutTopCaption(hWnd);
return result;
}
case WM_NCDESTROY:
{
HWND hTabs = TabsDlg_GetTabControl(hWnd);
if (hTabs) {
TabsDlg_UninstallTabNotify(hTabs);
}
HWND hCaption = state ? state->captionWnd : NULL;
if (IsWindow(hCaption)) {
DestroyWindow(hCaption);
}
if (oldProc) {
TabsDlg_SetWindowProc(hWnd, oldProc);
}
RemoveProp(hWnd, TABSDLG_TOP_CAPTION_STATE_PROP);
if (state) {
delete state;
}
return oldProc ? CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam) : DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return oldProc ? CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam) : DefWindowProc(hWnd, uMsg, wParam, lParam);
}
static void TabsDlg_InstallTopCaption(HWND hWnd) {
if (!IsWindow(hWnd)) {
return;
}
tabsDlgTopCaptionState_t* state = TabsDlg_GetTopCaptionState(hWnd);
if (!state) {
TabsDlg_RegisterTopCaptionClass();
state = new tabsDlgTopCaptionState_t;
memset(state, 0, sizeof(*state));
state->oldProc = TabsDlg_GetWindowProc(hWnd);
SetProp(hWnd, TABSDLG_TOP_CAPTION_STATE_PROP, (HANDLE)state);
TabsDlg_SetWindowProc(hWnd, TabsDlg_ParentProc);
}
if (!IsWindow(state->captionWnd)) {
state->captionWnd = CreateWindowEx(
0,
TABSDLG_TOP_CAPTION_CLASS,
"",
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
0,
0,
1,
TABSDLG_TOP_CAPTION_HEIGHT,
hWnd,
(HMENU)TABSDLG_TOP_CAPTION_CHILD_ID,
AfxGetInstanceHandle(),
NULL);
}
TabsDlg_InstallTabNotify(hWnd);
TabsDlg_LayoutTopCaption(hWnd);
TabsDlg_RefreshTopCaption(hWnd);
}
// CTabsDlg dialog
//IMPLEMENT_DYNAMIC ( CTabsDlg , CDialog )
CTabsDlg::CTabsDlg(UINT ID, CWnd* pParent /*=NULL*/)
: CDialog(ID, pParent)
{
m_DragTabActive = false;
}
BEGIN_MESSAGE_MAP(CTabsDlg, CDialog)
//}}AFX_MSG_MAP
// ON_NOTIFY(TCN_SELCHANGE, IDC_TAB1, OnTcnSelchangeTab1)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
ON_WM_DESTROY()
END_MESSAGE_MAP()
void CTabsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_TAB_INSPECTOR, m_Tabs);
}
// CTabsDlg message handlers
BOOL CTabsDlg::OnInitDialog()
{
CDialog::OnInitDialog();
TabsDlg_InstallTopCaption(GetSafeHwnd());
return TRUE; // return TRUE unless you set the focus to a control
}
void CTabsDlg::OnTcnSelchange(NMHDR* pNMHDR, LRESULT* pResult)
{
int ID = TabCtrl_GetCurSel(pNMHDR->hwndFrom);
if (ID >= 0)
{
TCITEM item;
item.mask = TCIF_PARAM;
ShowAllWindows(FALSE);
TabCtrl_GetItem(m_Tabs.GetSafeHwnd(), ID, &item);
DockedWindowInfo* info = (DockedWindowInfo*)item.lParam;
ASSERT(info);
info->m_TabControlIndex = ID;
info->m_Window->ShowWindow(TRUE);
TabsDlg_RefreshTopCaption(GetSafeHwnd());
}
}
void CTabsDlg::DockWindow(int ID, bool dock)
{
DockedWindowInfo* info = NULL;
m_Windows.Lookup((WORD)ID, (void*&)info);
ASSERT(info);
ASSERT(m_Tabs.GetSafeHwnd());
ShowAllWindows(FALSE);
if (!dock)
{
//make a containing window and assign the dialog to it
CRect rect;
CString classname = AfxRegisterWndClass(CS_DBLCLKS, 0, 0, 0);
info->m_State = DockedWindowInfo::FLOATING;
info->m_Window->GetWindowRect(rect);
info->m_Container.CreateEx(WS_EX_TOOLWINDOW, classname, info->m_Title, WS_THICKFRAME | WS_SYSMENU | WS_POPUP | WS_CAPTION, rect, this, 0);
info->m_Window->SetParent(&info->m_Container);
info->m_Window->ShowWindow(TRUE);
info->m_Container.SetDockManager(this);
info->m_Container.ShowWindow(TRUE);
info->m_Container.SetDialog(info->m_Window, info->m_ID);
if (info->m_TabControlIndex >= 0)
{
m_Tabs.DeleteItem(info->m_TabControlIndex);
}
if (m_Tabs.GetItemCount() > 0)
{
m_Tabs.SetCurFocus(0);
}
CString placementName = info->m_Title + "Placement";
LoadWindowPlacement(info->m_Container, placementName);
}
else
{
info->m_State = DockedWindowInfo::DOCKED;
info->m_TabControlIndex = m_Tabs.InsertItem(TCIF_TEXT | TCIF_IMAGE | TCIF_PARAM, 0, info->m_Title, info->m_ImageID, (LPARAM)info);
info->m_Window->SetParent(this);
info->m_Window->ShowWindow(TRUE);
info->m_Container.SetDockManager(NULL); //so it doesn't try to call back and redock this window
info->m_Container.DestroyWindow();
CRect rect;
GetWindowRect(rect);
//stupid hack to get the window reitself properly
rect.DeflateRect(0, 0, 0, 1);
MoveWindow(rect);
rect.InflateRect(0, 0, 0, 1);
MoveWindow(rect);
}
UpdateTabControlIndices();
FocusWindow(ID);
TabsDlg_InstallTopCaption(GetSafeHwnd());
TabsDlg_RefreshTopCaption(GetSafeHwnd());
if (info->m_DockCallback)
{
info->m_DockCallback(dock, info->m_ID, info->m_Window);
}
SaveWindowPlacement();
}
int CTabsDlg::PreTranslateMessage(MSG* msg)
{
if (msg->message == WM_LBUTTONDBLCLK && msg->hwnd == m_Tabs.GetSafeHwnd())
{
HandleUndock();
return TRUE;
}
//steal lbutton clicks for the main dialog too, but let the tabs do their default thing as well
if (msg->message == WM_LBUTTONDOWN && msg->hwnd == m_Tabs.GetSafeHwnd()) {
m_Tabs.SendMessage(msg->message, msg->wParam, msg->lParam);
m_DragTabActive = true;
TabsDlg_RefreshTopCaption(GetSafeHwnd());
}
else if (msg->message == WM_LBUTTONUP && msg->hwnd == m_Tabs.GetSafeHwnd()) {
m_Tabs.SendMessage(msg->message, msg->wParam, msg->lParam);
m_DragTabActive = false;
TabsDlg_RefreshTopCaption(GetSafeHwnd());
}
return CDialog::PreTranslateMessage(msg);
}
bool CTabsDlg::RectWithinDockManager(CRect& rect)
{
CRect tabsRect, intersectionRect;
m_Tabs.GetWindowRect(tabsRect);
intersectionRect.IntersectRect(tabsRect, rect);
return !(intersectionRect.IsRectEmpty());
}
void CTabsDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
CDialog::OnLButtonDown(nFlags, point);
}
void CTabsDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
if (m_DragTabActive && ((abs(point.x - m_DragDownPoint.x) > 50) || (abs(point.y - m_DragDownPoint.y) > 50)))
{
HandleUndock();
m_DragTabActive = false;
}
CDialog::OnLButtonUp(nFlags, point);
}
void CTabsDlg::HandleUndock()
{
TCITEM item;
item.mask = TCIF_PARAM;
int curSel = TabCtrl_GetCurSel(m_Tabs.GetSafeHwnd());
TabCtrl_GetItem(m_Tabs.GetSafeHwnd(), curSel, &item);
DockedWindowInfo* info = (DockedWindowInfo*)item.lParam;
ASSERT(info);
DockWindow(info->m_ID, false);
}
void CTabsDlg::OnMouseMove(UINT nFlags, CPoint point)
{
CDialog::OnMouseMove(nFlags, point);
}
void CTabsDlg::AddDockedWindow(CWnd* wnd, int ID, int imageID, const CString& title, bool dock, pfnOnDockEvent dockCallback)
{
DockedWindowInfo* info = NULL;
m_Windows.Lookup((WORD)ID, (void*&)info);
ASSERT(wnd);
ASSERT(info == NULL);
dock = true;
info = new DockedWindowInfo(wnd, ID, imageID, title, dockCallback);
m_Windows.SetAt((WORD)ID, info);
DockWindow(ID, dock);
UpdateTabControlIndices();
TabsDlg_RefreshTopCaption(GetSafeHwnd());
}
void CTabsDlg::ShowAllWindows(bool show)
{
POSITION pos;
WORD ID;
DockedWindowInfo* info = NULL;
for (pos = m_Windows.GetStartPosition(); pos != NULL; )
{
m_Windows.GetNextAssoc(pos, ID, (void*&)info);
ASSERT(info->m_Window);
if (info->m_State == DockedWindowInfo::DOCKED)
{
info->m_Window->ShowWindow(show);
}
}
}
void CTabsDlg::FocusWindow(int ID)
{
DockedWindowInfo* info = NULL;
m_Windows.Lookup((WORD)ID, (void*&)info);
ASSERT(info);
ASSERT(info->m_Window);
if (info->m_State == DockedWindowInfo::DOCKED)
{
TabCtrl_SetCurFocus(m_Tabs.GetSafeHwnd(), info->m_TabControlIndex);
}
else
{
info->m_Container.SetFocus();
}
TabsDlg_RefreshTopCaption(GetSafeHwnd());
}
void CTabsDlg::UpdateTabControlIndices()
{
TCITEM item;
item.mask = TCIF_PARAM;
DockedWindowInfo* info = NULL;
int itemCount = m_Tabs.GetItemCount();
for (int i = 0; i < itemCount; i++)
{
if (!m_Tabs.GetItem(i, &item))
{
Sys_Error("UpdateTabControlIndices(): GetItem failed!\n");
}
info = (DockedWindowInfo*)item.lParam;
info->m_TabControlIndex = i;
}
}
void CTabsDlg::OnDestroy()
{
TCITEM item;
item.mask = TCIF_PARAM;
DockedWindowInfo* info = NULL;
for (int i = 0; i < m_Tabs.GetItemCount(); i++)
{
m_Tabs.GetItem(i, &item);
info = (DockedWindowInfo*)item.lParam;
ASSERT(info);
delete info;
}
CDialog::OnDestroy();
}
bool CTabsDlg::IsDocked(CWnd* wnd)
{
bool docked = false;
DockedWindowInfo* info = NULL;
CString placementName;
POSITION pos;
WORD wID;
for (pos = m_Windows.GetStartPosition(); pos != NULL; )
{
m_Windows.GetNextAssoc(pos, wID, (void*&)info);
if (info->m_Window == wnd) {
docked = (info->m_State == DockedWindowInfo::DOCKED);
break;
}
}
return docked;
}
void CTabsDlg::SaveWindowPlacement(int ID)
{
DockedWindowInfo* info = NULL;
CString placementName;
POSITION pos;
WORD wID = ID;
for (pos = m_Windows.GetStartPosition(); pos != NULL; )
{
m_Windows.GetNextAssoc(pos, wID, (void*&)info);
if ((info->m_State == DockedWindowInfo::FLOATING) && ((ID == -1) || (ID == info->m_ID))) {
placementName = info->m_Title + "Placement";
::SaveWindowPlacement(info->m_Container.GetSafeHwnd(), placementName);
}
}
}