diff --git a/CMakeLists.txt b/CMakeLists.txt index 7678193..eb5c9a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,8 +26,8 @@ set(LIBS -lpcre2-8 -lpcre2-posix SDL2 imgui) set(IMGUI_LIBS SDL2 Freetype::Freetype) 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 misc.cpp pcre2_wrapper.cpp - windows/logs.cpp windows/settings.cpp windows/main.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) 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/backends/imgui_impl_sdl.cpp imgui/backends/imgui_impl_opengl3.cpp) @@ -42,7 +42,8 @@ endif() set(FLAGS "${FLAGS} -DIMGUI_USER_CONFIG='\"../myimconfig.h\"'") # https://t.me/NightShadowsHangout/670691 -set(FLAGS "${FLAGS} -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-struct -fno-rtti -fPIC -Wconversion -Wno-unused-parameter -Wimplicit-fallthrough") +# https://t.me/NightShadowsHangout/688372 +set(FLAGS "${FLAGS} -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-struct -fPIC -Wconversion -Wno-unused-parameter -Wimplicit-fallthrough") # https://sourceforge.net/p/valgrind/mailman/valgrind-users/thread/Ygze8PzaQAYWlKDj%40wildebeest.org/ set(FLAGS "${FLAGS} -gdwarf-4") diff --git a/config.h b/config.h index 33d490e..0b8f545 100644 --- a/config.h +++ b/config.h @@ -12,9 +12,3 @@ std::string get_config_folder(); void create_config_folders_if_necessary(); Config load_config(); void write_config(const Config& config); - -constexpr bool operator!=(const Config& lhs, const Config& rhs) { - return lhs.logcat_command != rhs.logcat_command - || lhs.normal_font_size != rhs.normal_font_size - || lhs.monospace_font_size != rhs.monospace_font_size; -} diff --git a/event_loop.cpp b/event_loop.cpp index 93b8c80..a8d0eea 100644 --- a/event_loop.cpp +++ b/event_loop.cpp @@ -2,23 +2,29 @@ #include "log.h" #include "config.h" +#include "filters.h" #include "logcat_thread.h" #include "windows/logs.h" +#include "windows/filters.h" #include "windows/settings.h" #include "windows/main.h" #ifndef NDEBUG #include "windows/debug.h" #endif -static inline void check_for_logcat_items(LogcatThread& logcat_thread, std::vector& filtered_logcat_entries) { +static inline void check_for_logcat_items(LogcatThread& logcat_thread, const Filters& filters, + std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets) { LogcatThreadItem* logcat_thread_item; while ((logcat_thread_item = logcat_thread.atomic_ring_buffer.get())) { if (std::holds_alternative(*logcat_thread_item)) { log_raw(std::move(std::get(*logcat_thread_item)), false); } else if (std::holds_alternative(*logcat_thread_item)) { - filtered_logcat_entries.push_back(std::move(std::get(*logcat_thread_item))); + logcat_entries.push_back(std::move(std::get(*logcat_thread_item))); + if (matches(filters, logcat_entries.back())) { + filtered_logcat_entry_offsets.push_back(logcat_entries.size() - 1); + } } else { throw std::runtime_error("Cannot handle all possible logcat thread item variants"); } @@ -27,14 +33,17 @@ static inline void check_for_logcat_items(LogcatThread& logcat_thread, std::vect } -void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& logcat_thread, bool* run_event_loop) { +void event_loop(ImFont* monospace_font, Config& active_config, Filters& active_filters, LogcatThread& logcat_thread, bool* run_event_loop) { static Config inactive_config; + static Filters inactive_filters; static bool show_settings_window = false; + static bool show_filters_window = false; static bool show_logs_window = false; static size_t log_entries_read = 0; - static std::vector filtered_logcat_entries; + static std::vector logcat_entries; + static std::vector filtered_logcat_entry_offsets; - check_for_logcat_items(logcat_thread, filtered_logcat_entries); + check_for_logcat_items(logcat_thread, active_filters, logcat_entries, filtered_logcat_entry_offsets); #ifndef NDEBUG debug_window(logcat_thread); @@ -44,6 +53,10 @@ void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& log settings_window(active_config, inactive_config, &show_settings_window); } + if (show_filters_window) { + filters_window(active_filters, inactive_filters, logcat_entries, filtered_logcat_entry_offsets, &show_filters_window); + } + if (show_logs_window) { bool autoscrolling = false; logs_window(monospace_font, &autoscrolling, &show_logs_window); @@ -53,8 +66,9 @@ void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& log } // log_entries must not be mutated until the show logs button - main_window(log_entries_read == log_entries.size(), - monospace_font, filtered_logcat_entries, - active_config, inactive_config, &show_settings_window, - &show_logs_window, run_event_loop); + main_window(log_entries_read == log_entries.size(), monospace_font, + logcat_entries, filtered_logcat_entry_offsets, + active_config, inactive_config, + active_filters, inactive_filters, + &show_settings_window, &show_filters_window, &show_logs_window, run_event_loop); } diff --git a/event_loop.h b/event_loop.h index 683a7f3..2bbf0fd 100644 --- a/event_loop.h +++ b/event_loop.h @@ -3,6 +3,7 @@ #include #include "config.h" +#include "filters.h" #include "logcat_thread.h" -void event_loop(ImFont* monospace_font, Config& active_config, LogcatThread& logcat_thread, bool* run_event_loop); +void event_loop(ImFont* monospace_font, Config& active_config, Filters& active_filters, LogcatThread& logcat_thread, bool* run_event_loop); diff --git a/filters.cpp b/filters.cpp new file mode 100644 index 0000000..520cb70 --- /dev/null +++ b/filters.cpp @@ -0,0 +1,275 @@ +#include +#include +#include + +#include "log.h" +#include "filters.h" + +Filter::~Filter() { +} + + +IntegerFilter::IntegerFilter(FilterKey key_, size_t other_, bool inverted_, bool disabled) + : key(key_), other(other_), inverted(inverted_), _disabled(disabled) { + if (key_ != FilterKey::PID && key_ != FilterKey::TID) { + throw std::runtime_error("IntegerFilter is passed a non-integer key"); + } +} + +void IntegerFilter::updated() { +} + +bool IntegerFilter::disabled() const { + return this->_disabled; +} + +void IntegerFilter::disabled(bool disabled) { + this->_disabled = disabled; +} + +std::unique_ptr IntegerFilter::clone() const { + return std::make_unique(this->key, this->other, this->inverted, this->_disabled); +} + +const std::string* IntegerFilter::error() const { + return nullptr; +} + +bool IntegerFilter::match(const LogcatEntry& entry) const { + bool matched; + switch (this->key) { + case FilterKey::PID: matched = entry.pid == this->other; break; + case FilterKey::TID: matched = entry.tid == this->other; break; + default: throw std::runtime_error("IntegerFilter::match received unhandled key"); + }; + return !this->inverted ? matched : !matched; +} + + +StringFilter::StringFilter(FilterKey key_, std::string other_, bool inverted_, bool disabled) + : key(key_), other(std::move(other_)), inverted(inverted_), _disabled(disabled) { + if (key_ != FilterKey::User && key_ != FilterKey::Tag && key_ != FilterKey::Message) { + throw std::runtime_error("StringFilter is passed a non-string key"); + } + this->updated(); +} + +void StringFilter::updated() { + this->_regex.reset(); + this->_error.clear(); + + if (this->other.empty()) { + this->_error = "String must not be empty"; + return; + } + if (!this->other.starts_with("regex:")) { + return; + } + if (this->other.size() == 6) { + this->_error = "Regex must not be empty"; + return; + } + + try { + this->_regex.emplace(&this->other.c_str()[6]); + } catch (const std::exception& e) { + this->_error = e.what(); + } +} + +bool StringFilter::disabled() const { + return this->_disabled; +} + +void StringFilter::disabled(bool disabled) { + this->_disabled = disabled; +} + +std::unique_ptr StringFilter::clone() const { + return std::make_unique(this->key, this->other, this->inverted, this->_disabled); +} + +const std::string* StringFilter::error() const { + return !this->_error.empty() ? &this->_error : nullptr; +} + +bool StringFilter::match(const LogcatEntry& entry) const { + bool matched; + + if (this->error()) { + log(std::string("StringFilter::match called despite there being an error: ") + *this->error()); + return true; + } + if (!this->other.starts_with("regex:")) { + switch (this->key) { + case FilterKey::User: matched = entry.user.value_or("") == this->other; break; + case FilterKey::Tag: matched = entry.tag == this->other; break; + case FilterKey::Message: matched = entry.message == this->other; break; + default: throw std::runtime_error("StringFilter::match received unhandled key"); + } + return !this->inverted ? matched : !matched; + } + if (!this->_regex) { + log("StringFilter::match called with a regex despite there being no regex"); + return true; + } + + std::string str; + regmatch_t pmatch[1]; + switch (this->key) { + case FilterKey::User: str = entry.user.value_or(""); break; + case FilterKey::Tag: str = entry.tag; break; + case FilterKey::Message: str = entry.message; break; + default: throw std::runtime_error("StringFilter::match received unhandled key"); + } + pmatch[0].rm_so = 0; + pmatch[0].rm_eo = static_cast(str.size()); + + matched = this->_regex->match(str.data(), 1, pmatch, REG_STARTEND) + && pmatch[0].rm_so == 0 && static_cast(pmatch[0].rm_eo) == str.size(); + return !this->inverted ? matched : !matched; +} + + +BufferFilter::BufferFilter(unsigned int wanted_, bool disabled) : wanted(wanted_), _disabled(disabled) { +} + +void BufferFilter::updated() { +} + +bool BufferFilter::disabled() const { + return this->_disabled; +} + +void BufferFilter::disabled(bool disabled) { + this->_disabled = disabled; +} + +std::unique_ptr BufferFilter::clone() const { + return std::make_unique(this->wanted, this->_disabled); +} + +const std::string* BufferFilter::error() const { + return nullptr; +} + +bool BufferFilter::match(const LogcatEntry& entry) const { + return this->wanted & static_cast(entry.buffer) || this->wanted == 0; +} + + +PriorityFilter::PriorityFilter(unsigned int wanted_, bool disabled) : wanted(wanted_), _disabled(disabled) { +} + +void PriorityFilter::updated() { +} + +bool PriorityFilter::disabled() const { + return this->_disabled; +} + +void PriorityFilter::disabled(bool disabled) { + this->_disabled = disabled; +} + +std::unique_ptr PriorityFilter::clone() const { + return std::make_unique(this->wanted, this->_disabled); +} + +const std::string* PriorityFilter::error() const { + return nullptr; +} + +bool PriorityFilter::match(const LogcatEntry& entry) const { + return this->wanted & static_cast(entry.priority) || this->wanted == 0; +} + + +GroupFilter::GroupFilter(std::vector> filters_, GroupFilter::Type type_, bool disabled) + : filters(std::move(filters_)), type(type_), _disabled(disabled) { +} + +void GroupFilter::updated() { +} + +bool GroupFilter::disabled() const { + return this->_disabled; +} + +void GroupFilter::disabled(bool disabled) { + this->_disabled = disabled; +} + +std::unique_ptr GroupFilter::clone() const { + std::vector> new_filters; + new_filters.reserve(this->filters.size()); + + for (const std::unique_ptr& filter : this->filters) { + new_filters.push_back(filter->clone()); + } + + return std::make_unique(std::move(new_filters), this->type, this->_disabled); +} + +const std::string* GroupFilter::error() const { + return nullptr; +} + +bool GroupFilter::match(const LogcatEntry& entry) const { + bool had_matched = false; + + for (const std::unique_ptr& filter : this->filters) { + if (filter->disabled() || filter->error()) { + continue; + } + + bool matched = filter->match(entry); + if (this->type == GroupFilter::Type::Any && matched) { + return true; + } else if (this->type == GroupFilter::Type::None && matched) { + return false; + } else if (this->type == GroupFilter::Type::All && !matched) { + return false; + } else if (this->type == GroupFilter::Type::One) { + if (had_matched && matched) { + return false; + } + if (matched) { + had_matched = true; + } + } + } + + if (this->type == GroupFilter::Type::Any) { + return false; + } else if (this->type == GroupFilter::Type::One && !had_matched) { + return false; + } + return true; +} + + +void copy_filters(Filters& filters, const Filters& other) { + filters.clear(); + filters.reserve(other.size()); + + for (const auto &[title, filter] : other) { + filters.push_back(std::make_pair(title, filter->clone())); + } +} + +bool matches(const Filters& filters, const LogcatEntry& entry) { + bool ok_filter_exists = false; + + for (const auto &[title, filter] : filters) { + if (filter->disabled() || filter->error()) { + continue; + } + ok_filter_exists = true; + if (filter->match(entry)) { + return true; + } + } + + return !ok_filter_exists; +} diff --git a/filters.h b/filters.h new file mode 100644 index 0000000..ba34099 --- /dev/null +++ b/filters.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include + +#include "logcat_entry.h" +#include "pcre2_wrapper.h" + +enum class FilterKey { + Buffer, + User, + PID, + TID, + Priority, + Tag, + Message, +}; + +class Filter { +public: + // https://stackoverflow.com/a/10024812 + virtual ~Filter() = 0; + + virtual void updated() = 0; + virtual bool disabled() const = 0; + virtual void disabled(bool disabled) = 0; + // https://stackoverflow.com/a/65344294 + virtual std::unique_ptr clone() const = 0; + virtual const std::string* error() const = 0; + virtual bool match(const LogcatEntry& entry) const = 0; +}; + +class IntegerFilter : public Filter { +public: + IntegerFilter(FilterKey key_, size_t other_, bool inverted_ = false, bool disabled = false); + + void updated() override; + bool disabled() const override; + void disabled(bool disabled) override; + std::unique_ptr clone() const override; + const std::string* error() const override; + bool match(const LogcatEntry& entry) const override; + + FilterKey key; + size_t other; + bool inverted; + +private: + bool _disabled; +}; + +class StringFilter : public Filter { +public: + // https://stackoverflow.com/a/2173764 + StringFilter(const StringFilter&) = delete; + StringFilter& operator=(const StringFilter&) = delete; + + StringFilter(FilterKey key_, std::string other_, bool inverted_ = false, bool disabled = false); + + void updated() override; + bool disabled() const override; + void disabled(bool disabled) override; + std::unique_ptr clone() const override; + const std::string* error() const override; + bool match(const LogcatEntry& entry) const override; + + FilterKey key; + std::string other; + bool inverted; + +private: + std::string _error; + std::optional _regex; + bool _disabled; +}; + +class BufferFilter : public Filter { +public: + BufferFilter(unsigned int wanted_, bool disabled = false); + + void updated() override; + bool disabled() const override; + void disabled(bool disabled) override; + std::unique_ptr clone() const override; + const std::string* error() const override; + bool match(const LogcatEntry& entry) const override; + + unsigned int wanted; + +private: + bool _disabled; +}; + +class PriorityFilter : public Filter { +public: + PriorityFilter(unsigned int wanted_, bool disabled = false); + + void updated() override; + bool disabled() const override; + void disabled(bool disabled) override; + std::unique_ptr clone() const override; + const std::string* error() const override; + bool match(const LogcatEntry& entry) const override; + + unsigned int wanted; + +private: + bool _disabled; +}; + +class GroupFilter : public Filter { +public: + enum class Type { + All = 0, + Any = 1, + One = 2, + None = 3, + }; + + GroupFilter(std::vector> filters_, Type type_, bool disabled = false); + + void updated() override; + bool disabled() const override; + void disabled(bool disabled) override; + std::unique_ptr clone() const override; + const std::string* error() const override; + bool match(const LogcatEntry& entry) const override; + + std::vector> filters; + Type type; + +private: + bool _disabled; +}; + +typedef std::vector>> Filters; +void copy_filters(Filters& filters, const Filters& other); +bool matches(const Filters& filters, const LogcatEntry& entry); diff --git a/group_panel.cpp b/group_panel.cpp new file mode 100644 index 0000000..24c1dfb --- /dev/null +++ b/group_panel.cpp @@ -0,0 +1,97 @@ +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include "group_panel.h" + +// The following code is slightly modified public domain code from https://github.com/thedmd +// All modifications have an inline comment with "[CUSTOM]" +// Public domain claim from https://github.com/ocornut/imgui/issues/1496#issuecomment-1287772456 +// Code from https://github.com/ocornut/imgui/issues/1496#issuecomment-569892444 and +// some snippets from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353 + +void ImGui::BeginGroupPanel(const ImVec2& size) // [CUSTOM] +{ + ImGui::BeginGroup(); + + //auto cursorPos = ImGui::GetCursorScreenPos(); // [CUSTOM] + auto itemSpacing = ImGui::GetStyle().ItemSpacing; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + + auto frameHeight = ImGui::GetFrameHeight(); + ImGui::BeginGroup(); + + ImVec2 effectiveSize = size; + if (size.x < 0.0f) + effectiveSize.x = ImGui::GetContentRegionAvail().x; // [CUSTOM] + else + effectiveSize.x = size.x; + ImGui::Dummy(ImVec2(effectiveSize.x, 0.0f)); + + ImGui::Dummy(ImVec2(frameHeight, 0.0f)); // [CUSTOM] + ImGui::SameLine(0.0f, 0.0f); + ImGui::BeginGroup(); + ImGui::Dummy(ImVec2(frameHeight, 0.0f)); // [CUSTOM] + ImGui::SameLine(0.0f, 0.0f); + //ImGui::TextUnformatted(name); // [CUSTOM] + ImGui::SameLine(0.0f, 0.0f); + ImGui::Dummy(ImVec2(0.0, frameHeight + itemSpacing.y)); + ImGui::BeginGroup(); + + ImGui::PopStyleVar(2); + + //ImGui::GetCurrentWindow()->ContentsRegionRect.Max.x -= frameHeight * 0.5f; // [CUSTOM] + ImGui::GetCurrentWindow()->ContentRegionRect.Max.x -= frameHeight * 0.5f; + ImGui::GetCurrentWindow()->WorkRect.Max.x -= frameHeight * 0.5f; + ImGui::GetCurrentWindow()->InnerRect.Max.x -= frameHeight * 0.5f; + ImGui::GetCurrentWindow()->Size.x -= frameHeight; + + auto itemWidth = ImGui::CalcItemWidth(); + ImGui::PushItemWidth(ImMax(0.0f, itemWidth - frameHeight)); + //ImGui::PushItemWidth(effectiveSize.x - frameHeight); // [CUSTOM] +} + +void ImGui::EndGroupPanel() +{ + ImGui::PopItemWidth(); + + auto itemSpacing = ImGui::GetStyle().ItemSpacing; + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + + auto frameHeight = ImGui::GetFrameHeight(); + + ImGui::EndGroup(); + + //ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64), 4.0f); + + ImGui::EndGroup(); + + ImGui::SameLine(0.0f, 0.0f); + ImGui::Dummy(ImVec2(frameHeight * 0.5f, 0.0f)); + ImGui::Dummy(ImVec2(0.0, frameHeight - itemSpacing.y)); // [CUSTOM] + + ImGui::EndGroup(); + + auto itemMin = ImGui::GetItemRectMin(); + auto itemMax = ImGui::GetItemRectMax(); + //ImGui::GetWindowDrawList()->AddRectFilled(itemMin, itemMax, IM_COL32(255, 0, 0, 64), 4.0f); + + ImVec2 halfFrame = ImVec2(frameHeight * 0.25f, frameHeight) * 0.5f; + ImGui::GetWindowDrawList()->AddRect( + itemMin + halfFrame, itemMax - ImVec2(halfFrame.x, 0.0f), + ImColor(ImGui::GetStyleColorVec4(ImGuiCol_Border)), + halfFrame.x); + + ImGui::PopStyleVar(2); + + //ImGui::GetCurrentWindow()->ContentsRegionRect.Max.x += frameHeight * 0.5f; // [CUSTOM] + ImGui::GetCurrentWindow()->ContentRegionRect.Max.x += frameHeight * 0.5f; + ImGui::GetCurrentWindow()->WorkRect.Max.x += frameHeight * 0.5f; + ImGui::GetCurrentWindow()->InnerRect.Max.x += frameHeight * 0.5f; + ImGui::GetCurrentWindow()->Size.x += frameHeight; + + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + + ImGui::EndGroup(); +} diff --git a/group_panel.h b/group_panel.h new file mode 100644 index 0000000..13e57af --- /dev/null +++ b/group_panel.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +// The following code is slightly modified public domain code from https://github.com/thedmd +// All modifications have an inline comment with "[CUSTOM]" +// Public domain claim from https://github.com/ocornut/imgui/issues/1496#issuecomment-1287772456 +// Code from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353 + +namespace ImGui { // [CUSTOM] +void BeginGroupPanel(const ImVec2& size = ImVec2(-1.0f, -1.0f)); // [CUSTOM] +void EndGroupPanel(); +}; // namespace ImGui // [CUSTOM] diff --git a/main.cpp b/main.cpp index ff35cf7..c311bb9 100644 --- a/main.cpp +++ b/main.cpp @@ -16,12 +16,15 @@ #include "log.h" #include "fonts.h" #include "config.h" +#include "filters.h" #include "event_loop.h" #include "logcat_thread.h" int main(int, char**) { setlocale(LC_TIME, ""); + // TODO add saving + Filters filters; Config config; try { config = load_config(); @@ -161,7 +164,7 @@ int main(int, char**) { ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); - event_loop(monospace_font, config, logcat_thread, &run_event_loop); + event_loop(monospace_font, config, filters, logcat_thread, &run_event_loop); // Rendering ImGui::Render(); diff --git a/windows/filters.cpp b/windows/filters.cpp new file mode 100644 index 0000000..a91de4e --- /dev/null +++ b/windows/filters.cpp @@ -0,0 +1,257 @@ +#include +#include + +#include "../group_panel.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 inline std::unique_ptr render_add_filter_popup(); +static void update_logcat_entries(const Filters& filters, + const std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets); + +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(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(filter); + IntegerFilter* ifilter = !sfilter ? dynamic_cast(filter) : nullptr; + if (sfilter || ifilter) { + ImGui::SameLine(); + ImGui::Checkbox("Inverted", sfilter ? &sfilter->inverted : &ifilter->inverted); + } + } + + if (filter->error()) { + ImGui::SameLine(); + ImGui::Text("(%s)", filter->error()->c_str()); + } + + if (IntegerFilter* ifilter = dynamic_cast(filter)) { + render_integer_filter(ifilter); + } else if (StringFilter* sfilter = dynamic_cast(filter)) { + render_string_filter(sfilter); + } else if (BufferFilter* bfilter = dynamic_cast(filter)) { + render_buffer_filter(bfilter); + } else if (PriorityFilter* pfilter = dynamic_cast(filter)) { + render_priority_filter(pfilter); + } else if (GroupFilter* gfilter = dynamic_cast(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 is%s", head, filter->inverted ? " not" : ""); + 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(Buffer::Unknown))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Main", &filter->wanted, static_cast(Buffer::Main))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("System", &filter->wanted, static_cast(Buffer::System))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Radio", &filter->wanted, static_cast(Buffer::Radio))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Events", &filter->wanted, static_cast(Buffer::Events))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Crash", &filter->wanted, static_cast(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(Priority::Unknown))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Verbose", &filter->wanted, static_cast(Priority::Verbose))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Debug", &filter->wanted, static_cast(Priority::Debug))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Info", &filter->wanted, static_cast(Priority::Info))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Warning", &filter->wanted, static_cast(Priority::Warn))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Error", &filter->wanted, static_cast(Priority::Error))); ImGui::SameLine(); + update_if_needed(ImGui::CheckboxFlags("Fatal", &filter->wanted, static_cast(Priority::Fatal))); +} + +static inline void render_group_filter(GroupFilter* filter) { + int selected_type = static_cast(filter->type); + if (ImGui::Combo("of these are true", &selected_type, "All\0Any\0One\0None\0")) { + filter->type = static_cast(selected_type); + } + + for (std::vector>::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(filter)); + if (ImGui::Button("+ add filter")) { + ImGui::OpenPopup(add_filter_id.c_str()); + } + if (ImGui::BeginPopup(add_filter_id.c_str())) { + std::unique_ptr added_filter = render_add_filter_popup(); + if (added_filter) { + filter->filters.push_back(std::move(added_filter)); + filter->updated(); + } + ImGui::EndPopup(); + } +} + +static inline std::unique_ptr render_add_filter_popup() { + if (ImGui::Selectable("Buffer")) { + return std::make_unique(0); + } else if (ImGui::Selectable("User")) { + return std::make_unique(FilterKey::User, ""); + } else if (ImGui::Selectable("PID")) { + return std::make_unique(FilterKey::PID, 0); + } else if (ImGui::Selectable("TID")) { + return std::make_unique(FilterKey::TID, 0); + } else if (ImGui::Selectable("Priority")) { + return std::make_unique(0); + } else if (ImGui::Selectable("Tag")) { + return std::make_unique(FilterKey::Tag, ""); + } else if (ImGui::Selectable("Message")) { + return std::make_unique(FilterKey::Message, ""); + } else if (ImGui::Selectable("Group of filters")) { + return std::make_unique(std::vector>(), GroupFilter::Type::All); + } + + return std::unique_ptr(); +} + +static void update_logcat_entries(const Filters& filters, + const std::vector& logcat_entries, std::vector& 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); + } + } +} + +void filters_window(Filters& active_filters, Filters& inactive_filters, + const std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets, + bool* p_open) { + if (!ImGui::Begin("Filters", p_open)) { + ImGui::End(); + return; + } + + 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 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); + update_logcat_entries(active_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_filters, inactive_filters); + update_logcat_entries(active_filters, logcat_entries, filtered_logcat_entry_offsets); + } + ImGui::End(); +} diff --git a/windows/filters.h b/windows/filters.h new file mode 100644 index 0000000..2183699 --- /dev/null +++ b/windows/filters.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include "../logcat_entry.h" +#include "../filters.h" + +void filters_window(Filters& active_filters, Filters& inactive_filters, + const std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets, + bool* p_open); diff --git a/windows/main.cpp b/windows/main.cpp index 9d35097..a3861c8 100644 --- a/windows/main.cpp +++ b/windows/main.cpp @@ -13,15 +13,15 @@ static std::string leftpad(std::string str, size_t characters) { return str; } -static inline void main_scrolling_region(ImFont* monospace_font, std::vector& filtered_logcat_entries) { +static inline void main_scrolling_region(ImFont* monospace_font, std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets) { ImGui::PushFont(monospace_font); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImGuiListClipper clipper; - clipper.Begin(static_cast(filtered_logcat_entries.size())); + clipper.Begin(static_cast(filtered_logcat_entry_offsets.size())); while (clipper.Step()) { for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - LogcatEntry* logcat_entry = &filtered_logcat_entries[static_cast(i)]; + LogcatEntry* logcat_entry = &logcat_entries[filtered_logcat_entry_offsets[static_cast(i)]]; char time_as_str[128] = {0}; strftime(time_as_str, 127 * sizeof(char), "%c", localtime(&logcat_entry->time)); @@ -49,8 +49,11 @@ static inline void main_scrolling_region(ImFont* monospace_font, std::vector& filtered_logcat_entries, - const Config& active_config, Config& inactive_config, bool* show_settings_window, bool* show_logs_window, bool* run_event_loop) { +void main_window(bool latest_log_entries_read, ImFont* monospace_font, + std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets, + const Config& active_config, Config& inactive_config, + const Filters& active_filters, Filters& inactive_filters, + bool* show_settings_window, bool* show_filters_window, bool* show_logs_window, bool* run_event_loop) { if (!ImGui::Begin("LogMeow", run_event_loop)) { ImGui::End(); return; @@ -61,6 +64,12 @@ void main_window(bool latest_log_entries_read, ImFont* monospace_font, std::vect *show_settings_window = true; } + ImGui::SameLine(); + if (ImGui::Button("Filters") && !*show_filters_window) { + copy_filters(inactive_filters, active_filters); + *show_filters_window = true; + } + ImGui::SameLine(); if (!latest_log_entries_read) { ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(0.0f, 0.6f, 0.6f)); @@ -79,7 +88,7 @@ void main_window(bool latest_log_entries_read, ImFont* monospace_font, std::vect // and [SECTION] Example App: Long Text / ShowExampleAppLongText() // and [SECTION] Example App: Debug Log / ShowExampleAppLog() if (ImGui::BeginChild("ScrollingRegion", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar)) { - main_scrolling_region(monospace_font, filtered_logcat_entries); + main_scrolling_region(monospace_font, logcat_entries, filtered_logcat_entry_offsets); } ImGui::EndChild(); ImGui::End(); diff --git a/windows/main.h b/windows/main.h index 1939e11..18f168f 100644 --- a/windows/main.h +++ b/windows/main.h @@ -4,7 +4,11 @@ #include #include "../config.h" +#include "../filters.h" #include "../logcat_entry.h" -void main_window(bool latest_log_entries_read, ImFont* monospace_font, std::vector& filtered_logcat_entries, - const Config& active_config, Config& inactive_config, bool* show_settings_window, bool* show_logs_window, bool* run_event_loop); +void main_window(bool latest_log_entries_read, ImFont* monospace_font, + std::vector& logcat_entries, std::vector& filtered_logcat_entry_offsets, + const Config& active_config, Config& inactive_config, + const Filters& active_filters, Filters& inactive_filters, + bool* show_settings_window, bool* show_filters_window, bool* show_logs_window, bool* run_event_loop); diff --git a/windows/settings.cpp b/windows/settings.cpp index f0baa44..c1f6d9e 100644 --- a/windows/settings.cpp +++ b/windows/settings.cpp @@ -14,15 +14,12 @@ static void write_config_and_update_structures(const Config& config) { } void settings_window(Config& active_config, Config& inactive_config, bool* p_open) { - ImGuiWindowFlags flags = active_config != inactive_config - ? ImGuiWindowFlags_UnsavedDocument - : 0; - if (!ImGui::Begin("Settings", p_open, flags)) { + if (!ImGui::Begin("Settings", p_open)) { ImGui::End(); return; } // TODO actually have process control - ImGui::Text("Logcat command only takes effect when logcat is not running"); + ImGui::TextUnformatted("Logcat command only takes effect when logcat is not running"); ImGui::InputTextWithHint("Logcat command", "adb logcat -Dv 'threadtime UTC epoch usec uid'", &inactive_config.logcat_command); ImGui::Text("Font sizes only take effect when LogMeow is restarted"); @@ -34,7 +31,7 @@ void settings_window(Config& active_config, Config& inactive_config, bool* p_ope ImGui::Separator(); ImVec2 button_size(4 * ImGui::GetFontSize(), 0); if (ImGui::Button("OK", button_size)) { - active_config = inactive_config; + active_config = std::move(inactive_config); write_config_and_update_structures(active_config); *p_open = false; }