2019-12-25 00:41:20 +00:00
|
|
|
/****************************************************************************
|
|
|
|
* *
|
|
|
|
* Project64 - A Nintendo 64 emulator. *
|
|
|
|
* http://www.pj64-emu.com/ *
|
|
|
|
* Copyright (C) 2012 Project64. All rights reserved. *
|
|
|
|
* *
|
|
|
|
* License: *
|
|
|
|
* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
|
|
|
|
* *
|
|
|
|
****************************************************************************/
|
|
|
|
|
2019-01-14 09:18:43 +00:00
|
|
|
#include "stdafx.h"
|
|
|
|
#include "DebuggerUI.h"
|
|
|
|
#include "CPULog.h"
|
|
|
|
|
|
|
|
#include "Debugger-CPULogView.h"
|
|
|
|
#include <Project64-core/N64System/Mips/OpCodeName.h>
|
|
|
|
|
|
|
|
CDebugCPULogView* CDebugCPULogView::_this = NULL;
|
|
|
|
HHOOK CDebugCPULogView::hWinMessageHook = NULL;
|
|
|
|
|
|
|
|
CDebugCPULogView::CDebugCPULogView(CDebuggerUI* debugger) :
|
|
|
|
CDebugDialog<CDebugCPULogView>(debugger),
|
2019-01-14 11:11:15 +00:00
|
|
|
m_CPULogCopy(NULL),
|
|
|
|
m_LogStartIndex(0),
|
|
|
|
m_RowHeight(13)
|
2019-01-14 09:18:43 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
CDebugCPULogView::~CDebugCPULogView()
|
|
|
|
{
|
|
|
|
if (m_CPULogCopy != NULL)
|
|
|
|
{
|
|
|
|
delete m_CPULogCopy;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
|
|
|
|
{
|
|
|
|
DlgResize_Init(false, true);
|
|
|
|
DlgToolTip_Init();
|
|
|
|
DlgSavePos_Init(DebuggerUI_CPULogPos);
|
|
|
|
|
|
|
|
m_CPUListView.Attach(GetDlgItem(IDC_CPU_LIST));
|
|
|
|
m_StateInfoEdit.Attach(GetDlgItem(IDC_STATEINFO_EDIT));
|
|
|
|
m_EnabledChk.Attach(GetDlgItem(IDC_CHK_ENABLE));
|
|
|
|
m_BuffSizeEdit.Attach(GetDlgItem(IDC_BUFFSIZE_EDIT));
|
|
|
|
m_Scrollbar.Attach(GetDlgItem(IDC_SCRL_BAR));
|
|
|
|
m_ExportBtn.Attach(GetDlgItem(IDC_BTN_EXPORT));
|
|
|
|
|
|
|
|
m_CPUListView.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);
|
|
|
|
m_CPUListView.ModifyStyle(LVS_OWNERDRAWFIXED, 0, 0);
|
2020-05-12 12:19:05 +00:00
|
|
|
m_CPUListView.AddColumn(L"PC", 0);
|
|
|
|
m_CPUListView.AddColumn(L"Command", 1);
|
|
|
|
m_CPUListView.AddColumn(L"Parameters", 2);
|
2019-01-14 09:18:43 +00:00
|
|
|
m_CPUListView.SetColumnWidth(0, 65);
|
|
|
|
m_CPUListView.SetColumnWidth(1, 60);
|
|
|
|
m_CPUListView.SetColumnWidth(2, 120);
|
|
|
|
|
|
|
|
bool bLoggingEnabled = g_Settings->LoadBool(Debugger_CPULoggingEnabled);
|
|
|
|
uint32_t bufferSize = g_Settings->LoadDword(Debugger_CPULogBufferSize);
|
|
|
|
|
|
|
|
m_EnabledChk.SetCheck(bLoggingEnabled);
|
|
|
|
|
|
|
|
m_BuffSizeEdit.SetDisplayType(CEditNumber32::DisplayDec);
|
|
|
|
m_BuffSizeEdit.SetValue(bufferSize);
|
|
|
|
m_BuffSizeEdit.EnableWindow(!bLoggingEnabled);
|
|
|
|
|
|
|
|
RefreshList(true);
|
|
|
|
|
|
|
|
m_ExportBtn.EnableWindow(false);
|
|
|
|
|
|
|
|
if (!bLoggingEnabled && m_CPULogCopy != NULL)
|
|
|
|
{
|
|
|
|
m_ExportBtn.EnableWindow(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
_this = this;
|
|
|
|
DWORD dwThreadID = ::GetCurrentThreadId();
|
|
|
|
hWinMessageHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)HookProc, NULL, dwThreadID);
|
|
|
|
|
|
|
|
LoadWindowPos();
|
|
|
|
WindowCreated();
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnDestroy(void)
|
|
|
|
{
|
2019-12-25 00:41:20 +00:00
|
|
|
UnhookWindowsHookEx(hWinMessageHook);
|
|
|
|
|
2019-01-14 09:18:43 +00:00
|
|
|
m_CPUListView.Detach();
|
|
|
|
m_StateInfoEdit.Detach();
|
|
|
|
m_EnabledChk.Detach();
|
|
|
|
m_BuffSizeEdit.Detach();
|
|
|
|
m_Scrollbar.Detach();
|
|
|
|
m_ExportBtn.Detach();
|
|
|
|
|
|
|
|
if (m_CPULogCopy != NULL)
|
|
|
|
{
|
|
|
|
delete m_CPULogCopy;
|
|
|
|
m_CPULogCopy = NULL;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CALLBACK CDebugCPULogView::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
|
|
{
|
|
|
|
MSG *pMsg = (MSG*)lParam;
|
|
|
|
|
|
|
|
switch (pMsg->message)
|
|
|
|
{
|
|
|
|
case WM_MOUSEWHEEL:
|
|
|
|
_this->InterceptMouseWheel(pMsg->wParam, pMsg->lParam);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nCode < 0)
|
|
|
|
{
|
|
|
|
return CallNextHookEx(hWinMessageHook, nCode, wParam, lParam);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnClicked(WORD /*wNotifyCode*/, WORD wID, HWND, BOOL& /*bHandled*/)
|
|
|
|
{
|
|
|
|
switch (wID)
|
|
|
|
{
|
|
|
|
case IDOK:
|
|
|
|
EndDialog(0);
|
|
|
|
break;
|
|
|
|
case IDCANCEL:
|
|
|
|
EndDialog(0);
|
|
|
|
break;
|
|
|
|
case IDC_CHK_ENABLE:
|
|
|
|
ToggleLoggingEnabled();
|
|
|
|
break;
|
|
|
|
case IDC_BTN_EXPORT:
|
|
|
|
Export();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnActivate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
|
|
|
|
{
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnListItemChanged(NMHDR* pNMHDR)
|
|
|
|
{
|
|
|
|
NMITEMACTIVATE* pIA = reinterpret_cast<NMITEMACTIVATE*>(pNMHDR);
|
|
|
|
int nItem = pIA->iItem;
|
2019-01-14 11:11:15 +00:00
|
|
|
|
|
|
|
ShowRegStates(m_LogStartIndex + nItem);
|
|
|
|
|
2019-01-14 09:18:43 +00:00
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnListDblClicked(NMHDR* pNMHDR)
|
|
|
|
{
|
|
|
|
NMITEMACTIVATE* pIA = reinterpret_cast<NMITEMACTIVATE*>(pNMHDR);
|
|
|
|
int nItem = pIA->iItem;
|
|
|
|
|
2019-01-14 11:11:15 +00:00
|
|
|
CPUState* state = m_CPULogCopy->GetEntry(m_LogStartIndex + nItem);
|
2019-01-14 09:18:43 +00:00
|
|
|
|
|
|
|
if (state == NULL)
|
|
|
|
{
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_Debugger->Debug_ShowCommandsLocation(state->pc, true);
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnMeasureItem(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)
|
|
|
|
{
|
|
|
|
if (wParam == IDC_CPU_LIST)
|
|
|
|
{
|
|
|
|
CClientDC dc(m_hWnd);
|
|
|
|
dc.SelectFont(GetFont());
|
|
|
|
TEXTMETRIC tm;
|
|
|
|
dc.GetTextMetrics(&tm);
|
|
|
|
|
|
|
|
m_RowHeight = tm.tmHeight + tm.tmExternalLeading;
|
|
|
|
|
|
|
|
MEASUREITEMSTRUCT* lpMeasureItem = (MEASUREITEMSTRUCT*)lParam;
|
|
|
|
lpMeasureItem->itemHeight = m_RowHeight;
|
|
|
|
}
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LRESULT CDebugCPULogView::OnScroll(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)
|
|
|
|
{
|
|
|
|
WORD type = LOWORD(wParam);
|
|
|
|
HWND hScrollbar = (HWND)lParam;
|
2019-01-27 22:40:23 +00:00
|
|
|
WORD scrlId = (WORD)::GetDlgCtrlID(hScrollbar);
|
2019-01-14 09:18:43 +00:00
|
|
|
|
|
|
|
SCROLLINFO scrollInfo;
|
|
|
|
scrollInfo.cbSize = sizeof(SCROLLINFO);
|
|
|
|
scrollInfo.fMask = SIF_ALL;
|
|
|
|
|
|
|
|
::GetScrollInfo(hScrollbar, SB_CTL, &scrollInfo);
|
|
|
|
|
|
|
|
int newPos;
|
|
|
|
|
|
|
|
switch (type)
|
|
|
|
{
|
2019-01-19 20:14:26 +00:00
|
|
|
case SB_LINEUP: newPos = max(scrollInfo.nMin, scrollInfo.nPos - 1); break;
|
|
|
|
case SB_LINEDOWN: newPos = min(scrollInfo.nMax, scrollInfo.nPos + 1); break;
|
2019-01-14 09:18:43 +00:00
|
|
|
case SB_THUMBTRACK: newPos = scrollInfo.nTrackPos; break;
|
|
|
|
default: return 0;
|
|
|
|
}
|
|
|
|
|
2019-01-14 11:11:15 +00:00
|
|
|
m_LogStartIndex = newPos;
|
2019-01-19 20:14:26 +00:00
|
|
|
::SetScrollPos(hScrollbar, SB_CTL, newPos, TRUE);
|
2019-01-14 09:18:43 +00:00
|
|
|
|
|
|
|
if (scrlId == IDC_SCRL_BAR)
|
|
|
|
{
|
|
|
|
RefreshList(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-01-27 22:40:23 +00:00
|
|
|
void CDebugCPULogView::InterceptMouseWheel(WPARAM wParam, LPARAM /*lParam*/)
|
2019-01-14 09:18:43 +00:00
|
|
|
{
|
|
|
|
int nScroll = -((short)HIWORD(wParam) / WHEEL_DELTA);
|
|
|
|
|
|
|
|
if (MouseHovering(IDC_CPU_LIST) || MouseHovering(IDC_SCRL_BAR))
|
|
|
|
{
|
2019-01-19 20:14:26 +00:00
|
|
|
int scrlMin, scrlMax;
|
|
|
|
m_Scrollbar.GetScrollRange(&scrlMin, &scrlMax);
|
|
|
|
|
2019-01-14 09:18:43 +00:00
|
|
|
int scrollPos = m_Scrollbar.GetScrollPos();
|
2019-01-19 20:14:26 +00:00
|
|
|
int newPos = scrollPos + nScroll;
|
|
|
|
|
|
|
|
if (newPos < scrlMin)
|
|
|
|
{
|
|
|
|
newPos = scrlMin;
|
|
|
|
}
|
|
|
|
else if (newPos > scrlMax)
|
|
|
|
{
|
|
|
|
newPos = scrlMax;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_LogStartIndex = newPos;
|
|
|
|
m_Scrollbar.SetScrollPos(newPos, true);
|
|
|
|
|
2019-01-14 09:18:43 +00:00
|
|
|
RefreshList(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CDebugCPULogView::OnExitSizeMove(void)
|
|
|
|
{
|
|
|
|
RefreshList(false);
|
2019-04-19 01:18:55 +00:00
|
|
|
SaveWindowPos(true);
|
2019-01-14 09:18:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CDebugCPULogView::ToggleLoggingEnabled(void)
|
|
|
|
{
|
2019-01-27 22:40:23 +00:00
|
|
|
bool bEnableLogging = (m_EnabledChk.GetCheck() == BST_CHECKED);
|
2019-01-14 09:18:43 +00:00
|
|
|
|
|
|
|
m_BuffSizeEdit.EnableWindow(!bEnableLogging);
|
|
|
|
|
|
|
|
g_Settings->SaveBool(Debugger_CPULoggingEnabled, bEnableLogging);
|
|
|
|
|
|
|
|
if (bEnableLogging)
|
|
|
|
{
|
|
|
|
uint32_t newSize = m_BuffSizeEdit.GetValue();
|
|
|
|
g_Settings->SaveDword(Debugger_CPULogBufferSize, newSize);
|
|
|
|
m_Debugger->CPULog()->Reset();
|
|
|
|
m_ExportBtn.EnableWindow(false);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
RefreshList(true);
|
|
|
|
m_ExportBtn.EnableWindow(m_CPULogCopy != NULL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CDebugCPULogView::RefreshList(bool bUpdateBuffer)
|
|
|
|
{
|
|
|
|
if (bUpdateBuffer)
|
|
|
|
{
|
|
|
|
if (m_CPULogCopy != NULL)
|
|
|
|
{
|
|
|
|
delete m_CPULogCopy;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_CPULogCopy = m_Debugger->CPULog()->Clone();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (m_CPULogCopy == NULL)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t count = m_CPULogCopy->GetCount();
|
|
|
|
size_t numVisibleRows = GetNumVisibleRows(m_CPUListView);
|
|
|
|
|
|
|
|
bool bCanDisplayAll = (numVisibleRows >= count);
|
|
|
|
int scrollRangeMax = bCanDisplayAll ? 0 : count - numVisibleRows;
|
|
|
|
|
|
|
|
m_Scrollbar.SetScrollRange(0, scrollRangeMax, false);
|
|
|
|
m_Scrollbar.EnableWindow(!bCanDisplayAll);
|
|
|
|
|
|
|
|
if (bUpdateBuffer)
|
|
|
|
{
|
2019-01-14 11:11:15 +00:00
|
|
|
m_LogStartIndex = scrollRangeMax;
|
|
|
|
m_Scrollbar.SetScrollPos(m_LogStartIndex, true);
|
2019-01-19 20:14:26 +00:00
|
|
|
ShowRegStates(count - 1);
|
2019-01-14 09:18:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
size_t start = m_Scrollbar.GetScrollPos();
|
|
|
|
size_t end = start + numVisibleRows;
|
|
|
|
|
|
|
|
if (end > count)
|
|
|
|
{
|
|
|
|
end = count;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_CPUListView.SetRedraw(FALSE);
|
|
|
|
m_CPUListView.DeleteAllItems();
|
|
|
|
|
|
|
|
int nItem = 0;
|
|
|
|
|
|
|
|
for (size_t i = start; i < end; i++)
|
|
|
|
{
|
|
|
|
CPUState* state = m_CPULogCopy->GetEntry(i);
|
|
|
|
|
|
|
|
if (state == NULL)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
char szPC[9];
|
|
|
|
sprintf(szPC, "%08X", state->pc);
|
|
|
|
|
|
|
|
char *tokctx;
|
|
|
|
|
|
|
|
char* szCommand = (char*)R4300iOpcodeName(state->opcode.Hex, state->pc);
|
|
|
|
char* szCmdName = strtok_s((char*)szCommand, "\t", &tokctx);
|
|
|
|
char* szCmdArgs = strtok_s(NULL, "\t", &tokctx);
|
|
|
|
|
2020-05-12 12:19:05 +00:00
|
|
|
m_CPUListView.AddItem(nItem, 0, stdstr(szPC).ToUTF16().c_str());
|
|
|
|
m_CPUListView.AddItem(nItem, 1, stdstr(szCmdName).ToUTF16().c_str());
|
|
|
|
m_CPUListView.AddItem(nItem, 2, stdstr(szCmdArgs).ToUTF16().c_str());
|
2019-01-14 09:18:43 +00:00
|
|
|
|
|
|
|
nItem++;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_CPUListView.SetRedraw(TRUE);
|
|
|
|
}
|
|
|
|
|
|
|
|
void CDebugCPULogView::ShowRegStates(size_t stateIndex)
|
|
|
|
{
|
|
|
|
CPUState* state = m_CPULogCopy->GetEntry(stateIndex);
|
|
|
|
|
|
|
|
if (state == NULL)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
char szRegStates[2048];
|
|
|
|
char* out = szRegStates;
|
|
|
|
|
|
|
|
out += sprintf(out, "PC: %08X\r\n\r\n", state->pc);
|
|
|
|
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
|
|
{
|
|
|
|
int regl = i, regr = i + 16;
|
|
|
|
out += sprintf(out, "%s: %08X %08X %s: %08X %08X\r\n",
|
|
|
|
CRegName::GPR[regl], state->gpr[regl].UW[1], state->gpr[regl].UW[0],
|
|
|
|
CRegName::GPR[regr], state->gpr[regr].UW[1], state->gpr[regr].UW[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
out += sprintf(out, "HI: %08X %08X LO: %08X %08X\r\n\r\n",
|
|
|
|
state->gprHi.UW[1], state->gprHi.UW[0],
|
|
|
|
state->gprLo.UW[1], state->gprLo.UW[0]);
|
|
|
|
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
|
|
{
|
|
|
|
int regl = i, regr = i + 16;
|
|
|
|
out += sprintf(out, "%-3s: %08X %-3s: %08X\r\n",
|
|
|
|
CRegName::FPR[regl], *(uint32_t*)&state->fpr[regl],
|
|
|
|
CRegName::FPR[regr], *(uint32_t*)&state->fpr[regr]);
|
|
|
|
}
|
|
|
|
|
|
|
|
out += sprintf(out, "FPCR: %08X\r\n", state->fpcr);
|
|
|
|
|
2020-05-12 12:19:05 +00:00
|
|
|
m_StateInfoEdit.SetWindowText(stdstr(szRegStates).ToUTF16().c_str());
|
2019-01-14 09:18:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CDebugCPULogView::Export(void)
|
|
|
|
{
|
|
|
|
if (m_CPULogCopy == NULL)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-12 12:19:05 +00:00
|
|
|
OPENFILENAMEA openfilename;
|
2019-01-14 09:18:43 +00:00
|
|
|
char filePath[255];
|
|
|
|
|
|
|
|
memset(&filePath, 0, sizeof(filePath));
|
|
|
|
memset(&openfilename, 0, sizeof(openfilename));
|
|
|
|
|
|
|
|
sprintf(filePath, "CPULOG.txt");
|
|
|
|
|
|
|
|
openfilename.lStructSize = sizeof(openfilename);
|
|
|
|
openfilename.hwndOwner = (HWND)m_hWnd;
|
|
|
|
openfilename.lpstrFilter = "CPU Log (*.*)\0*.*;\0";
|
|
|
|
openfilename.lpstrFile = filePath;
|
|
|
|
openfilename.lpstrInitialDir = "Logs";
|
|
|
|
openfilename.nMaxFile = MAX_PATH;
|
|
|
|
openfilename.Flags = OFN_HIDEREADONLY;
|
|
|
|
|
2020-05-12 12:19:05 +00:00
|
|
|
if (GetSaveFileNameA(&openfilename))
|
2019-01-14 09:18:43 +00:00
|
|
|
{
|
|
|
|
m_CPULogCopy->DumpToFile(filePath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// util
|
|
|
|
|
2019-04-19 23:48:53 +00:00
|
|
|
int CDebugCPULogView::GetNumVisibleRows(CListViewCtrl& list)
|
2019-01-14 09:18:43 +00:00
|
|
|
{
|
|
|
|
CHeaderCtrl header = list.GetHeader();
|
|
|
|
CRect listRect, headRect;
|
|
|
|
list.GetWindowRect(&listRect);
|
|
|
|
header.GetWindowRect(&headRect);
|
|
|
|
int innerHeight = listRect.Height() - headRect.Height();
|
|
|
|
return (innerHeight / m_RowHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool CDebugCPULogView::MouseHovering(WORD ctrlId, int xMargin, int yMargin)
|
|
|
|
{
|
|
|
|
CRect rect;
|
|
|
|
POINT pointerPos;
|
|
|
|
|
|
|
|
::GetWindowRect(GetDlgItem(ctrlId), &rect);
|
|
|
|
::GetCursorPos(&pointerPos);
|
|
|
|
|
|
|
|
return (
|
|
|
|
pointerPos.x >= rect.left - xMargin &&
|
|
|
|
pointerPos.x <= rect.right + xMargin &&
|
|
|
|
pointerPos.y >= rect.top - yMargin &&
|
|
|
|
pointerPos.y <= rect.bottom + yMargin);
|
|
|
|
}
|