blob: c74989ce3f48524725626357ae1985e8b0913e3c [file] [log] [blame]
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "analyzer.hpp"
#include "bargraph.hpp"
#include "main.hpp"
#include "menu.hpp"
#include "sensorhelper.hpp"
#include <ncurses.h>
#include <string>
#include <vector>
constexpr int MARGIN_BOTTOM = 1;
class DBusTopWindow
{
public:
DBusTopWindow()
{
win = newwin(25, 80, 0, 0); // Default to 80x25, will be updated
has_border_ = true;
focused_ = false;
selectable_ = true;
visible_ = true;
maximize_ = false;
}
virtual ~DBusTopWindow() {}
virtual void OnKeyDown(const std::string& key) = 0;
virtual void Render() = 0;
virtual void OnResize(int win_w, int win_h) = 0;
void UpdateWindowSizeAndPosition()
{
mvwin(win, rect.y, rect.x);
wresize(win, rect.h, rect.w);
}
void DrawBorderIfNeeded()
{
if (focused_)
{
wborder(win, '*', '*', '*', '*', '*', '*', '*', '*');
}
else
{
wborder(win, '|', '|', '-', '-', '+', '+', '+', '+');
}
wrefresh(win);
}
virtual void RecreateWindow()
{
delwin(win);
win = newwin(25, 80, 0, 0);
UpdateWindowSizeAndPosition();
}
virtual std::string GetStatusString() = 0;
WINDOW* win;
Rect rect;
bool has_border_;
bool focused_;
bool selectable_;
bool maximize_;
bool visible_;
};
class SummaryView : public DBusTopWindow
{
public:
SummaryView() : DBusTopWindow() {}
void Render() override;
void OnResize(int win_w, [[maybe_unused]] int win_h) override
{
rect.h = 8;
rect.w = win_w;
rect.x = 0;
rect.y = 0;
UpdateWindowSizeAndPosition();
}
void UpdateDBusTopStatistics(DBusTopStatistics* stat);
void OnKeyDown([[maybe_unused]] const std::string& key) override {}
std::string GetStatusString() override
{
return "Summary View";
}
private:
float method_call_, method_return_, signal_, error_, total_;
};
class SensorDetailView : public DBusTopWindow
{
public:
SensorDetailView() : DBusTopWindow()
{
choice_ = -999; // -999 means invalid
h_padding = 2;
h_spacing = 3;
col_width = 15;
idx0 = idx1 = -999;
state = SensorList;
}
void Render() override;
int DispSensorsPerColumn()
{
return rect.h - 3;
}
int DispSensorsPerRow()
{
int ncols = 0;
while (true)
{
int next = ncols + 1;
int w = 2 * h_padding + col_width * next;
if (next > 1)
w += (next - 1) * h_spacing;
if (w <= rect.w - 2)
{
ncols = next;
}
else
{
break;
}
}
return ncols;
}
void OnKeyDown(const std::string& key) override
{
if (state == SensorList)
{ // Currently in sensor list
if (key == "right")
{
MoveChoiceCursorHorizontally(1);
}
else if (key == "left")
{
MoveChoiceCursorHorizontally(-1);
}
else if (key == "up")
{
MoveChoiceCursor(-1, true);
}
else if (key == "down")
{
MoveChoiceCursor(1, true);
}
else if (key == "enter")
{
if (choice_ != -999)
{
state = SensorDetail;
}
}
else if (key == "escape")
{
choice_ = -999;
}
}
else if (state == SensorDetail)
{ // Currently focusing on a sensor
if (key == "right" || key == "down")
{
MoveChoiceCursor(1, true);
}
else if (key == "left" || key == "up")
{
MoveChoiceCursor(-1, true);
}
else if (key == "escape")
{
state = SensorList;
}
}
Render(); // This window is already on top, redrawing won't corrupt
}
void MoveChoiceCursor(int delta, bool wrap_around = true)
{
const int ns = sensor_ids_.size();
if (ns < 1)
return;
// First of all, if cursor is inactive, activate it
if (choice_ == -999)
{
if (delta > 0)
{
choice_ = 0;
curr_sensor_id_ = sensor_ids_[0];
return;
}
else
{
choice_ = ns - 1;
curr_sensor_id_ = sensor_ids_.back();
return;
}
}
int choice_next = choice_ + delta;
while (choice_next >= ns)
{
if (wrap_around)
{
choice_next -= ns;
}
else
{
choice_next = ns - 1;
}
}
while (choice_next < 0)
{
if (wrap_around)
{
choice_next += ns;
}
else
{
choice_next = 0;
}
}
choice_ = choice_next;
curr_sensor_id_ = sensor_ids_[choice_];
}
void MoveChoiceCursorHorizontally(int delta)
{
if (delta != 0 && delta != -1 && delta != 1)
return;
const int ns = sensor_ids_.size();
if (ns < 1)
return;
if (choice_ == -999)
{
if (delta > 0)
{
choice_ = 0;
curr_sensor_id_ = sensor_ids_[0];
return;
}
else
{
curr_sensor_id_ = sensor_ids_.back();
choice_ = ns - 1;
return;
}
}
const int nrows = DispSensorsPerColumn();
int tot_columns = (ns - 1) / nrows + 1;
int num_rows_last_column = ns - nrows * (tot_columns - 1);
int y = choice_ % nrows, x = choice_ / nrows;
if (delta == 1)
{
x++;
}
else
{
x--;
}
bool overflow_to_right = false;
if (y < num_rows_last_column)
{
if (x >= tot_columns)
{
overflow_to_right = true;
}
}
else
{
if (x >= tot_columns - 1)
{
overflow_to_right = true;
}
}
bool overflow_to_left = false;
if (x < 0)
{
overflow_to_left = true;
}
{
if (overflow_to_right)
{
y++;
// overflow past the right of window
// Start probing next line
if (y >= nrows)
{
choice_ = 0;
return;
}
else
{
choice_ = y;
return;
}
}
else if (overflow_to_left)
{ // overflow past the left of window
y--;
if (y < 0)
{ // overflow past the top of window
// Focus on the visually bottom-right entry
if (num_rows_last_column == nrows)
{ // last col fully populated
choice_ = ns - 1;
}
else
{ // last column is not fully populated
choice_ = ns - num_rows_last_column - 1;
}
return;
}
else
{
if (y < num_rows_last_column)
{
choice_ = nrows * (tot_columns - 1) + y;
}
else
{
choice_ = nrows * (tot_columns - 2) + y;
}
}
}
else
{
choice_ = y + x * nrows;
}
}
curr_sensor_id_ = sensor_ids_[choice_];
}
// Make a copy of the SensorSnapshot object for display usage
void UpdateSensorSnapshot(SensorSnapshot* snapshot)
{
sensor_snapshot_ = *snapshot;
std::string old_sensor_id = "";
if (choice_ != -999)
{
old_sensor_id = sensor_ids_[choice_];
}
std::vector<std::string> new_sensors =
snapshot->GetDistinctSensorNames();
if (new_sensors == sensor_ids_)
{
return; // Nothing is changed
}
// Assume changed
sensor_ids_ = new_sensors;
choice_ = -999;
for (int i = 0; i < static_cast<int>(new_sensors.size()); i++)
{
if (new_sensors[i] == old_sensor_id)
{
choice_ = i;
break;
curr_sensor_id_ = sensor_ids_[choice_];
}
}
}
void OnResize(int win_w, int win_h) override
{
rect.x = 0;
rect.y = 8 - MARGIN_BOTTOM;
rect.w = win_w / 2;
rect.h = win_h - rect.y - MARGIN_BOTTOM;
UpdateWindowSizeAndPosition();
}
std::vector<std::string> sensor_ids_;
// We need to keep track of the currently-selected sensor ID because
// the sensor ID might theoretically become invalidated at any moment, and
// we should allow the UI to show an error gracefully in that case.
std::string curr_sensor_id_;
int choice_;
int h_padding;
int h_spacing;
int col_width;
int idx0, idx1; // Range of sensors on display
enum State
{
SensorList,
SensorDetail,
};
State state;
std::string GetStatusString() override;
SensorSnapshot sensor_snapshot_;
};
class DBusStatListView : public DBusTopWindow
{
public:
DBusStatListView();
void Render() override;
void OnResize(int win_w, int win_h) override;
void OnKeyDown(const std::string& key) override;
int horizontal_pan_;
int menu_h_, menu_w_, menu_margin_;
ArrowKeyNavigationMenu* menu1_;
ArrowKeyNavigationMenu* menu2_;
int highlight_col_idx_; // Currently highlighted column
int row_idx_; // Currently highlighted row
int sort_col_idx_; // Column used for sorting
enum SortOrder
{
Ascending,
Descending,
};
SortOrder sort_order_;
int disp_row_idx_; // From which row to start displaying? (essentially a
// vertical scroll bar)
int last_choices_[2]; // Last choice index on either side
enum MenuState
{
Hidden,
LeftSide, // When the user is choosing an entry on the left side
RightSide, // When the user is choosing an entry on the right side
};
std::vector<DBusTopSortField> GetSortFields();
MenuState curr_menu_state_, last_menu_state_;
std::string GetStatusString() override;
void RecreateWindow()
{
delwin(win);
win = newwin(25, 80, 0, 0);
menu1_->win_ = win;
menu2_->win_ = win;
UpdateWindowSizeAndPosition();
}
private:
void SetMenuState(MenuState s)
{
last_menu_state_ = curr_menu_state_;
// Moving out from a certain side: save the last choice of that side
switch (curr_menu_state_)
{
case LeftSide:
if (s == RightSide)
{
last_choices_[0] = menu1_->Choice();
menu1_->Deselect();
}
break;
case RightSide:
if (s == LeftSide)
{
last_choices_[1] = menu2_->Choice();
menu2_->Deselect();
}
break;
default:
break;
}
// Moving into a certain side: save the cursor
switch (s)
{
case LeftSide:
if (!menu1_->Empty())
{
menu1_->SetChoiceAndConstrain(last_choices_[0]);
}
break;
case RightSide:
if (!menu2_->Empty())
{
menu2_->SetChoiceAndConstrain(last_choices_[1]);
}
break;
default:
break;
}
curr_menu_state_ = s;
}
void PanViewportOrMoveHighlightedColumn(const int delta_x);
// ColumnHeaders and ColumnWidths are the actual column widths used for
// display. They are "msg/s" or "I2c/s" prepended to the chosen set of
// fields.
std::vector<std::string> ColumnHeaders();
std::vector<int> ColumnWidths();
// X span, for checking visibility
std::pair<int, int> GetXSpanForColumn(const int col_idx);
bool IsXSpanVisible(const std::pair<int, int>& xs,
const int tolerance); // uses horizontal_pan_
std::vector<std::string> visible_columns_;
std::unordered_map<std::string, int> column_widths_;
std::map<std::vector<std::string>, DBusTopComputedMetrics> stats_snapshot_;
};
class FooterView : public DBusTopWindow
{
public:
FooterView() : DBusTopWindow()
{
selectable_ = false; // Cannot be selected by the tab key
}
void OnKeyDown([[maybe_unused]] const std::string& key) override {}
void OnResize(int win_w, int win_h) override
{
rect.h = 1;
rect.w = win_w;
rect.x = 0;
rect.y = win_h - 1;
UpdateWindowSizeAndPosition();
}
void Render() override;
std::string GetStatusString() override
{
return "";
}
void SetStatusString(const std::string& s)
{
status_string_ = s;
}
std::string status_string_;
};