Add exclusions
This commit is contained in:
parent
49a74d552b
commit
d62822640c
|
@ -28,7 +28,7 @@ set(IMGUI_LIBS SDL2 Freetype::Freetype)
|
||||||
|
|
||||||
set(INCLUDES imgui imgui/backends imgui/misc/cpp imgui/freetype /usr/include/SDL2)
|
set(INCLUDES imgui imgui/backends imgui/misc/cpp imgui/freetype /usr/include/SDL2)
|
||||||
set(SOURCES main.cpp event_loop.cpp logcat_thread.cpp logcat_entry.cpp log.cpp config.cpp filters.cpp misc.cpp pcre2_wrapper.cpp
|
set(SOURCES main.cpp event_loop.cpp logcat_thread.cpp logcat_entry.cpp log.cpp config.cpp filters.cpp misc.cpp pcre2_wrapper.cpp
|
||||||
group_panel.cpp windows/logs.cpp windows/settings.cpp windows/filters.cpp windows/main.cpp)
|
group_panel.cpp fragments/filters.cpp windows/logs.cpp windows/settings.cpp windows/filters.cpp windows/exclusions.cpp windows/main.cpp)
|
||||||
set(IMGUI_SOURCES imgui/imgui.cpp imgui/imgui_draw.cpp imgui/imgui_widgets.cpp imgui/imgui_tables.cpp
|
set(IMGUI_SOURCES imgui/imgui.cpp imgui/imgui_draw.cpp imgui/imgui_widgets.cpp imgui/imgui_tables.cpp
|
||||||
imgui/misc/cpp/imgui_stdlib.cpp imgui/misc/freetype/imgui_freetype.cpp
|
imgui/misc/cpp/imgui_stdlib.cpp imgui/misc/freetype/imgui_freetype.cpp
|
||||||
imgui/backends/imgui_impl_sdl.cpp imgui/backends/imgui_impl_opengl3.cpp)
|
imgui/backends/imgui_impl_sdl.cpp imgui/backends/imgui_impl_opengl3.cpp)
|
||||||
|
|
|
@ -71,6 +71,7 @@ void from_json(const nlohmann::json& j, Config& config) {
|
||||||
j.at("normal_font_size").get_to(config.normal_font_size);
|
j.at("normal_font_size").get_to(config.normal_font_size);
|
||||||
j.at("monospace_font_size").get_to(config.monospace_font_size);
|
j.at("monospace_font_size").get_to(config.monospace_font_size);
|
||||||
j.at("filters").get_to(config.filters);
|
j.at("filters").get_to(config.filters);
|
||||||
|
j.at("exclusions").get_to(config.exclusions);
|
||||||
}
|
}
|
||||||
|
|
||||||
Config load_config() {
|
Config load_config() {
|
||||||
|
@ -90,6 +91,7 @@ void to_json(nlohmann::json& j, const Config& config) {
|
||||||
j["normal_font_size"] = config.normal_font_size;
|
j["normal_font_size"] = config.normal_font_size;
|
||||||
j["monospace_font_size"] = config.monospace_font_size;
|
j["monospace_font_size"] = config.monospace_font_size;
|
||||||
j["filters"] = config.filters;
|
j["filters"] = config.filters;
|
||||||
|
j["exclusions"] = config.exclusions;
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_config(const Config& config) {
|
void write_config(const Config& config) {
|
||||||
|
|
1
config.h
1
config.h
|
@ -10,6 +10,7 @@ struct Config {
|
||||||
float monospace_font_size = 13.0f;
|
float monospace_font_size = 13.0f;
|
||||||
|
|
||||||
Filters filters;
|
Filters filters;
|
||||||
|
Filters exclusions;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string get_config_folder();
|
std::string get_config_folder();
|
||||||
|
|
|
@ -7,13 +7,14 @@
|
||||||
|
|
||||||
#include "windows/logs.h"
|
#include "windows/logs.h"
|
||||||
#include "windows/filters.h"
|
#include "windows/filters.h"
|
||||||
|
#include "windows/exclusions.h"
|
||||||
#include "windows/settings.h"
|
#include "windows/settings.h"
|
||||||
#include "windows/main.h"
|
#include "windows/main.h"
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
#include "windows/debug.h"
|
#include "windows/debug.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static inline void check_for_logcat_items(LogcatThread& logcat_thread, const Filters& filters,
|
static inline void check_for_logcat_items(LogcatThread& logcat_thread, const Config& active_config,
|
||||||
std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets) {
|
std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets) {
|
||||||
LogcatThreadItem* logcat_thread_item;
|
LogcatThreadItem* logcat_thread_item;
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ static inline void check_for_logcat_items(LogcatThread& logcat_thread, const Fil
|
||||||
log(std::move(std::get<LogEntry>(*logcat_thread_item)), false);
|
log(std::move(std::get<LogEntry>(*logcat_thread_item)), false);
|
||||||
} else if (std::holds_alternative<LogcatEntry>(*logcat_thread_item)) {
|
} else if (std::holds_alternative<LogcatEntry>(*logcat_thread_item)) {
|
||||||
logcat_entries.push_back(std::move(std::get<LogcatEntry>(*logcat_thread_item)));
|
logcat_entries.push_back(std::move(std::get<LogcatEntry>(*logcat_thread_item)));
|
||||||
if (matches(filters, logcat_entries.back())) {
|
if (matches(logcat_entries.back(), active_config.filters, active_config.exclusions)) {
|
||||||
filtered_logcat_entry_offsets.push_back(logcat_entries.size() - 1);
|
filtered_logcat_entry_offsets.push_back(logcat_entries.size() - 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,12 +38,13 @@ void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& log
|
||||||
static Config inactive_config;
|
static Config inactive_config;
|
||||||
static bool show_settings_window = false;
|
static bool show_settings_window = false;
|
||||||
static bool show_filters_window = false;
|
static bool show_filters_window = false;
|
||||||
|
static bool show_exclusions_window = false;
|
||||||
static bool show_logs_window = false;
|
static bool show_logs_window = false;
|
||||||
static size_t log_entries_read = 0;
|
static size_t log_entries_read = 0;
|
||||||
static std::vector<LogcatEntry> logcat_entries;
|
static std::vector<LogcatEntry> logcat_entries;
|
||||||
static std::vector<size_t> filtered_logcat_entry_offsets;
|
static std::vector<size_t> filtered_logcat_entry_offsets;
|
||||||
|
|
||||||
check_for_logcat_items(logcat_thread, active_config.filters, logcat_entries, filtered_logcat_entry_offsets);
|
check_for_logcat_items(logcat_thread, active_config, logcat_entries, filtered_logcat_entry_offsets);
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
debug_window(logcat_thread);
|
debug_window(logcat_thread);
|
||||||
|
@ -55,6 +57,9 @@ void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& log
|
||||||
if (show_filters_window) {
|
if (show_filters_window) {
|
||||||
filters_window(active_config, inactive_config, logcat_entries, filtered_logcat_entry_offsets, &show_filters_window);
|
filters_window(active_config, inactive_config, logcat_entries, filtered_logcat_entry_offsets, &show_filters_window);
|
||||||
}
|
}
|
||||||
|
if (show_exclusions_window) {
|
||||||
|
exclusions_window(active_config, inactive_config, logcat_entries, filtered_logcat_entry_offsets, &show_exclusions_window);
|
||||||
|
}
|
||||||
|
|
||||||
if (show_logs_window) {
|
if (show_logs_window) {
|
||||||
bool autoscrolling = false;
|
bool autoscrolling = false;
|
||||||
|
@ -68,5 +73,5 @@ void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& log
|
||||||
main_window(log_entries_read == log_entries.size(), monospace_font,
|
main_window(log_entries_read == log_entries.size(), monospace_font,
|
||||||
logcat_entries, filtered_logcat_entry_offsets,
|
logcat_entries, filtered_logcat_entry_offsets,
|
||||||
active_config, inactive_config,
|
active_config, inactive_config,
|
||||||
&show_settings_window, &show_filters_window, &show_logs_window, run_event_loop);
|
&show_settings_window, &show_filters_window, &show_exclusions_window, &show_logs_window, run_event_loop);
|
||||||
}
|
}
|
||||||
|
|
14
filters.cpp
14
filters.cpp
|
@ -274,20 +274,22 @@ void copy_filters(Filters& filters, const Filters& other) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool matches(const Filters& filters, const LogcatEntry& entry) {
|
static bool matches(const LogcatEntry& entry, const Filters& filters, bool return_true_if_empty) {
|
||||||
bool ok_filter_exists = false;
|
for (const auto &[_, filter] : filters) {
|
||||||
|
|
||||||
for (const auto &[title, filter] : filters) {
|
|
||||||
if (filter->disabled() || filter->error()) {
|
if (filter->disabled() || filter->error()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ok_filter_exists = true;
|
|
||||||
if (filter->match(entry)) {
|
if (filter->match(entry)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return_true_if_empty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !ok_filter_exists;
|
return return_true_if_empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matches(const LogcatEntry& entry, const Filters& filters, const Filters& exclusions) {
|
||||||
|
return !matches(entry, exclusions, false) && matches(entry, filters, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ private:
|
||||||
|
|
||||||
typedef std::vector<std::pair<std::string, std::unique_ptr<Filter>>> Filters;
|
typedef std::vector<std::pair<std::string, std::unique_ptr<Filter>>> Filters;
|
||||||
void copy_filters(Filters& filters, const Filters& other);
|
void copy_filters(Filters& filters, const Filters& other);
|
||||||
bool matches(const Filters& filters, const LogcatEntry& entry);
|
bool matches(const LogcatEntry& entry, const Filters& filters, const Filters& exclusions);
|
||||||
|
|
||||||
void from_json(const nlohmann::json& j, FilterKey& key);
|
void from_json(const nlohmann::json& j, FilterKey& key);
|
||||||
void to_json(nlohmann::json& j, const FilterKey& key);
|
void to_json(nlohmann::json& j, const FilterKey& key);
|
||||||
|
|
|
@ -0,0 +1,269 @@
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <imgui_stdlib.h>
|
||||||
|
|
||||||
|
#include "../group_panel.h"
|
||||||
|
#include "../filters.h"
|
||||||
|
#include "../config.h"
|
||||||
|
#include "filters.h"
|
||||||
|
|
||||||
|
static inline void render_integer_filter(IntegerFilter* filter);
|
||||||
|
static inline void render_string_filter(StringFilter* filter);
|
||||||
|
static inline void render_buffer_filter(BufferFilter* filter);
|
||||||
|
static inline void render_priority_filter(PriorityFilter* filter);
|
||||||
|
static inline void render_group_filter(GroupFilter* filter);
|
||||||
|
static std::unique_ptr<Filter> render_add_filter_popup();
|
||||||
|
static void update_logcat_entries(const Config& active_config,
|
||||||
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets);
|
||||||
|
static void try_write_config(const Config& config);
|
||||||
|
|
||||||
|
static void render_filter(Filter* filter, std::string* title, bool* request_removal) {
|
||||||
|
ImGui::PushID(filter);
|
||||||
|
ImGui::BeginGroupPanel();
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::Text("Title:%s%s", title->empty() ? "" : " ", title->c_str());
|
||||||
|
ImGui::SameLine();
|
||||||
|
std::string change_title_id = std::string("changefiltertitle_") + std::to_string(reinterpret_cast<size_t>(filter));
|
||||||
|
if (ImGui::Button("Edit")) {
|
||||||
|
ImGui::OpenPopup(change_title_id.c_str());
|
||||||
|
}
|
||||||
|
if (ImGui::BeginPopup(change_title_id.c_str())) {
|
||||||
|
ImGui::InputText("##title", title);
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Button("Remove")) {
|
||||||
|
*request_removal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ImGui::SameLine();
|
||||||
|
bool disabled = filter->disabled();
|
||||||
|
if (ImGui::Checkbox("Disabled", &disabled)) {
|
||||||
|
filter->disabled(disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
StringFilter* sfilter = dynamic_cast<StringFilter*>(filter);
|
||||||
|
IntegerFilter* ifilter = !sfilter ? dynamic_cast<IntegerFilter*>(filter) : nullptr;
|
||||||
|
if (sfilter || ifilter) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox("Inverted", sfilter ? &sfilter->inverted : &ifilter->inverted);
|
||||||
|
}
|
||||||
|
if (sfilter) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox("Exact match", &sfilter->exact_match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter->error()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("(%s)", filter->error()->c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IntegerFilter* ifilter = dynamic_cast<IntegerFilter*>(filter)) {
|
||||||
|
render_integer_filter(ifilter);
|
||||||
|
} else if (StringFilter* sfilter = dynamic_cast<StringFilter*>(filter)) {
|
||||||
|
render_string_filter(sfilter);
|
||||||
|
} else if (BufferFilter* bfilter = dynamic_cast<BufferFilter*>(filter)) {
|
||||||
|
render_buffer_filter(bfilter);
|
||||||
|
} else if (PriorityFilter* pfilter = dynamic_cast<PriorityFilter*>(filter)) {
|
||||||
|
render_priority_filter(pfilter);
|
||||||
|
} else if (GroupFilter* gfilter = dynamic_cast<GroupFilter*>(filter)) {
|
||||||
|
render_group_filter(gfilter);
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted("An unknown filter, this probably shouldn't be mutated");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndGroupPanel();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void render_integer_filter(IntegerFilter* filter) {
|
||||||
|
const char* head;
|
||||||
|
switch (filter->key) {
|
||||||
|
case FilterKey::PID: head = "PID"; break;
|
||||||
|
case FilterKey::TID: head = "TID"; break;
|
||||||
|
default: head = "Something";
|
||||||
|
};
|
||||||
|
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::Text("%s is%s", head, filter->inverted ? " not" : "");
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::InputScalar("##int", ImGuiDataType_U64, &filter->other)) {
|
||||||
|
filter->updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void render_string_filter(StringFilter* filter) {
|
||||||
|
const char* head;
|
||||||
|
switch (filter->key) {
|
||||||
|
case FilterKey::User: head = "User"; break;
|
||||||
|
case FilterKey::Tag: head = "Tag"; break;
|
||||||
|
case FilterKey::Message: head = "Message"; break;
|
||||||
|
default: head = "Something";
|
||||||
|
};
|
||||||
|
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::Text("%s %s", head, filter->exact_match
|
||||||
|
? (filter->inverted ? "is not" : "is")
|
||||||
|
: (filter->inverted ? "doesn't have" : "has"));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::InputText("##str", &filter->other)) {
|
||||||
|
filter->updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void render_buffer_filter(BufferFilter* filter) {
|
||||||
|
auto update_if_needed = [&](bool updated) {
|
||||||
|
if (updated) {
|
||||||
|
filter->updated();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Buffer is:"); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Unknown", &filter->wanted, static_cast<unsigned int>(Buffer::Unknown))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Main", &filter->wanted, static_cast<unsigned int>(Buffer::Main))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("System", &filter->wanted, static_cast<unsigned int>(Buffer::System))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Radio", &filter->wanted, static_cast<unsigned int>(Buffer::Radio))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Events", &filter->wanted, static_cast<unsigned int>(Buffer::Events))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Crash", &filter->wanted, static_cast<unsigned int>(Buffer::Crash)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void render_priority_filter(PriorityFilter* filter) {
|
||||||
|
auto update_if_needed = [&](bool updated) {
|
||||||
|
if (updated) {
|
||||||
|
filter->updated();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Priority is:"); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Unknown", &filter->wanted, static_cast<unsigned int>(Priority::Unknown))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Verbose", &filter->wanted, static_cast<unsigned int>(Priority::Verbose))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Debug", &filter->wanted, static_cast<unsigned int>(Priority::Debug))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Info", &filter->wanted, static_cast<unsigned int>(Priority::Info))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Warning", &filter->wanted, static_cast<unsigned int>(Priority::Warn))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Error", &filter->wanted, static_cast<unsigned int>(Priority::Error))); ImGui::SameLine();
|
||||||
|
update_if_needed(ImGui::CheckboxFlags("Fatal", &filter->wanted, static_cast<unsigned int>(Priority::Fatal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void render_group_filter(GroupFilter* filter) {
|
||||||
|
int selected_type = static_cast<int>(filter->type);
|
||||||
|
if (ImGui::Combo("of these are true", &selected_type, "All\0Any\0One\0None\0")) {
|
||||||
|
filter->type = static_cast<GroupFilter::Type>(selected_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::vector<std::unique_ptr<Filter>>::iterator it = filter->filters.begin(); it != filter->filters.end();) {
|
||||||
|
bool removal_requested = false;
|
||||||
|
render_filter(it->get(), nullptr, &removal_requested);
|
||||||
|
if (removal_requested) {
|
||||||
|
filter->filters.erase(it);
|
||||||
|
filter->updated();
|
||||||
|
} else {
|
||||||
|
it++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string add_filter_id = std::string("addfilter_") + std::to_string(reinterpret_cast<size_t>(filter));
|
||||||
|
if (ImGui::Button("+ add filter")) {
|
||||||
|
ImGui::OpenPopup(add_filter_id.c_str());
|
||||||
|
}
|
||||||
|
if (ImGui::BeginPopup(add_filter_id.c_str())) {
|
||||||
|
std::unique_ptr<Filter> added_filter = render_add_filter_popup();
|
||||||
|
if (added_filter) {
|
||||||
|
filter->filters.push_back(std::move(added_filter));
|
||||||
|
filter->updated();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::unique_ptr<Filter> render_add_filter_popup() {
|
||||||
|
if (ImGui::Selectable("Buffer")) {
|
||||||
|
return std::make_unique<BufferFilter>(0, false);
|
||||||
|
} else if (ImGui::Selectable("User")) {
|
||||||
|
return std::make_unique<StringFilter>(FilterKey::User, "", false, true, false);
|
||||||
|
} else if (ImGui::Selectable("PID")) {
|
||||||
|
return std::make_unique<IntegerFilter>(FilterKey::PID, 0, false, false);
|
||||||
|
} else if (ImGui::Selectable("TID")) {
|
||||||
|
return std::make_unique<IntegerFilter>(FilterKey::TID, 0, false, false);
|
||||||
|
} else if (ImGui::Selectable("Priority")) {
|
||||||
|
return std::make_unique<PriorityFilter>(0, false);
|
||||||
|
} else if (ImGui::Selectable("Tag")) {
|
||||||
|
return std::make_unique<StringFilter>(FilterKey::Tag, "", false, true, false);
|
||||||
|
} else if (ImGui::Selectable("Message")) {
|
||||||
|
return std::make_unique<StringFilter>(FilterKey::Message, "", false, true, false);
|
||||||
|
} else if (ImGui::Selectable("Group of filters")) {
|
||||||
|
return std::make_unique<GroupFilter>(std::vector<std::unique_ptr<Filter>>(), GroupFilter::Type::All, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::unique_ptr<Filter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_logcat_entries(const Config& active_config,
|
||||||
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets) {
|
||||||
|
filtered_logcat_entry_offsets.clear();
|
||||||
|
for (size_t i=0; i < logcat_entries.size(); i++) {
|
||||||
|
if (matches(logcat_entries[i], active_config.filters, active_config.exclusions)) {
|
||||||
|
filtered_logcat_entry_offsets.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void try_write_config(const Config& config) {
|
||||||
|
try {
|
||||||
|
write_config(config);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
log(std::string("Failed to write config: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void filters_fragment(Config& active_config, Filters& active_filters, Filters& inactive_filters,
|
||||||
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
|
bool* p_open) {
|
||||||
|
ImGui::TextUnformatted("You can use regex for strings by prepending \"regex:\"");
|
||||||
|
|
||||||
|
for (Filters::iterator it = inactive_filters.begin(); it != inactive_filters.end();) {
|
||||||
|
bool removal_requested = false;
|
||||||
|
render_filter(it->second.get(), &it->first, &removal_requested);
|
||||||
|
if (removal_requested) {
|
||||||
|
inactive_filters.erase(it);
|
||||||
|
} else {
|
||||||
|
it++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Button("+ add filter")) {
|
||||||
|
ImGui::OpenPopup("addfilter_root");
|
||||||
|
}
|
||||||
|
if (ImGui::BeginPopup("addfilter_root")) {
|
||||||
|
std::unique_ptr<Filter> added_filter = render_add_filter_popup();
|
||||||
|
if (added_filter) {
|
||||||
|
inactive_filters.push_back(std::make_pair("", std::move(added_filter)));
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImVec2 button_size(4 * ImGui::GetFontSize(), 0);
|
||||||
|
if (ImGui::Button("OK", button_size)) {
|
||||||
|
active_filters = std::move(inactive_filters);
|
||||||
|
try_write_config(active_config);
|
||||||
|
update_logcat_entries(active_config, logcat_entries, filtered_logcat_entry_offsets);
|
||||||
|
*p_open = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel", button_size)) {
|
||||||
|
*p_open = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Apply", button_size)) {
|
||||||
|
copy_filters(active_filters, inactive_filters);
|
||||||
|
try_write_config(active_config);
|
||||||
|
update_logcat_entries(active_config, logcat_entries, filtered_logcat_entry_offsets);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "../config.h"
|
||||||
|
#include "../filters.h"
|
||||||
|
|
||||||
|
void filters_fragment(Config& active_config, Filters& active_filters, Filters& inactive_filters,
|
||||||
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
|
bool* p_open);
|
|
@ -0,0 +1,19 @@
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include "../fragments/filters.h"
|
||||||
|
#include "../filters.h"
|
||||||
|
#include "../config.h"
|
||||||
|
#include "exclusions.h"
|
||||||
|
|
||||||
|
void exclusions_window(Config& active_config, Config& inactive_config,
|
||||||
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
|
bool* p_open) {
|
||||||
|
if (!ImGui::Begin("Exclusions", p_open)) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filters_fragment(active_config, active_config.exclusions, inactive_config.exclusions, logcat_entries, filtered_logcat_entry_offsets, p_open);
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../logcat_entry.h"
|
||||||
|
#include "../config.h"
|
||||||
|
|
||||||
|
void exclusions_window(Config& active_config, Config& inactive_config,
|
||||||
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
|
bool* p_open);
|
|
@ -1,227 +1,10 @@
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_stdlib.h>
|
|
||||||
|
|
||||||
#include "../group_panel.h"
|
#include "../fragments/filters.h"
|
||||||
#include "../filters.h"
|
#include "../filters.h"
|
||||||
#include "../config.h"
|
#include "../config.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
|
|
||||||
static inline void render_integer_filter(IntegerFilter* filter);
|
|
||||||
static inline void render_string_filter(StringFilter* filter);
|
|
||||||
static inline void render_buffer_filter(BufferFilter* filter);
|
|
||||||
static inline void render_priority_filter(PriorityFilter* filter);
|
|
||||||
static inline void render_group_filter(GroupFilter* filter);
|
|
||||||
static std::unique_ptr<Filter> render_add_filter_popup();
|
|
||||||
static void update_logcat_entries(const Filters& filters,
|
|
||||||
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets);
|
|
||||||
static void try_write_config(const Config& config);
|
|
||||||
|
|
||||||
static void render_filter(Filter* filter, std::string* title, bool* request_removal) {
|
|
||||||
ImGui::PushID(filter);
|
|
||||||
ImGui::BeginGroupPanel();
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::Text("Title:%s%s", title->empty() ? "" : " ", title->c_str());
|
|
||||||
ImGui::SameLine();
|
|
||||||
std::string change_title_id = std::string("changefiltertitle_") + std::to_string(reinterpret_cast<size_t>(filter));
|
|
||||||
if (ImGui::Button("Edit")) {
|
|
||||||
ImGui::OpenPopup(change_title_id.c_str());
|
|
||||||
}
|
|
||||||
if (ImGui::BeginPopup(change_title_id.c_str())) {
|
|
||||||
ImGui::InputText("##title", title);
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::Button("Remove")) {
|
|
||||||
*request_removal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
|
||||||
bool disabled = filter->disabled();
|
|
||||||
if (ImGui::Checkbox("Disabled", &disabled)) {
|
|
||||||
filter->disabled(disabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
StringFilter* sfilter = dynamic_cast<StringFilter*>(filter);
|
|
||||||
IntegerFilter* ifilter = !sfilter ? dynamic_cast<IntegerFilter*>(filter) : nullptr;
|
|
||||||
if (sfilter || ifilter) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Checkbox("Inverted", sfilter ? &sfilter->inverted : &ifilter->inverted);
|
|
||||||
}
|
|
||||||
if (sfilter) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Checkbox("Exact match", &sfilter->exact_match);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter->error()) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::Text("(%s)", filter->error()->c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IntegerFilter* ifilter = dynamic_cast<IntegerFilter*>(filter)) {
|
|
||||||
render_integer_filter(ifilter);
|
|
||||||
} else if (StringFilter* sfilter = dynamic_cast<StringFilter*>(filter)) {
|
|
||||||
render_string_filter(sfilter);
|
|
||||||
} else if (BufferFilter* bfilter = dynamic_cast<BufferFilter*>(filter)) {
|
|
||||||
render_buffer_filter(bfilter);
|
|
||||||
} else if (PriorityFilter* pfilter = dynamic_cast<PriorityFilter*>(filter)) {
|
|
||||||
render_priority_filter(pfilter);
|
|
||||||
} else if (GroupFilter* gfilter = dynamic_cast<GroupFilter*>(filter)) {
|
|
||||||
render_group_filter(gfilter);
|
|
||||||
} else {
|
|
||||||
ImGui::TextUnformatted("An unknown filter, this probably shouldn't be mutated");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndGroupPanel();
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void render_integer_filter(IntegerFilter* filter) {
|
|
||||||
const char* head;
|
|
||||||
switch (filter->key) {
|
|
||||||
case FilterKey::PID: head = "PID"; break;
|
|
||||||
case FilterKey::TID: head = "TID"; break;
|
|
||||||
default: head = "Something";
|
|
||||||
};
|
|
||||||
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::Text("%s is%s", head, filter->inverted ? " not" : "");
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::InputScalar("##int", ImGuiDataType_U64, &filter->other)) {
|
|
||||||
filter->updated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void render_string_filter(StringFilter* filter) {
|
|
||||||
const char* head;
|
|
||||||
switch (filter->key) {
|
|
||||||
case FilterKey::User: head = "User"; break;
|
|
||||||
case FilterKey::Tag: head = "Tag"; break;
|
|
||||||
case FilterKey::Message: head = "Message"; break;
|
|
||||||
default: head = "Something";
|
|
||||||
};
|
|
||||||
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::Text("%s %s", head, filter->exact_match
|
|
||||||
? (filter->inverted ? "is not" : "is")
|
|
||||||
: (filter->inverted ? "doesn't have" : "has"));
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::InputText("##str", &filter->other)) {
|
|
||||||
filter->updated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void render_buffer_filter(BufferFilter* filter) {
|
|
||||||
auto update_if_needed = [&](bool updated) {
|
|
||||||
if (updated) {
|
|
||||||
filter->updated();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::TextUnformatted("Buffer is:"); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Unknown", &filter->wanted, static_cast<unsigned int>(Buffer::Unknown))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Main", &filter->wanted, static_cast<unsigned int>(Buffer::Main))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("System", &filter->wanted, static_cast<unsigned int>(Buffer::System))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Radio", &filter->wanted, static_cast<unsigned int>(Buffer::Radio))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Events", &filter->wanted, static_cast<unsigned int>(Buffer::Events))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Crash", &filter->wanted, static_cast<unsigned int>(Buffer::Crash)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void render_priority_filter(PriorityFilter* filter) {
|
|
||||||
auto update_if_needed = [&](bool updated) {
|
|
||||||
if (updated) {
|
|
||||||
filter->updated();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::TextUnformatted("Priority is:"); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Unknown", &filter->wanted, static_cast<unsigned int>(Priority::Unknown))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Verbose", &filter->wanted, static_cast<unsigned int>(Priority::Verbose))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Debug", &filter->wanted, static_cast<unsigned int>(Priority::Debug))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Info", &filter->wanted, static_cast<unsigned int>(Priority::Info))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Warning", &filter->wanted, static_cast<unsigned int>(Priority::Warn))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Error", &filter->wanted, static_cast<unsigned int>(Priority::Error))); ImGui::SameLine();
|
|
||||||
update_if_needed(ImGui::CheckboxFlags("Fatal", &filter->wanted, static_cast<unsigned int>(Priority::Fatal)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void render_group_filter(GroupFilter* filter) {
|
|
||||||
int selected_type = static_cast<int>(filter->type);
|
|
||||||
if (ImGui::Combo("of these are true", &selected_type, "All\0Any\0One\0None\0")) {
|
|
||||||
filter->type = static_cast<GroupFilter::Type>(selected_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (std::vector<std::unique_ptr<Filter>>::iterator it = filter->filters.begin(); it != filter->filters.end();) {
|
|
||||||
bool removal_requested = false;
|
|
||||||
render_filter(it->get(), nullptr, &removal_requested);
|
|
||||||
if (removal_requested) {
|
|
||||||
filter->filters.erase(it);
|
|
||||||
filter->updated();
|
|
||||||
} else {
|
|
||||||
it++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string add_filter_id = std::string("addfilter_") + std::to_string(reinterpret_cast<size_t>(filter));
|
|
||||||
if (ImGui::Button("+ add filter")) {
|
|
||||||
ImGui::OpenPopup(add_filter_id.c_str());
|
|
||||||
}
|
|
||||||
if (ImGui::BeginPopup(add_filter_id.c_str())) {
|
|
||||||
std::unique_ptr<Filter> added_filter = render_add_filter_popup();
|
|
||||||
if (added_filter) {
|
|
||||||
filter->filters.push_back(std::move(added_filter));
|
|
||||||
filter->updated();
|
|
||||||
}
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::unique_ptr<Filter> render_add_filter_popup() {
|
|
||||||
if (ImGui::Selectable("Buffer")) {
|
|
||||||
return std::make_unique<BufferFilter>(0, false);
|
|
||||||
} else if (ImGui::Selectable("User")) {
|
|
||||||
return std::make_unique<StringFilter>(FilterKey::User, "", false, true, false);
|
|
||||||
} else if (ImGui::Selectable("PID")) {
|
|
||||||
return std::make_unique<IntegerFilter>(FilterKey::PID, 0, false, false);
|
|
||||||
} else if (ImGui::Selectable("TID")) {
|
|
||||||
return std::make_unique<IntegerFilter>(FilterKey::TID, 0, false, false);
|
|
||||||
} else if (ImGui::Selectable("Priority")) {
|
|
||||||
return std::make_unique<PriorityFilter>(0, false);
|
|
||||||
} else if (ImGui::Selectable("Tag")) {
|
|
||||||
return std::make_unique<StringFilter>(FilterKey::Tag, "", false, true, false);
|
|
||||||
} else if (ImGui::Selectable("Message")) {
|
|
||||||
return std::make_unique<StringFilter>(FilterKey::Message, "", false, true, false);
|
|
||||||
} else if (ImGui::Selectable("Group of filters")) {
|
|
||||||
return std::make_unique<GroupFilter>(std::vector<std::unique_ptr<Filter>>(), GroupFilter::Type::All, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::unique_ptr<Filter>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void update_logcat_entries(const Filters& filters,
|
|
||||||
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets) {
|
|
||||||
filtered_logcat_entry_offsets.clear();
|
|
||||||
for (size_t i=0; i < logcat_entries.size(); i++) {
|
|
||||||
if (matches(filters, logcat_entries[i])) {
|
|
||||||
filtered_logcat_entry_offsets.push_back(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void try_write_config(const Config& config) {
|
|
||||||
try {
|
|
||||||
write_config(config);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
log(std::string("Failed to write config: ") + e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void filters_window(Config& active_config, Config& inactive_config,
|
void filters_window(Config& active_config, Config& inactive_config,
|
||||||
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
const std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
bool* p_open) {
|
bool* p_open) {
|
||||||
|
@ -230,46 +13,7 @@ void filters_window(Config& active_config, Config& inactive_config,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextUnformatted("You can use regex for strings by prepending \"regex:\"");
|
filters_fragment(active_config, active_config.filters, inactive_config.filters, logcat_entries, filtered_logcat_entry_offsets, p_open);
|
||||||
|
|
||||||
for (Filters::iterator it = inactive_config.filters.begin(); it != inactive_config.filters.end();) {
|
|
||||||
bool removal_requested = false;
|
|
||||||
render_filter(it->second.get(), &it->first, &removal_requested);
|
|
||||||
if (removal_requested) {
|
|
||||||
inactive_config.filters.erase(it);
|
|
||||||
} else {
|
|
||||||
it++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::Button("+ add filter")) {
|
|
||||||
ImGui::OpenPopup("addfilter_root");
|
|
||||||
}
|
|
||||||
if (ImGui::BeginPopup("addfilter_root")) {
|
|
||||||
std::unique_ptr<Filter> added_filter = render_add_filter_popup();
|
|
||||||
if (added_filter) {
|
|
||||||
inactive_config.filters.push_back(std::make_pair("", std::move(added_filter)));
|
|
||||||
}
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Separator();
|
|
||||||
ImVec2 button_size(4 * ImGui::GetFontSize(), 0);
|
|
||||||
if (ImGui::Button("OK", button_size)) {
|
|
||||||
active_config.filters = std::move(inactive_config.filters);
|
|
||||||
try_write_config(active_config);
|
|
||||||
update_logcat_entries(active_config.filters, logcat_entries, filtered_logcat_entry_offsets);
|
|
||||||
*p_open = false;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::Button("Cancel", button_size)) {
|
|
||||||
*p_open = false;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::Button("Apply", button_size)) {
|
|
||||||
copy_filters(active_config.filters, inactive_config.filters);
|
|
||||||
try_write_config(active_config);
|
|
||||||
update_logcat_entries(active_config.filters, logcat_entries, filtered_logcat_entry_offsets);
|
|
||||||
}
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ static inline void render_table(ImFont* monospace_font, std::vector<LogcatEntry>
|
||||||
void main_window(bool latest_log_entries_read, ImFont* monospace_font,
|
void main_window(bool latest_log_entries_read, ImFont* monospace_font,
|
||||||
std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
const Config& active_config, Config& inactive_config,
|
const Config& active_config, Config& inactive_config,
|
||||||
bool* show_settings_window, bool* show_filters_window, bool* show_logs_window, bool* run_event_loop) {
|
bool* show_settings_window, bool* show_filters_window, bool* show_exclusions_window, bool* show_logs_window, bool* run_event_loop) {
|
||||||
|
|
||||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos);
|
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos);
|
||||||
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize);
|
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize);
|
||||||
|
@ -78,6 +78,12 @@ void main_window(bool latest_log_entries_read, ImFont* monospace_font,
|
||||||
*show_filters_window = true;
|
*show_filters_window = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Exclusions") && !*show_exclusions_window) {
|
||||||
|
copy_filters(inactive_config.exclusions, active_config.exclusions);
|
||||||
|
*show_exclusions_window = true;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
bool open_logs;
|
bool open_logs;
|
||||||
if (!latest_log_entries_read) {
|
if (!latest_log_entries_read) {
|
||||||
|
|
|
@ -9,4 +9,4 @@
|
||||||
void main_window(bool latest_log_entries_read, ImFont* monospace_font,
|
void main_window(bool latest_log_entries_read, ImFont* monospace_font,
|
||||||
std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
std::vector<LogcatEntry>& logcat_entries, std::vector<size_t>& filtered_logcat_entry_offsets,
|
||||||
const Config& active_config, Config& inactive_config,
|
const Config& active_config, Config& inactive_config,
|
||||||
bool* show_settings_window, bool* show_filters_window, bool* show_logs_window, bool* run_event_loop);
|
bool* show_settings_window, bool* show_filters_window, bool* show_exclusions_window, bool* show_logs_window, bool* run_event_loop);
|
||||||
|
|
Loading…
Reference in New Issue