diff --git a/console_backend/src/connection.rs b/console_backend/src/connection.rs index ad8fdbeb5..7bd5e30b5 100644 --- a/console_backend/src/connection.rs +++ b/console_backend/src/connection.rs @@ -228,6 +228,7 @@ impl ConnectionState { ) -> JoinHandle<()> { thread::spawn(move || { let mut conn = None; + info!("Console started..."); while shared_state.clone().is_server_running() { if let Ok(conn_option) = receiver.recv_timeout(Duration::from_secs_f64( SERVER_STATE_CONNECTION_LOOP_TIMEOUT_SEC, @@ -253,6 +254,7 @@ impl ConnectionState { } shared_state.set_running(false, client_send.clone()); } + log::logger().flush(); } }) } diff --git a/console_backend/src/errors.rs b/console_backend/src/errors.rs index 8ec8484d4..5e68bcb0e 100644 --- a/console_backend/src/errors.rs +++ b/console_backend/src/errors.rs @@ -14,3 +14,4 @@ pub(crate) const TCP_CONNECTION_PARSING_FAILURE: &str = "unable to parse the provided string for ip string"; pub(crate) const SERVER_STATE_NEW_CONNECTION_FAILURE: &str = "server state new connection failure"; pub(crate) const SERVER_STATE_DISCONNECT_FAILURE: &str = "server state disconnect failure"; +pub(crate) const CONSOLE_LOG_JSON_TO_STRING_FAILURE: &str = "unable to convert json to string"; diff --git a/console_backend/src/log_panel.rs b/console_backend/src/log_panel.rs index 4ad7cd196..3d4c39634 100644 --- a/console_backend/src/log_panel.rs +++ b/console_backend/src/log_panel.rs @@ -1,14 +1,28 @@ +use async_logger_log::Logger; use sbp::messages::logging::MsgLog; use capnp::message::Builder; use crate::common_constants as cc; +use crate::constants::LOG_WRITER_BUFFER_MESSAGE_COUNT; +use crate::errors::CONSOLE_LOG_JSON_TO_STRING_FAILURE; use crate::types::*; use crate::utils::serialize_capnproto_builder; use async_logger::Writer; use chrono::Local; use log::{debug, error, info, warn, LevelFilter, Record}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct ConsoleLogPacket { + level: String, + timestamp: String, + msg: String, +} + +const DEVICE: &str = "DEVICE"; +const CONSOLE: &str = "CONSOLE"; pub type LogLevel = cc::LogLevel; impl LogLevel { @@ -24,13 +38,19 @@ impl LogLevel { // Custom formatting of `log::Record` to account for SbpLog values pub fn splitable_log_formatter(record: &Record) -> String { - // TODO (JV): CPP-117 Extract SbpLog timestamp and level from message - format!( - "{} {} {}", - Local::now().format("%Y-%m-%dT%H:%M:%S"), - record.level(), - record.args() - ) + let level = if record.target() != DEVICE { + CONSOLE + } else { + record.level().as_str() + }; + let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S"); + let msg = record.args(); + let msg_packet = ConsoleLogPacket { + level: level.to_string(), + timestamp: timestamp.to_string(), + msg: msg.to_string(), + }; + serde_json::to_string(&msg_packet).expect(CONSOLE_LOG_JSON_TO_STRING_FAILURE) } enum SbpMsgLevel { @@ -64,18 +84,29 @@ impl From for SbpMsgLevel { pub fn handle_log_msg(msg: MsgLog) { let text = msg.text.to_string(); let level: SbpMsgLevel = SbpMsgLevel::from(msg.level); - // TODO(JV): CPP-117 Include log level and remote timestamp in text message match level { SbpMsgLevel::Emergency | SbpMsgLevel::Alert | SbpMsgLevel::Critical - | SbpMsgLevel::Error => error!("{}", text), - SbpMsgLevel::Warn | SbpMsgLevel::Notice => warn!("{}", text), - SbpMsgLevel::Info => info!("{}", text), - _ => debug!("{}", text), + | SbpMsgLevel::Error => error!(target: DEVICE, "{}", text), + SbpMsgLevel::Warn | SbpMsgLevel::Notice => warn!(target: DEVICE, "{}", text), + SbpMsgLevel::Info => info!(target: DEVICE, "{}", text), + _ => debug!(target: DEVICE, "{}", text), } } +pub fn setup_logging(client_sender: ClientSender) { + let log_panel = LogPanelWriter::new(client_sender); + let logger = Logger::builder() + .buf_size(LOG_WRITER_BUFFER_MESSAGE_COUNT) + .formatter(splitable_log_formatter) + .writer(Box::new(log_panel)) + .build() + .unwrap(); + + log::set_boxed_logger(Box::new(logger)).expect("Failed to set logger"); +} + #[derive(Debug)] pub struct LogPanelWriter { pub client_sender: S, @@ -102,7 +133,6 @@ impl Writer> for LogPanelWriter { for (idx, item) in slice.iter().enumerate() { let mut entry = entries.reborrow().get(idx as u32); - //TODO: split line into timestamp, level and text entry.set_line(&**item); } diff --git a/console_backend/src/piksi_tools_constants.rs b/console_backend/src/piksi_tools_constants.rs index 57d27286d..26fe9a279 100644 --- a/console_backend/src/piksi_tools_constants.rs +++ b/console_backend/src/piksi_tools_constants.rs @@ -508,7 +508,7 @@ pub const SMOOTHPOSE: i32 = 0; pub const DR_RUNNER: i32 = 1; lazy_static! { pub static ref ins_type_dict: HashMap = - [(SMOOTHPOSE, "SP"), (DR_RUNNER, "DR")] + [(SMOOTHPOSE, "SP-"), (DR_RUNNER, "")] .iter() .cloned() .collect::>(); diff --git a/console_backend/src/server.rs b/console_backend/src/server.rs index bf6c5f467..b3d05264c 100644 --- a/console_backend/src/server.rs +++ b/console_backend/src/server.rs @@ -1,11 +1,9 @@ use capnp::serialize; - +use log::{error, info}; use pyo3::exceptions; use pyo3::prelude::*; use pyo3::types::PyBytes; -use async_logger_log::Logger; -use log::error; use std::{ io::{BufReader, Cursor}, path::PathBuf, @@ -17,9 +15,8 @@ use std::{ use crate::cli_options::*; use crate::connection::ConnectionState; use crate::console_backend_capnp as m; -use crate::constants::LOG_WRITER_BUFFER_MESSAGE_COUNT; use crate::errors::*; -use crate::log_panel::{splitable_log_formatter, LogLevel, LogPanelWriter}; +use crate::log_panel::{setup_logging, LogLevel}; use crate::output::{CsvLogging, SbpLogging}; use crate::types::{ClientSender, FlowControl, RealtimeDelay, SharedState}; use crate::utils::{refresh_loggingbar, refresh_navbar}; @@ -112,6 +109,201 @@ fn handle_cli(opt: CliOptions, connection_state: &ConnectionState, shared_state: (*shared_data).logging_bar.sbp_logging = SbpLogging::from_str(&sbp_log.to_string()).expect(CONVERT_TO_STR_FAILURE); } + log::logger().flush(); +} + +fn backend_recv_thread( + connection_state: ConnectionState, + client_send: ClientSender, + server_recv: mpsc::Receiver>, + shared_state: SharedState, +) { + thread::spawn(move || { + let client_send_clone = client_send.clone(); + loop { + log::logger().flush(); + let buf = server_recv.recv(); + if let Ok(buf) = buf { + let mut buf_reader = BufReader::new(Cursor::new(buf)); + let message_reader = serialize::read_message( + &mut buf_reader, + ::capnp::message::ReaderOptions::new(), + ) + .unwrap(); + let message = message_reader + .get_root::() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let message = match message.which() { + Ok(msg) => msg, + Err(e) => { + error!("error reading message: {}", e); + continue; + } + }; + match message { + m::message::ConnectRequest(Ok(conn_req)) => { + let request = conn_req + .get_request() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let request = request + .get_as::() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let request = match request.which() { + Ok(msg) => msg, + Err(e) => { + error!("error reading message: {}", e); + continue; + } + }; + let shared_state_clone = shared_state.clone(); + match request { + m::message::SerialRefreshRequest(Ok(_)) => { + refresh_navbar(&mut client_send_clone.clone(), shared_state_clone); + } + m::message::DisconnectRequest(Ok(_)) => { + connection_state.disconnect(client_send_clone.clone()); + } + m::message::FileRequest(Ok(req)) => { + let filename = req + .get_filename() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let filename = filename.to_string(); + connection_state.connect_to_file( + filename, + RealtimeDelay::On, + /*close_when_done*/ false, + ); + } + m::message::PauseRequest(Ok(_)) => { + if shared_state_clone.is_paused() { + shared_state_clone.set_paused(false); + } else { + shared_state_clone.set_paused(true); + } + } + m::message::TcpRequest(Ok(req)) => { + let host = + req.get_host().expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let port = req.get_port(); + connection_state.connect_to_host(host.to_string(), port); + } + m::message::SerialRequest(Ok(req)) => { + let device = + req.get_device().expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let device = device.to_string(); + let baudrate = req.get_baudrate(); + let flow = req.get_flow_control().unwrap(); + let flow = FlowControl::from_str(flow).unwrap(); + connection_state.connect_to_serial(device, baudrate, flow); + } + _ => println!("err"), + } + } + m::message::TrackingSignalsStatusFront(Ok(cv_in)) => { + let check_visibility = cv_in + .get_tracking_signals_check_visibility() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let check_visibility: Vec = check_visibility + .iter() + .map(|x| String::from(x.unwrap())) + .collect(); + let shared_state_clone = shared_state.clone(); + { + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + (*shared_data).tracking_tab.signals_tab.check_visibility = + check_visibility; + } + } + m::message::LoggingBarFront(Ok(cv_in)) => { + let directory = cv_in + .get_directory() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + shared_state.set_logging_directory(PathBuf::from(directory)); + let shared_state_clone = shared_state.clone(); + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + (*shared_data).logging_bar.csv_logging = + CsvLogging::from(cv_in.get_csv_logging()); + let sbp_logging = cv_in + .get_sbp_logging() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + (*shared_data).logging_bar.sbp_logging = + SbpLogging::from_str(sbp_logging).expect(CONVERT_TO_STR_FAILURE); + } + m::message::LogLevelFront(Ok(cv_in)) => { + let shared_state_clone = shared_state.clone(); + let log_level = cv_in + .get_log_level() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let log_level = + LogLevel::from_str(log_level).expect(CONVERT_TO_STR_FAILURE); + info!("Log Level: {}", log_level); + shared_state_clone.set_log_level(log_level); + refresh_navbar(&mut client_send.clone(), shared_state.clone()); + } + m::message::SolutionVelocityStatusFront(Ok(cv_in)) => { + let unit = cv_in + .get_solution_velocity_unit() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + let shared_state_clone = shared_state.clone(); + { + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + (*shared_data).solution_tab.velocity_tab.unit = unit.to_string(); + } + } + m::message::SolutionPositionStatusUnitFront(Ok(cv_in)) => { + let shared_state_clone = shared_state.clone(); + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + let unit = cv_in + .get_solution_position_unit() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); + (*shared_data).solution_tab.position_tab.unit = unit.to_string(); + } + m::message::SolutionPositionStatusButtonFront(Ok(cv_in)) => { + let shared_state_clone = shared_state.clone(); + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + (*shared_data).solution_tab.position_tab.clear = + cv_in.get_solution_position_clear(); + (*shared_data).solution_tab.position_tab.pause = + cv_in.get_solution_position_pause(); + } + m::message::BaselinePlotStatusButtonFront(Ok(cv_in)) => { + let shared_state_clone = shared_state.clone(); + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + (*shared_data).baseline_tab.clear = cv_in.get_clear(); + (*shared_data).baseline_tab.pause = cv_in.get_pause(); + (*shared_data).baseline_tab.reset = cv_in.get_reset_filters(); + } + m::message::AdvancedSpectrumAnalyzerStatusFront(Ok(cv_in)) => { + let shared_state_clone = shared_state.clone(); + let mut shared_data = shared_state_clone + .lock() + .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); + (*shared_data).advanced_spectrum_analyzer_tab.channel_idx = + cv_in.get_channel(); + } + _ => { + error!("unknown message from front-end"); + } + } + } else { + break; + } + } + eprintln!("client recv loop shutdown"); + client_send_clone.connected.set(false); + }); } #[pymethods] @@ -155,7 +347,6 @@ impl Server { #[text_signature = "($self, /)"] pub fn start(&mut self) -> PyResult { - let opt = CliOptions::from_filtered_cli(); let (client_send_, client_recv) = mpsc::channel::>(); let (server_send, server_recv) = mpsc::channel::>(); let client_send = ClientSender::new(client_send_); @@ -164,210 +355,15 @@ impl Server { let server_endpoint = ServerEndpoint { server_send: Some(server_send), }; + setup_logging(client_send.clone()); + let opt = CliOptions::from_filtered_cli(); let shared_state = SharedState::new(); let connection_state = ConnectionState::new(client_send.clone(), shared_state.clone()); - - let logger = Logger::builder() - .buf_size(LOG_WRITER_BUFFER_MESSAGE_COUNT) - .formatter(splitable_log_formatter) - .writer(Box::new(LogPanelWriter::new(client_send.clone()))) - .build() - .unwrap(); - - log::set_boxed_logger(Box::new(logger)).expect("Failed to set logger"); - // Handle CLI Opts. handle_cli(opt, &connection_state, shared_state.clone()); refresh_navbar(&mut client_send.clone(), shared_state.clone()); refresh_loggingbar(&mut client_send.clone(), shared_state.clone()); - thread::spawn(move || { - let client_send_clone = client_send.clone(); - loop { - let buf = server_recv.recv(); - if let Ok(buf) = buf { - let mut buf_reader = BufReader::new(Cursor::new(buf)); - let message_reader = serialize::read_message( - &mut buf_reader, - ::capnp::message::ReaderOptions::new(), - ) - .unwrap(); - let message = message_reader - .get_root::() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let message = match message.which() { - Ok(msg) => msg, - Err(e) => { - error!("error reading message: {}", e); - continue; - } - }; - match message { - m::message::ConnectRequest(Ok(conn_req)) => { - let request = conn_req - .get_request() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let request = request - .get_as::() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let request = match request.which() { - Ok(msg) => msg, - Err(e) => { - error!("error reading message: {}", e); - continue; - } - }; - let shared_state_clone = shared_state.clone(); - match request { - m::message::SerialRefreshRequest(Ok(_)) => { - refresh_navbar( - &mut client_send_clone.clone(), - shared_state_clone, - ); - } - m::message::DisconnectRequest(Ok(_)) => { - connection_state.disconnect(client_send_clone.clone()); - } - m::message::FileRequest(Ok(req)) => { - let filename = req - .get_filename() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let filename = filename.to_string(); - connection_state.connect_to_file( - filename, - RealtimeDelay::On, - /*close_when_done*/ false, - ); - } - m::message::PauseRequest(Ok(_)) => { - if shared_state_clone.is_paused() { - shared_state_clone.set_paused(false); - } else { - shared_state_clone.set_paused(true); - } - } - m::message::TcpRequest(Ok(req)) => { - let host = - req.get_host().expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let port = req.get_port(); - connection_state.connect_to_host(host.to_string(), port); - } - m::message::SerialRequest(Ok(req)) => { - let device = req - .get_device() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let device = device.to_string(); - let baudrate = req.get_baudrate(); - let flow = req.get_flow_control().unwrap(); - let flow = FlowControl::from_str(flow).unwrap(); - connection_state.connect_to_serial(device, baudrate, flow); - } - _ => println!("err"), - } - } - m::message::TrackingSignalsStatusFront(Ok(cv_in)) => { - let check_visibility = cv_in - .get_tracking_signals_check_visibility() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let check_visibility: Vec = check_visibility - .iter() - .map(|x| String::from(x.unwrap())) - .collect(); - let shared_state_clone = shared_state.clone(); - { - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - (*shared_data).tracking_tab.signals_tab.check_visibility = - check_visibility; - } - } - m::message::LoggingBarFront(Ok(cv_in)) => { - let directory = cv_in - .get_directory() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - shared_state.set_logging_directory(PathBuf::from(directory)); - let shared_state_clone = shared_state.clone(); - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - (*shared_data).logging_bar.csv_logging = - CsvLogging::from(cv_in.get_csv_logging()); - let sbp_logging = cv_in - .get_sbp_logging() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - (*shared_data).logging_bar.sbp_logging = - SbpLogging::from_str(sbp_logging).expect(CONVERT_TO_STR_FAILURE); - } - m::message::LogLevelFront(Ok(cv_in)) => { - let shared_state_clone = shared_state.clone(); - let log_level = cv_in - .get_log_level() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let log_level = - LogLevel::from_str(log_level).expect(CONVERT_TO_STR_FAILURE); - shared_state_clone.set_log_level(log_level); - refresh_navbar(&mut client_send.clone(), shared_state.clone()); - } - m::message::SolutionVelocityStatusFront(Ok(cv_in)) => { - let unit = cv_in - .get_solution_velocity_unit() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - let shared_state_clone = shared_state.clone(); - { - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - (*shared_data).solution_tab.velocity_tab.unit = unit.to_string(); - } - } - m::message::SolutionPositionStatusUnitFront(Ok(cv_in)) => { - let shared_state_clone = shared_state.clone(); - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - let unit = cv_in - .get_solution_position_unit() - .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE); - (*shared_data).solution_tab.position_tab.unit = unit.to_string(); - } - m::message::SolutionPositionStatusButtonFront(Ok(cv_in)) => { - let shared_state_clone = shared_state.clone(); - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - (*shared_data).solution_tab.position_tab.clear = - cv_in.get_solution_position_clear(); - (*shared_data).solution_tab.position_tab.pause = - cv_in.get_solution_position_pause(); - } - m::message::BaselinePlotStatusButtonFront(Ok(cv_in)) => { - let shared_state_clone = shared_state.clone(); - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - (*shared_data).baseline_tab.clear = cv_in.get_clear(); - (*shared_data).baseline_tab.pause = cv_in.get_pause(); - (*shared_data).baseline_tab.reset = cv_in.get_reset_filters(); - } - m::message::AdvancedSpectrumAnalyzerStatusFront(Ok(cv_in)) => { - let shared_state_clone = shared_state.clone(); - let mut shared_data = shared_state_clone - .lock() - .expect(SHARED_STATE_LOCK_MUTEX_FAILURE); - (*shared_data).advanced_spectrum_analyzer_tab.channel_idx = - cv_in.get_channel(); - } - _ => { - error!("unknown message from front-end"); - } - } - } else { - break; - } - } - eprintln!("client recv loop shutdown"); - client_send_clone.connected.set(false); - }); + backend_recv_thread(connection_state, client_send, server_recv, shared_state); Ok(server_endpoint) } } diff --git a/resources/Constants/Constants.qml b/resources/Constants/Constants.qml index 91609fcc6..5b893e526 100644 --- a/resources/Constants/Constants.qml +++ b/resources/Constants/Constants.qml @@ -13,6 +13,7 @@ QtObject { readonly property real logPanelPreferredHeight: 100 readonly property real navBarPreferredHeight: 50 readonly property real statusBarPreferredHeight: 30 + property QtObject logPanel property QtObject statusBar property QtObject navBar property QtObject sideNavBar @@ -60,11 +61,12 @@ QtObject { } genericTable: QtObject { + readonly property int headerZOffset: 100 readonly property int padding: 2 readonly property int mouseAreaResizeWidth: 10 readonly property int cellHeight: 25 readonly property string cellHighlightedColor: "crimson" - readonly property string cellColor: "ghostwhite" + readonly property string cellColor: "white" readonly property string gradientColor: "gainsboro" readonly property string borderColor: "gainsboro" readonly property string fontFamily: "Roboto" @@ -237,6 +239,16 @@ QtObject { readonly property var colors: ["#0000FF", "#00B3FF", "#BF00BF", "#FFA500", "#000000", "#00FF00"] } + logPanel: QtObject { + readonly property int width: 220 + readonly property variant defaultColumnWidthRatios: [0.15, 0.1, 0.75] + readonly property int maxRows: 200 + readonly property int cellHeight: 20 + readonly property string timestampHeader: "Host Timestamp" + readonly property string levelHeader: "Log Level" + readonly property string msgHeader: "Message" + } + solutionTable: QtObject { readonly property int width: 220 readonly property int defaultColumnWidth: 80 diff --git a/resources/LogPanel.qml b/resources/LogPanel.qml index 13f856c25..332c40d66 100644 --- a/resources/LogPanel.qml +++ b/resources/LogPanel.qml @@ -1,40 +1,230 @@ import "./Constants" -import QtQuick 2.14 +import Qt.labs.qmlmodels 1.0 +import QtCharts 2.2 +import QtQuick 2.15 import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 import SwiftConsole 1.0 -Rectangle { +Item { + property var logEntries: [] + property variant columnWidths: [parent.width * Constants.logPanel.defaultColumnWidthRatios[0], parent.width * Constants.logPanel.defaultColumnWidthRatios[1], parent.width * Constants.logPanel.defaultColumnWidthRatios[2]] + property real mouse_x: 0 + property int selectedRow: -1 + property bool forceLayoutLock: false + + function syncColumnWidths() { + let column_width_sum = columnWidths[0] + columnWidths[1] + columnWidths[2]; + if (column_width_sum != tableView.width) { + let final_column_diff = tableView.width - column_width_sum; + columnWidths[2] += final_column_diff; + } + tableView.forceLayout(); + } + + width: parent.width + height: parent.height + LogPanelData { id: logPanelData } - Text { - id: innerText + Rectangle { + anchors.fill: parent - text: "" - font.pointSize: Constants.largePointSize - padding: 5 - } + TextEdit { + id: textEdit - Timer { - interval: Globals.currentRefreshRate - running: true - repeat: true - onTriggered: { - if (innerText.text.length > 32000) { - innerText.text = "Overflowed"; - logPanelData.entries = []; - return ; + visible: false + } + + Shortcut { + sequence: StandardKey.Copy + onActivated: { + if (selectedRow != -1) { + textEdit.text = JSON.stringify(tableView.model.getRow(selectedRow)); + textEdit.selectAll(); + textEdit.copy(); + selectedRow = -1; + } + } + } + + HorizontalHeaderView { + id: horizontalHeader + + interactive: false + syncView: tableView + anchors.top: parent.top + z: Constants.genericTable.headerZOffset + + delegate: Rectangle { + implicitWidth: columnWidths[index] + implicitHeight: Constants.genericTable.cellHeight + border.color: Constants.genericTable.borderColor + + Text { + width: parent.width + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: tableView.model.columns[index].display + elide: Text.ElideRight + clip: true + font.family: Constants.genericTable.fontFamily + } + + MouseArea { + width: Constants.genericTable.mouseAreaResizeWidth + height: parent.height + anchors.right: parent.right + cursorShape: Qt.SizeHorCursor + onPressed: { + mouse_x = mouseX; + forceLayoutLock = true; + } + onPositionChanged: { + if (pressed) { + if (index == 2) + return ; + + var delta_x = (mouseX - mouse_x); + columnWidths[index] += delta_x; + syncColumnWidths(); + } + } + onReleased: { + forceLayoutLock = false; + } + } + + gradient: Gradient { + GradientStop { + position: 0 + color: Constants.genericTable.cellColor + } + + GradientStop { + position: 1 + color: Constants.genericTable.gradientColor + } + + } + + } + + } + + TableView { + id: tableView + + columnSpacing: -1 + rowSpacing: -1 + columnWidthProvider: function(column) { + return columnWidths[column]; + } + reuseItems: true + boundsBehavior: Flickable.StopAtBounds + anchors.top: horizontalHeader.bottom + width: parent.width + height: parent.height - horizontalHeader.height + + ScrollBar.horizontal: ScrollBar { } - log_panel_model.fill_data(logPanelData); - let newText = ''; - for (const entry of logPanelData.entries) { - newText = entry + '\n' + newText; + + ScrollBar.vertical: ScrollBar { + } + + model: TableModel { + id: tableModel + + Component.onCompleted: { + let row_init = { + }; + row_init[Constants.logPanel.timestampHeader] = ""; + row_init[Constants.logPanel.levelHeader] = ""; + row_init[Constants.logPanel.msgHeader] = ""; + tableView.model.setRow(0, row_init); + } + rows: [] + + TableModelColumn { + display: Constants.logPanel.timestampHeader + } + + TableModelColumn { + display: Constants.logPanel.levelHeader + } + + TableModelColumn { + display: Constants.logPanel.msgHeader + } + + } + + delegate: Rectangle { + implicitHeight: Constants.logPanel.cellHeight + implicitWidth: tableView.columnWidthProvider(column) + color: row == selectedRow ? Constants.genericTable.cellHighlightedColor : Constants.genericTable.cellColor + + Text { + width: parent.width + horizontalAlignment: Text.AlignLeft + clip: true + font.family: Constants.genericTable.fontFamily + font.pointSize: Constants.largePointSize + text: model.display + elide: Text.ElideRight + padding: Constants.genericTable.padding + } + + MouseArea { + width: parent.width + height: parent.height + anchors.centerIn: parent + onPressed: { + if (selectedRow == row) + selectedRow = -1; + else + selectedRow = row; + } + } + + } + + } + + Timer { + interval: Globals.currentRefreshRate + running: true + repeat: true + onTriggered: { + log_panel_model.fill_data(logPanelData); + if (!logPanelData.entries.length) + return ; + + if (forceLayoutLock) + return ; + + for (var idx in logPanelData.entries) { + var new_row = { + }; + new_row[Constants.logPanel.timestampHeader] = logPanelData.entries[idx].timestamp; + new_row[Constants.logPanel.levelHeader] = logPanelData.entries[idx].level; + new_row[Constants.logPanel.msgHeader] = logPanelData.entries[idx].msg; + logEntries.unshift(new_row); + } + logEntries = logEntries.slice(0, Constants.logPanel.maxRows); + for (var idx in logEntries) { + tableView.model.setRow(idx, logEntries[idx]); + } + if (logPanelData.entries.length && selectedRow != -1) + selectedRow += logPanelData.entries.length; + + logPanelData.entries = []; + tableView.forceLayout(); } - logPanelData.entries = []; - innerText.text = newText + innerText.text; } + } } diff --git a/resources/SolutionTabComponents/SolutionTable.qml b/resources/SolutionTabComponents/SolutionTable.qml index 791b6a7e7..2365bb497 100644 --- a/resources/SolutionTabComponents/SolutionTable.qml +++ b/resources/SolutionTabComponents/SolutionTable.qml @@ -1,6 +1,5 @@ import "../Constants" import Qt.labs.qmlmodels 1.0 -import Qt.labs.qmlmodels 1.0 import QtCharts 2.2 import QtQuick 2.15 import QtQuick.Controls 2.15 @@ -49,6 +48,7 @@ Item { interactive: false syncView: tableView + z: Constants.genericTable.headerZOffset delegate: Rectangle { implicitWidth: columnWidths[index] diff --git a/resources/view.qml b/resources/view.qml index 4124955cf..088ee9969 100644 --- a/resources/view.qml +++ b/resources/view.qml @@ -60,15 +60,9 @@ ApplicationWindow { Layout.rightMargin: Constants.margins } - Rectangle { - id: consoleLog - + LogPanel { SplitView.fillWidth: true SplitView.preferredHeight: Constants.logPanelPreferredHeight - - LogPanel { - } - } } diff --git a/src/main/python/log_panel.py b/src/main/python/log_panel.py index 5c63e0d02..59c443940 100644 --- a/src/main/python/log_panel.py +++ b/src/main/python/log_panel.py @@ -1,6 +1,8 @@ -"""Nav Bar QObjects. +"""Log Panel QObjects. """ +import json + from typing import Dict, List, Any from PySide2.QtCore import Property, QMutex, QObject, Slot @@ -15,19 +17,19 @@ class LogPanelData(QObject): - _entries: List[str] = [] + _entries: List[Dict[str, str]] = [] - def get_entries(self) -> List[str]: + def get_entries(self) -> List[Dict[str, str]]: """Getter for _entries.""" return self._entries - def set_entries(self, entries: List[str]) -> None: + def set_entries(self, entries: List[Dict[str, str]]) -> None: """Setter for _entries.""" self._entries = entries entries = Property(QTKeys.QVARIANTLIST, get_entries, set_entries) # type: ignore - def append_entries(self, entries: List[str]) -> None: + def append_entries(self, entries: List[Dict[str, str]]) -> None: self._entries += entries @@ -37,7 +39,10 @@ def fill_data(self, cp: LogPanelData) -> LogPanelData: # pylint:disable=no-self # Avoid locking so that message processor has priority to lock if LOG_PANEL[Keys.ENTRIES]: if log_panel_lock.try_lock(): - cp.append_entries(LOG_PANEL[Keys.ENTRIES]) + entries = [] + for entry in LOG_PANEL[Keys.ENTRIES]: + entries.append(json.loads(entry)) + cp.append_entries(entries) LOG_PANEL[Keys.ENTRIES][:] = [] log_panel_lock.unlock() return cp