diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c505cf..99971cf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,8 @@ "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build/debug/bin/cpp_lab_project", // path to the executable + // "program": "${workspaceFolder}/build/debug/bin/cpp_lab_project", // path to the executable + "program": "${workspaceFolder}/build/debug/bin/mvc_ap", // path to the executable "args": [], "stopAtEntry": true, "cwd": "${workspaceFolder}", // working directory diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index d83ffb1..ef689cc 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -13,3 +13,13 @@ FetchContent_Declare( # Make GoogleTest available FetchContent_MakeAvailable(googletest) + + +# ---------------------------------------------------------------------------------------- +# Dependencies - GTK4 +# ---------------------------------------------------------------------------------------- +# Use the package PkgConfig to detect GTK+ headers/library files +find_package(PkgConfig REQUIRED) + +# Check for gtkmm-4.0 specifically, not just gtk4 +pkg_check_modules(GTKMM REQUIRED gtkmm-4.0) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 35f5910..64b2f67 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(controller) add_subdirectory(dp) add_subdirectory(socket) add_subdirectory(excercise) +add_subdirectory(ap) # main application executable does NOT link to this library. add_subdirectory(leetcode) diff --git a/src/ap/CMakeLists.txt b/src/ap/CMakeLists.txt new file mode 100644 index 0000000..7e9ae84 --- /dev/null +++ b/src/ap/CMakeLists.txt @@ -0,0 +1,21 @@ +# Create the gtk4-setting interface library +# Link against the libraries found for GTKMM +add_library(gtk4-settings INTERFACE) +target_link_libraries(gtk4-settings INTERFACE ${GTKMM_LIBRARIES}) + +# Include the directories where gtkmm.h resides +target_include_directories(gtk4-settings INTERFACE ${GTKMM_INCLUDE_DIRS}) + +# Add compiler flags (defines, etc.) required by gtkmm +target_compile_options(gtk4-settings INTERFACE ${GTKMM_CFLAGS_OTHER}) + +# ap executable +add_executable(ap) +target_sources(ap + PRIVATE + simple_ap.cpp +) +target_link_libraries(ap PRIVATE gtk4-settings) + +add_subdirectory(mvc) +# add_subdirectory(mvvm) \ No newline at end of file diff --git a/src/ap/README.md b/src/ap/README.md index 450fcf0..0eaf53b 100644 --- a/src/ap/README.md +++ b/src/ap/README.md @@ -25,7 +25,7 @@ View ↔ ViewModel ↔ Model # view automatically update - **MVC (Model - View - Controller)** is an architectural pattern that separates the user interface (`View`) from the application logic and data (`Model`) using an intermediary component called the `Controller`. - **Components:** - **Model**: is responsible for managing and abstracting data sources (databases, APIs, etc.). The `Model` handles data retrieval, storage, and business logic. - - **View**: displays the data provided by the `Model` and represents the user interface. The `View` is responsible only for presentation and does not contain business logic. + - **View**: displays the data provided by the `Model` and represents the user interface. The `View` is responsible only for presentation and `does not contain business logic`. - **Controller**: acts as an intermediary between the `View` and the `Model`. It receives user input from the `View`, processes it, and interacts with the `Model` to update or retrieve data. The `Controller` then determines which `View` should display the result. - **Workflow:** @@ -41,4 +41,16 @@ View → Controller → Model ↓ View # view update manually ``` -### 3. Examples \ No newline at end of file +### 3. GTK4 +- [Refer](https://docs.gtk.org/gtk4/getting_started.html) + +### 4. Examples +### 4.1. simple_ap +- Cos: + - Quick, Simple +- Pos: + - Dependency: e.g. what happen when we delete Gtk::Label m_labelMonitorA; + - Scalability: + - Reusability: + +### 4.2. mvc_ap \ No newline at end of file diff --git a/src/ap/mvc/CMakeLists.txt b/src/ap/mvc/CMakeLists.txt new file mode 100644 index 0000000..c265c19 --- /dev/null +++ b/src/ap/mvc/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(mvc_ap) + +target_sources(mvc_ap + PRIVATE + mvc_ap.cpp + model/SharedData.cpp + view/EditorWidget.cpp + view/DisplayWidget.cpp + controller/Controller.cpp +) + +target_link_libraries(mvc_ap PRIVATE gtk4-settings) diff --git a/src/ap/mvc/IObserver.h b/src/ap/mvc/IObserver.h new file mode 100644 index 0000000..60c9baf --- /dev/null +++ b/src/ap/mvc/IObserver.h @@ -0,0 +1,7 @@ +#pragma once + +class IObserver { + public: + virtual ~IObserver() = default; + virtual void onDataChanged(const std::string& newData) = 0; +}; \ No newline at end of file diff --git a/src/ap/mvc/controller/Controller.cpp b/src/ap/mvc/controller/Controller.cpp new file mode 100644 index 0000000..843268a --- /dev/null +++ b/src/ap/mvc/controller/Controller.cpp @@ -0,0 +1,12 @@ +#include "Controller.h" + +Controller::Controller(std::shared_ptr model) : model_(model) {} + +void Controller::updateRequest(const std::string& text) { + if (text.empty()) + return; + + // the Controller updates the Model when receiving new data from the View, + // which then notifies all Views of the change + model_->setData(text); +} \ No newline at end of file diff --git a/src/ap/mvc/controller/Controller.h b/src/ap/mvc/controller/Controller.h new file mode 100644 index 0000000..37198f5 --- /dev/null +++ b/src/ap/mvc/controller/Controller.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include "../model/SharedData.h" + +class Controller { + public: + explicit Controller(std::shared_ptr model); + + void updateRequest(const std::string& text); + + private: + std::shared_ptr model_; +}; \ No newline at end of file diff --git a/src/ap/mvc/model/SharedData.cpp b/src/ap/mvc/model/SharedData.cpp new file mode 100644 index 0000000..0f44a39 --- /dev/null +++ b/src/ap/mvc/model/SharedData.cpp @@ -0,0 +1,23 @@ +#include "SharedData.h" + +SharedData::SharedData() : data_{"Initial Data"} {} + +void SharedData::setData(const std::string& data) { + this->data_ = data; + // Notify observers of the changed data + notifyObservers(); +} + +void SharedData::notifyObservers() { + for (auto o : observers_) { + o->onDataChanged(this->data_); + } +} +void SharedData::addObserver(IObserver* obs) { + if (obs != nullptr) + observers_.push_back(obs); +} + +std::string SharedData::getData() const { + return data_; +} \ No newline at end of file diff --git a/src/ap/mvc/model/SharedData.h b/src/ap/mvc/model/SharedData.h new file mode 100644 index 0000000..83f3349 --- /dev/null +++ b/src/ap/mvc/model/SharedData.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include "../IObserver.h" + +class SharedData { + public: + SharedData(); + + void setData(const std::string& data); + std::string getData() const; + + void addObserver(IObserver* obs); + + private: + void notifyObservers(); + + private: + std::string data_; + std::vector observers_; +}; \ No newline at end of file diff --git a/src/ap/mvc/mvc_ap.cpp b/src/ap/mvc/mvc_ap.cpp new file mode 100644 index 0000000..e6cb20c --- /dev/null +++ b/src/ap/mvc/mvc_ap.cpp @@ -0,0 +1,74 @@ +#include +#include +#include "controller/Controller.h" +#include "model/SharedData.h" +#include "view/DisplayWidget.h" +#include "view/EditorWidget.h" + +class ContainerWindow : public Gtk::Window { + public: + ContainerWindow(); + + private: + // Main Layout + Gtk::Box mainLayout_; + Gtk::Box topRowLayout_; // Horizontal arrangement (2 Displays side-by-side) + + // Model & Controller are shared across all Views, + // so we should use as shared pointers + // Model + std::shared_ptr dataModel_; + // Controllers + std::shared_ptr controller_; + + // Views + std::unique_ptr editorView_; + std::unique_ptr displayViewLeft_; + std::unique_ptr displayViewRight_; +}; + +ContainerWindow::ContainerWindow() + : mainLayout_(Gtk::Orientation::VERTICAL), + topRowLayout_(Gtk::Orientation::HORIZONTAL) { + set_title("MVC Integrated Demo"); + set_default_size(600, 400); + + // Init MVC + dataModel_ = std::make_shared(); + controller_ = std::make_shared(dataModel_); + + // Create child Views (Widgets) + editorView_ = + std::make_unique(controller_, dataModel_->getData()); + + // Create two displays of different colors for easier viewing + displayViewLeft_ = std::make_unique( + "ZONE 2: MONITOR A (Blue)", "blue", dataModel_->getData()); + displayViewRight_ = std::make_unique( + "ZONE 3: MONITOR B (Red)", "red", dataModel_->getData()); + + dataModel_->addObserver(editorView_.get()); + dataModel_->addObserver(displayViewLeft_.get()); + dataModel_->addObserver(displayViewRight_.get()); + + // Layout Arrangement (Container) + // Top Row: 2 Displays side-by-side (evenly spaced) + displayViewLeft_->set_hexpand(true); + displayViewRight_->set_hexpand(true); + topRowLayout_.append(*displayViewLeft_); + topRowLayout_.append(*displayViewRight_); + + // Bottom Row: Editor + editorView_->set_vexpand( + false); // Editor doesn't need to be stretched too large + + // Combine into Main Layout + mainLayout_.append(topRowLayout_); // Add top row + mainLayout_.append(*editorView_); // Add bottom row + set_child(mainLayout_); +} + +int main(int argc, char* argv[]) { + auto app = Gtk::Application::create("org.gtkmm.example.singlemvc"); + return app->make_window_and_run(argc, argv); +} \ No newline at end of file diff --git a/src/ap/mvc/view/DisplayWidget.cpp b/src/ap/mvc/view/DisplayWidget.cpp new file mode 100644 index 0000000..9686cbc --- /dev/null +++ b/src/ap/mvc/view/DisplayWidget.cpp @@ -0,0 +1,29 @@ +#include "DisplayWidget.h" + +DisplayWidget::DisplayWidget(const std::string& title, const std::string& color, + const std::string& startData) + : Gtk::Box(Gtk::Orientation::VERTICAL), + color_(color), + innerBox_(Gtk::Orientation::VERTICAL) { + frame_.set_label(title); + frame_.set_margin(10); + + updateLabel(startData); + + innerBox_.append(labelData_); + innerBox_.set_margin(20); + + frame_.set_child(innerBox_); + this->append(frame_); +} + +void DisplayWidget::updateLabel(const std::string& text) { + // Use HTML markup to change text color + std::string markup = "" + text + ""; + labelData_.set_markup(markup); +} + +void DisplayWidget::onDataChanged(const std::string& newData) { + updateLabel(newData); +} \ No newline at end of file diff --git a/src/ap/mvc/view/DisplayWidget.h b/src/ap/mvc/view/DisplayWidget.h new file mode 100644 index 0000000..48022da --- /dev/null +++ b/src/ap/mvc/view/DisplayWidget.h @@ -0,0 +1,19 @@ +#include +#include "../IObserver.h" + +class DisplayWidget : public Gtk::Box, public IObserver { + public: + DisplayWidget(const std::string& title, const std::string& color, + const std::string& startData); + + void onDataChanged(const std::string& newData) override; + + private: + void updateLabel(const std::string& text); + + private: + std::string color_; + Gtk::Frame frame_; + Gtk::Box innerBox_; + Gtk::Label labelData_; +}; \ No newline at end of file diff --git a/src/ap/mvc/view/EditorWidget.cpp b/src/ap/mvc/view/EditorWidget.cpp new file mode 100644 index 0000000..61a1d90 --- /dev/null +++ b/src/ap/mvc/view/EditorWidget.cpp @@ -0,0 +1,36 @@ +#include "EditorWidget.h" + +EditorWidget::EditorWidget(std::shared_ptr c, + const std::string& initData) + : Gtk::Box(Gtk::Orientation::VERTICAL), + innerBox_(Gtk::Orientation::VERTICAL), + controller_(c) { + // Create a beautiful border + frame_.set_label("ZONE 1: EDITOR (Input View)"); + frame_.set_margin(10); + + labelTitle_.set_text("Enter new data:"); + entry_.set_text(initData); + button_.set_label("Broadcast Update"); + + // Layout inside the frame + innerBox_.append(labelTitle_); + innerBox_.append(entry_); + innerBox_.append(button_); + innerBox_.set_margin(15); + innerBox_.set_spacing(10); + + frame_.set_child(innerBox_); + this->append(frame_); // Add a frame to the main Box of this class + + // MVC logic to help the Controller receive update input from the View + button_.signal_clicked().connect( + [this]() { controller_->updateRequest(entry_.get_text()); }); +} + +void EditorWidget::onDataChanged(const std::string& newData) { + // Fix string comparison error or do not thing ? + if (entry_.get_text() != Glib::ustring(newData)) { + entry_.set_text(newData); + } +} \ No newline at end of file diff --git a/src/ap/mvc/view/EditorWidget.h b/src/ap/mvc/view/EditorWidget.h new file mode 100644 index 0000000..88c69b9 --- /dev/null +++ b/src/ap/mvc/view/EditorWidget.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include "../controller/Controller.h" +#include "../IObserver.h" + +class EditorWidget : public Gtk::Box, public IObserver { + public: + EditorWidget(std::shared_ptr c, const std::string& initData); + + void onDataChanged(const std::string& newData) override; + + private: + Gtk::Frame frame_; + Gtk::Box innerBox_; + Gtk::Label labelTitle_; + Gtk::Entry entry_; + Gtk::Button button_; + + std::shared_ptr controller_; +}; \ No newline at end of file diff --git a/src/ap/simple_ap.cpp b/src/ap/simple_ap.cpp new file mode 100644 index 0000000..9d297cd --- /dev/null +++ b/src/ap/simple_ap.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +// We put everything into a single window class (God Class) +class MainWindow : public Gtk::Window { + + // --- Layout Containers --- + Gtk::Box m_mainLayout; + Gtk::Box m_topRowLayout; + + // --- Widgets --- + // We must declare everything here so that the buttons are accessible + + // Editor + Gtk::Frame frame_Editor; + Gtk::Box m_boxEditor; + Gtk::Entry entry_Input; + Gtk::Button button_Update; + + // Monitor A + Gtk::Frame frame_MonitorA; + Gtk::Box m_boxMonitorA; + Gtk::Label m_labelMonitorA; + + // Monitor B + Gtk::Frame frame_MonitorB; + Gtk::Box m_boxMonitorB; + Gtk::Label m_labelMonitorB; + + public: + MainWindow() + : m_mainLayout(Gtk::Orientation::VERTICAL), + m_topRowLayout(Gtk::Orientation::HORIZONTAL), + m_boxEditor(Gtk::Orientation::VERTICAL), + m_boxMonitorA(Gtk::Orientation::VERTICAL), + m_boxMonitorB(Gtk::Orientation::VERTICAL) { + set_title("No-MVC (Coupled) Demo"); + set_default_size(600, 400); + + // 1. SETUP UI (Visually identical to MVC) + // --- Monitor A --- + frame_MonitorA.set_label("ZONE 2: MONITOR A (Blue)"); + m_labelMonitorA.set_markup( + "Initial Data"); + m_boxMonitorA.append(m_labelMonitorA); + frame_MonitorA.set_child(m_boxMonitorA); + frame_MonitorA.set_hexpand(true); // Stretch + + // --- Monitor B --- + frame_MonitorB.set_label("ZONE 3: MONITOR B (Red)"); + m_labelMonitorB.set_markup( + "Initial Data"); + m_boxMonitorB.append(m_labelMonitorB); + frame_MonitorB.set_child(m_boxMonitorB); + frame_MonitorB.set_hexpand(true); + + // --- Editor --- + frame_Editor.set_label("ZONE 1: EDITOR"); + entry_Input.set_text("Initial Data"); + button_Update.set_label("Direct Update"); // Live updates + m_boxEditor.append(entry_Input); + m_boxEditor.append(button_Update); + frame_Editor.set_child(m_boxEditor); + + // --- Layout --- + m_topRowLayout.append(frame_MonitorA); + m_topRowLayout.append(frame_MonitorB); + m_mainLayout.append(m_topRowLayout); + m_mainLayout.append(frame_Editor); + + // Margin for aesthetics + m_boxEditor.set_margin(10); + m_boxEditor.set_spacing(5); + m_boxMonitorA.set_margin(20); + m_boxMonitorB.set_margin(20); + set_child(m_mainLayout); + + // 2. LOGIC HANDLING + // Here, the button must "know" exactly who m_labelMonitorA and m_labelMonitorB are. + // It directly controls the other widgets. + // (THE BAD PART) + button_Update.signal_clicked().connect([this]() { + // Step 1: Get data directly from UI (Entry) + std::string text = entry_Input.get_text(); + // There may be processing logic here (Validating...) + if (text.empty()) + return; + + // Step 2: Update Monitor A (Hard-coded) directly + m_labelMonitorA.set_markup("" + + text + ""); + + // Step 3: Update Monitor B (Hard-coded) directly + m_labelMonitorB.set_markup("" + + text + ""); + std::cout << "Updated directly without Model!" << std::endl; + }); + } +}; + +int main(int argc, char* argv[]) { + auto app = Gtk::Application::create("org.gtkmm.example.nomvc"); + return app->make_window_and_run(argc, argv); +} \ No newline at end of file