Files
2025-12-10 14:38:26 -08:00

546 lines
18 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "log_panel.h"
#include <wx+/window_tools.h>
#include <wx+/image_resources.h>
#include <wx+/rtl.h>
#include <wx+/context_menu.h>
#include <wx+/popup_dlg.h>
using namespace zen;
using namespace fff;
namespace
{
inline wxColor getColorGridLine() { return {192, 192, 192}; } //light grey
inline
wxImage getImageButtonPressed(const char* imageName)
{
return layOver(loadImage("msg_button_pressed"),
loadImage(imageName));
}
inline
wxImage getImageButtonReleased(const char* imageName)
{
return greyScale(loadImage(imageName));
//loadImage(utfTo<wxString>(imageName)).ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally!
//brighten(output, 30);
}
enum class ColumnTypeLog
{
time,
severity,
text,
};
}
//a vector-view on ErrorLog considering multi-line messages: prepare consumption by Grid
class fff::MessageView
{
public:
explicit MessageView(const SharedRef<const ErrorLog>& log) : log_(log) {}
size_t rowsOnView() const { return viewRef_.size(); }
struct LogEntryView
{
time_t time = 0;
MessageType type = MSG_TYPE_INFO;
std::string_view messageLine;
bool firstLine = false; //if LogEntry::message spans multiple rows
};
std::optional<LogEntryView> getEntry(size_t row) const
{
if (row < viewRef_.size())
{
const Line& line = viewRef_[row];
LogEntryView output;
output.time = line.logIt->time;
output.type = line.logIt->type;
output.messageLine = extractLine(line.logIt->message, line.row);
output.firstLine = line.row == 0; //this is virtually always correct, unless first line of the original message is empty!
return output;
}
return {};
}
void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, etc. see error_log.h
{
viewRef_.clear();
for (auto it = log_.ref().begin(); it != log_.ref().end(); ++it)
if (it->type & includedTypes)
{
assert(!startsWith(it->message, '\n'));
size_t rowNumber = 0;
bool lastCharNewline = true;
for (const char c : it->message)
if (c == '\n')
{
if (!lastCharNewline) //do not reference empty lines!
viewRef_.push_back({it, rowNumber});
++rowNumber;
lastCharNewline = true;
}
else
lastCharNewline = false;
if (!lastCharNewline)
viewRef_.push_back({it, rowNumber});
}
}
private:
static std::string_view extractLine(const Zstringc& message, size_t textRow)
{
auto it1 = message.begin();
for (;;)
{
auto it2 = std::find_if(it1, message.end(), [](const char c) { return c == '\n'; });
if (textRow == 0)
return makeStringView(it1, it2 - it1);
if (it2 == message.end())
{
assert(false);
return makeStringView(it1, 0);
}
it1 = it2 + 1; //skip newline
--textRow;
}
}
struct Line
{
ErrorLog::const_iterator logIt; //always bound!
size_t row; //LogEntry::message may span multiple rows
};
std::vector<Line> viewRef_; //partial view on log_
/* /|\
| updateView()
| */
const SharedRef<const ErrorLog> log_;
};
//-----------------------------------------------------------------------------
namespace
{
//Grid data implementation referencing MessageView
class GridDataMessages : public GridData
{
public:
explicit GridDataMessages(const SharedRef<const ErrorLog>& log) : msgView_(log) {}
MessageView& getDataView() { return msgView_; }
size_t getRowCount() const override { return msgView_.rowsOnView(); }
std::wstring getValue(size_t row, ColumnType colType) const override
{
if (const std::optional<MessageView::LogEntryView> entry = msgView_.getEntry(row))
switch (static_cast<ColumnTypeLog>(colType))
{
case ColumnTypeLog::time:
if (entry->firstLine)
return utfTo<std::wstring>(formatTime(formatTimeTag, getLocalTime(entry->time))); //empty string on error
break;
case ColumnTypeLog::severity:
if (entry->firstLine)
switch (entry->type)
{
case MSG_TYPE_INFO:
return _("Info");
case MSG_TYPE_WARNING:
return _("Warning");
case MSG_TYPE_ERROR:
return _("Error");
}
break;
case ColumnTypeLog::text:
return utfTo<std::wstring>(entry->messageLine);
}
return std::wstring();
}
void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override
{
if (!enabled || !selected)
; //clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default
else
GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover);
//-------------- draw item separation line -----------------
const bool drawBottomLine = [&] //don't separate multi-line messages
{
if (std::optional<MessageView::LogEntryView> nextEntry = msgView_.getEntry(row + 1))
return nextEntry->firstLine;
return true;
}();
if (drawBottomLine)
clearArea(dc, {rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)}, getColorGridLine());
//--------------------------------------------------------
}
void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override
{
wxDCTextColourChanger textColor(dc);
if (enabled && selected) //accessibility: always set *both* foreground AND background colors!
textColor.Set(*wxBLACK);
wxRect rectTmp = rect;
if (std::optional<MessageView::LogEntryView> entry = msgView_.getEntry(row))
switch (static_cast<ColumnTypeLog>(colType))
{
case ColumnTypeLog::time:
drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER);
break;
case ColumnTypeLog::severity:
if (entry->firstLine)
{
wxImage msgTypeIcon = [&]
{
switch (entry->type)
{
case MSG_TYPE_INFO:
return loadImage("msg_info", dipToScreen(getMenuIconDipSize()));
case MSG_TYPE_WARNING:
return loadImage("msg_warning", dipToScreen(getMenuIconDipSize()));
case MSG_TYPE_ERROR:
return loadImage("msg_error", dipToScreen(getMenuIconDipSize()));
}
assert(false);
return wxNullImage;
}();
drawBitmapRtlNoMirror(dc, enabled ? msgTypeIcon : msgTypeIcon.ConvertToDisabled(), rectTmp, wxALIGN_CENTER);
}
break;
case ColumnTypeLog::text:
rectTmp.x += getColumnGapLeft();
rectTmp.width -= getColumnGapLeft();
drawCellText(dc, rectTmp, getValue(row, colType));
break;
}
}
int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override
{
// -> synchronize renderCell() <-> getBestSize()
if (msgView_.getEntry(row))
switch (static_cast<ColumnTypeLog>(colType))
{
case ColumnTypeLog::time:
return 2 * getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
case ColumnTypeLog::severity:
return dipToWxsize(getMenuIconDipSize());
case ColumnTypeLog::text:
return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
}
return 0;
}
static int getColumnTimeDefaultWidth(Grid& grid)
{
wxInfoDC dc(&grid.getMainWin());
dc.SetFont(grid.getMainWin().GetFont());
return 2 * getColumnGapLeft() + dc.GetTextExtent(utfTo<wxString>(formatTime(formatTimeTag))).GetWidth();
}
static int getColumnSeverityDefaultWidth()
{
return dipToWxsize(getMenuIconDipSize());
}
static int getRowDefaultHeight(const Grid& grid)
{
return std::max(dipToWxsize(getMenuIconDipSize()), grid.getMainWin().GetCharHeight() + dipToWxsize(2) /*extra space*/) + dipToWxsize(1) /*bottom border*/;
}
std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override
{
switch (static_cast<ColumnTypeLog>(colType))
{
case ColumnTypeLog::time:
case ColumnTypeLog::text:
break;
case ColumnTypeLog::severity:
return getValue(row, colType);
}
return std::wstring();
}
std::wstring getColumnLabel(ColumnType colType) const override { return std::wstring(); }
private:
MessageView msgView_;
};
}
//########################################################################################
LogPanel::LogPanel(wxWindow* parent) : LogPanelGenerated(parent)
{
const int rowHeight = GridDataMessages::getRowDefaultHeight(*m_gridMessages);
const int colMsgTimeWidth = GridDataMessages::getColumnTimeDefaultWidth(*m_gridMessages);
const int colMsgSeverityWidth = GridDataMessages::getColumnSeverityDefaultWidth();
m_gridMessages->setColumnLabelHeight(0);
m_gridMessages->showRowLabel(false);
m_gridMessages->setRowHeight(rowHeight);
m_gridMessages->setColumnConfig(
{
{static_cast<ColumnType>(ColumnTypeLog::time ), colMsgTimeWidth, 0, true},
{static_cast<ColumnType>(ColumnTypeLog::severity), colMsgSeverityWidth, 0, true},
{static_cast<ColumnType>(ColumnTypeLog::text ), -colMsgTimeWidth - colMsgSeverityWidth, 1, true},
});
//support for CTRL + C
m_gridMessages->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event); });
m_gridMessages->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onMsgGridContext(event); });
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events
setLog(nullptr);
}
void LogPanel::setLog(const std::shared_ptr<const ErrorLog>& log)
{
SharedRef<const ErrorLog> newLog = [&]
{
if (log)
return SharedRef<const ErrorLog>(log);
ErrorLog dummyLog;
logMsg(dummyLog, _("No log entries"), MSG_TYPE_INFO);
return makeSharedRef<const ErrorLog>(std::move(dummyLog));
}();
const ErrorLogStats logCount = getStats(newLog.ref());
auto initButton = [](ToggleButton& btn, const char* imgName, const wxString& tooltip)
{
btn.init(getImageButtonPressed(imgName), getImageButtonReleased(imgName));
btn.SetToolTip(tooltip);
};
initButton(*m_bpButtonErrors, "msg_error", _("Error" ) + L" (" + formatNumber(logCount.errors) + L')');
initButton(*m_bpButtonWarnings, "msg_warning", _("Warning") + L" (" + formatNumber(logCount.warnings) + L')');
initButton(*m_bpButtonInfo, "msg_info", _("Info" ) + L" (" + formatNumber(logCount.infos) + L')');
m_bpButtonErrors ->setActive(true);
m_bpButtonWarnings->setActive(true);
m_bpButtonInfo ->setActive(logCount.warnings + logCount.errors == 0);
m_bpButtonErrors ->Show(logCount.errors != 0);
m_bpButtonWarnings->Show(logCount.warnings != 0);
m_bpButtonInfo ->Show(logCount.infos != 0);
m_gridMessages->setDataProvider(std::make_shared<GridDataMessages>(newLog));
updateGrid();
}
MessageView& LogPanel::getDataView()
{
if (auto* prov = dynamic_cast<GridDataMessages*>(m_gridMessages->getDataProvider()))
return prov->getDataView();
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] m_gridMessages was not initialized.");
}
void LogPanel::updateGrid()
{
int includedTypes = 0;
if (m_bpButtonErrors->isActive())
includedTypes |= MSG_TYPE_ERROR;
if (m_bpButtonWarnings->isActive())
includedTypes |= MSG_TYPE_WARNING;
if (m_bpButtonInfo->isActive())
includedTypes |= MSG_TYPE_INFO;
getDataView().updateView(includedTypes); //update MVC "model"
m_gridMessages->Refresh(); //update MVC "view"
}
void LogPanel::onErrors(wxCommandEvent& event)
{
m_bpButtonErrors->toggle();
updateGrid();
}
void LogPanel::onWarnings(wxCommandEvent& event)
{
m_bpButtonWarnings->toggle();
updateGrid();
}
void LogPanel::onInfo(wxCommandEvent& event)
{
m_bpButtonInfo->toggle();
updateGrid();
}
void LogPanel::onMsgGridContext(GridContextMenuEvent& event)
{
const std::vector<size_t> selection = m_gridMessages->getSelectedRows();
const size_t rowCount = [&]() -> size_t
{
if (auto prov = m_gridMessages->getDataProvider())
return prov->getRowCount();
return 0;
}();
ContextMenu menu;
menu.addItem(_("&Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, loadImage("item_copy_sicon"), !selection.empty());
menu.addSeparator();
menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridMessages->selectAllRows(GridEventPolicy::allow); }, wxNullImage, rowCount > 0);
menu.popup(*m_gridMessages, event.mousePos_);
}
void LogPanel::onGridKeyEvent(wxKeyEvent& event)
{
int keyCode = event.GetKeyCode();
if (event.ControlDown())
switch (keyCode)
{
case 'C':
case WXK_INSERT: //CTRL + C || CTRL + INS
case WXK_NUMPAD_INSERT:
copySelectionToClipboard();
return; // -> swallow event! don't allow default grid commands!
}
//else
//switch (keyCode)
//{
// case WXK_RETURN:
// case WXK_NUMPAD_ENTER:
// return;
//}
event.Skip(); //unknown keypress: propagate
}
void LogPanel::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
{
if (!processingKeyEventHandler_) //avoid recursion
{
processingKeyEventHandler_ = true;
ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false);
const int keyCode = event.GetKeyCode();
if (event.ControlDown())
switch (keyCode)
{
case 'A':
m_gridMessages->SetFocus();
m_gridMessages->selectAllRows(GridEventPolicy::allow);
return; // -> swallow event! don't allow default grid commands!
}
else
switch (keyCode)
{
//redirect certain (unhandled) keys directly to grid!
case WXK_UP:
case WXK_DOWN:
case WXK_LEFT:
case WXK_RIGHT:
case WXK_PAGEUP:
case WXK_PAGEDOWN:
case WXK_HOME:
case WXK_END:
case WXK_NUMPAD_UP:
case WXK_NUMPAD_DOWN:
case WXK_NUMPAD_LEFT:
case WXK_NUMPAD_RIGHT:
case WXK_NUMPAD_PAGEUP:
case WXK_NUMPAD_PAGEDOWN:
case WXK_NUMPAD_HOME:
case WXK_NUMPAD_END:
if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus
m_gridMessages->IsEnabled())
{
m_gridMessages->SetFocus();
event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
m_gridMessages->getMainWin().GetEventHandler()->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it...
event.Skip(false); //definitively handled now!
return;
}
break;
}
}
event.Skip();
}
void LogPanel::copySelectionToClipboard()
{
try
{
wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based:
static_assert(std::is_same_v<wxStringImpl, std::wstring>);
if (auto prov = m_gridMessages->getDataProvider())
{
std::vector<Grid::ColAttributes> colAttr = m_gridMessages->getColumnConfig();
std::erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; });
for (size_t row : m_gridMessages->getSelectedRows())
for (auto it = colAttr.begin(); it != colAttr.end(); ++it)
{
clipBuf += prov->getValue(row, it->type);
clipBuf += it == colAttr.end() - 1 ? L'\n' : L'\t';
}
}
setClipboardText(clipBuf);
}
catch (const std::bad_alloc& e)
{
showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L' ' + utfTo<std::wstring>(e.what())));
}
}