diff --git a/include/gui/GUIController.h b/include/gui/GUIController.h index 6ed231d..aa8722b 100644 --- a/include/gui/GUIController.h +++ b/include/gui/GUIController.h @@ -22,6 +22,8 @@ namespace gui Q_PROPERTY(QString currentScheduler READ currentScheduler WRITE setCurrentScheduler NOTIFY currentSchedulerChanged) Q_PROPERTY(bool isRunning READ isRunning NOTIFY isRunningChanged) Q_PROPERTY(int timeQuantum READ timeQuantum WRITE setTimeQuantum NOTIFY timeQuantumChanged) + Q_PROPERTY(bool isDarkMode READ isDarkMode WRITE setIsDarkMode NOTIFY isDarkModeChanged) + Q_PROPERTY(bool animationsEnabled READ animationsEnabled WRITE setAnimationsEnabled NOTIFY animationsEnabledChanged) Q_PROPERTY(QObject* ganttBlocksModel READ ganttBlocksModel CONSTANT) Q_PROPERTY(QObject* readyQueueModel READ readyQueueModel CONSTANT) Q_PROPERTY(QObject* threadListModel READ threadListModel CONSTANT) @@ -40,6 +42,12 @@ namespace gui int timeQuantum() const { return m_timeQuantum; } void setTimeQuantum(int quantum); + bool isDarkMode() const { return m_isDarkMode; } + void setIsDarkMode(bool darkMode); + + bool animationsEnabled() const { return m_animationsEnabled; } + void setAnimationsEnabled(bool enabled); + QObject* ganttBlocksModel() { return &m_ganttChartModel; } QObject* readyQueueModel() { return &m_readyQueueModel; } QObject* threadListModel() { return &m_threadListModel; } @@ -64,6 +72,8 @@ namespace gui void currentSchedulerChanged(); void isRunningChanged(); void timeQuantumChanged(); + void isDarkModeChanged(); + void animationsEnabledChanged(); void simulationUpdated(); void currentTimeChanged(); @@ -82,6 +92,8 @@ namespace gui int m_timeQuantum{ 2 }; int m_currentTime{ 0 }; int m_nextThreadId{ 0 }; + bool m_isDarkMode{ true }; + bool m_animationsEnabled{ true }; ThreadListModel m_threadListModel; ReadyQueueModel m_readyQueueModel; diff --git a/src/gui/GUIController.cpp b/src/gui/GUIController.cpp index ba82020..5a45daf 100644 --- a/src/gui/GUIController.cpp +++ b/src/gui/GUIController.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace gui { @@ -67,6 +69,25 @@ namespace gui } } + void GUIController::setIsDarkMode(bool darkMode) + { + if (m_isDarkMode != darkMode) + { + m_isDarkMode = darkMode; + emit isDarkModeChanged(); + } + } + + void GUIController::setAnimationsEnabled(bool enabled) + { + if (m_animationsEnabled != enabled) + { + m_animationsEnabled = enabled; + emit animationsEnabledChanged(); + emit simulationUpdated(); // Принудительное обновление UI при переключении анимаций + } + } + void GUIController::startSimulation() { if (!m_isRunning) @@ -92,19 +113,48 @@ namespace gui void GUIController::resetSimulation() { + // 1. Останавливаем таймер, чтобы не порождать новые stepSimulation. if (m_timer->isActive()) { m_timer->stop(); } - m_nextThreadId = 0; - m_lastLoadedThreads.clear(); + // 2. Синхронный reset воркера. Блокирующий вызов гарантирует, что + // все ранее поставленные в очередь stepSimulation() уже + // выполнились, и их сигналы (QueuedConnection) уже лежат + // в очереди событий GUI-потока. + QMetaObject::invokeMethod(m_worker, "resetSimulation", Qt::BlockingQueuedConnection); + + // 3. Удаляем из очереди GUI-потока все отложенные MetaCall-события, + // адресованные этому объекту. Это «устаревшие» сигналы от + // уже отработавших step'ов — без этой очистки они бы + // повторно наполнили модели сразу после их очистки ниже. + QCoreApplication::removePostedEvents(this, QEvent::MetaCall); + + // 4. Очищаем все модели. m_ganttChartModel.clear(); m_readyQueueModel.clear(); m_threadListModel.clear(); m_benchmarkResultsModel.clear(); - QMetaObject::invokeMethod(m_worker, "resetSimulation", Qt::QueuedConnection); + // 5. Сбрасываем внутреннее состояние. + m_nextThreadId = 0; + m_lastLoadedThreads.clear(); + + // 6. Явно сбрасываем свойства симуляции — не полагаемся на сигналы + // воркера, потому что они были удалены на шаге 3. + if (m_currentTime != 0) + { + m_currentTime = 0; + emit currentTimeChanged(); + } + if (m_isRunning) + { + m_isRunning = false; + emit isRunningChanged(); + } + + emit simulationUpdated(); } void GUIController::addThread(int id, int burstTime, int priority) @@ -191,14 +241,8 @@ namespace gui void GUIController::saveToJson(const QString& filePath) { QString path = filePath; - if (path.startsWith("file:///")) - { - path = path.mid(8); - } - else if (path.startsWith("file://")) - { - path = path.mid(7); - } + if (path.startsWith("file:///")) path = path.mid(8); + else if (path.startsWith("file://")) path = path.mid(7); QJsonObject rootObj; rootObj["scheduler"] = m_currentScheduler; @@ -228,40 +272,22 @@ namespace gui void GUIController::loadFromJson(const QString& filePath) { QString path = filePath; - if (path.startsWith("file:///")) - { - path = path.mid(8); - } - else if (path.startsWith("file://")) - { - path = path.mid(7); - } + if (path.startsWith("file:///")) path = path.mid(8); + else if (path.startsWith("file://")) path = path.mid(7); QFile file(path); - if (!file.open(QIODevice::ReadOnly)) - { - return; - } + if (!file.open(QIODevice::ReadOnly)) return; QByteArray data = file.readAll(); file.close(); QJsonDocument doc = QJsonDocument::fromJson(data); - if (doc.isNull() || !doc.isObject()) - { - return; - } + if (doc.isNull() || !doc.isObject()) return; QJsonObject rootObj = doc.object(); - if (rootObj.contains("scheduler")) - { - setCurrentScheduler(rootObj["scheduler"].toString()); - } - if (rootObj.contains("timeQuantum")) - { - setTimeQuantum(rootObj["timeQuantum"].toInt()); - } + if (rootObj.contains("scheduler")) setCurrentScheduler(rootObj["scheduler"].toString()); + if (rootObj.contains("timeQuantum")) setTimeQuantum(rootObj["timeQuantum"].toInt()); resetSimulation(); @@ -282,20 +308,11 @@ namespace gui void GUIController::exportCsv(const QString& filePath) { QString path = filePath; - if (path.startsWith("file:///")) - { - path = path.mid(8); - } - else if (path.startsWith("file://")) - { - path = path.mid(7); - } + if (path.startsWith("file:///")) path = path.mid(8); + else if (path.startsWith("file://")) path = path.mid(7); QFile file(path); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) - { - return; - } + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; QTextStream out(&file); out << "Algorithm,TotalTicks,Throughput,ContextSwitches,JainsFairness,AvgWaiting,AvgTurnaround\n"; diff --git a/src/qml/main.qml b/src/qml/main.qml index abfdd99..4c4f92d 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -6,11 +6,13 @@ import QtQuick.Dialogs 1.3 ApplicationWindow { id: window - width: 1024 + width: 1500 height: 900 visible: true title: "Thread Scheduler Visualizer" + color: (guiController && guiController.isDarkMode) ? "#121212" : "#f5f5f5" + ScrollView { anchors.fill: parent contentWidth: availableWidth @@ -28,11 +30,26 @@ ApplicationWindow { title: "Simulation & Algorithm Controls" Layout.fillWidth: true + background: Rectangle { + color: (guiController && guiController.isDarkMode) ? "#1e1e1e" : "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#333333" : "#cccccc" + radius: 4 + } + + label: Label { + text: parent.title + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + font.bold: true + } + RowLayout { spacing: 15 anchors.fill: parent - Label { text: "Algorithm:" } + Label { + text: "Algorithm:" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + } ComboBox { id: algoSelector model: [ @@ -49,8 +66,9 @@ ApplicationWindow { } } - Label { - text: "Quantum:" + Label { + text: "Quantum:" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" visible: algoSelector.currentText === "Round Robin" } SpinBox { @@ -66,6 +84,36 @@ ApplicationWindow { Item { Layout.fillWidth: true } // Spacer + Switch { + text: "Dark Theme" + checked: guiController ? guiController.isDarkMode : true + onCheckedChanged: { + if (guiController) guiController.isDarkMode = checked + } + + contentItem: Text { + text: parent.text + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + verticalAlignment: Text.AlignVCenter + leftPadding: parent.indicator.width + parent.spacing + } + } + + Switch { + text: "Animations" + checked: guiController ? guiController.animationsEnabled : true + onCheckedChanged: { + if (guiController) guiController.animationsEnabled = checked + } + + contentItem: Text { + text: parent.text + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + verticalAlignment: Text.AlignVCenter + leftPadding: parent.indicator.width + parent.spacing + } + } + Button { text: "Compare Algorithms" onClicked: { @@ -116,11 +164,26 @@ ApplicationWindow { title: "Predefined Workload Scenarios & I/O" Layout.fillWidth: true + background: Rectangle { + color: (guiController && guiController.isDarkMode) ? "#1e1e1e" : "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#333333" : "#cccccc" + radius: 4 + } + + label: Label { + text: parent.title + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + font.bold: true + } + RowLayout { spacing: 15 anchors.fill: parent - Label { text: "Scenario:" } + Label { + text: "Scenario:" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + } ComboBox { id: scenarioSelector model: guiController ? guiController.scenarioNames : [] @@ -162,16 +225,31 @@ ApplicationWindow { title: "Add New Thread" Layout.fillWidth: true + background: Rectangle { + color: (guiController && guiController.isDarkMode) ? "#1e1e1e" : "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#333333" : "#cccccc" + radius: 4 + } + + label: Label { + text: parent.title + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + font.bold: true + } + RowLayout { spacing: 15 anchors.fill: parent - Label { text: "ID:" } - SpinBox { + Label { + text: "ID:" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + } + SpinBox { id: threadIdInput from: 1 to: 99 - value: 1 + value: 1 Connections { target: guiController ? guiController : null @@ -183,10 +261,16 @@ ApplicationWindow { } } - Label { text: "Burst Time:" } + Label { + text: "Burst Time:" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + } SpinBox { id: burstTimeInput; from: 1; to: 20; value: 3 } - Label { text: "Priority:" } + Label { + text: "Priority:" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" + } SpinBox { id: priorityInput; from: 1; to: 10; value: 1 } Button { @@ -207,8 +291,8 @@ ApplicationWindow { Rectangle { Layout.fillWidth: true implicitHeight: 110 - color: "#1e1e1e" - border.color: "#333333" + color: (guiController && guiController.isDarkMode) ? "#1e1e1e" : "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#333333" : "#cccccc" radius: 5 clip: true @@ -219,7 +303,7 @@ ApplicationWindow { Text { text: "Ready Queue / MLFQ Priority Levels" - color: "#ffffff" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" font.pixelSize: 14 font.bold: true } @@ -232,20 +316,25 @@ ApplicationWindow { Row { spacing: 12 - + Repeater { model: guiController ? guiController.readyQueueModel : null - Rectangle { + delegate: Rectangle { width: 100 height: 60 - color: "#2a2a2a" + color: (guiController && guiController.isDarkMode) ? "#2a2a2a" : "#eaeaea" border.color: model.color || "#0277bd" border.width: 2 radius: 4 scale: hoverHandler.hovered ? 1.03 : 1.0 - Behavior on scale { NumberAnimation { duration: 150 } } + Behavior on scale { + NumberAnimation { + duration: 150 + running: guiController ? guiController.animationsEnabled : true + } + } HoverHandler { id: hoverHandler @@ -259,14 +348,14 @@ ApplicationWindow { spacing: 2 Text { text: "T" + model.threadId + " (L" + model.queueLevel + ")" - color: "#ffffff" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" font.bold: true font.pixelSize: 11 anchors.horizontalCenter: parent.horizontalCenter } Text { text: "Burst: " + model.burstTime - color: "#aaaaaa" + color: (guiController && guiController.isDarkMode) ? "#aaaaaa" : "#555555" font.pixelSize: 10 anchors.horizontalCenter: parent.horizontalCenter } @@ -282,8 +371,8 @@ ApplicationWindow { Rectangle { Layout.fillWidth: true implicitHeight: 140 - color: "#1e1e1e" - border.color: "#333333" + color: (guiController && guiController.isDarkMode) ? "#1e1e1e" : "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#333333" : "#cccccc" radius: 5 clip: true @@ -296,14 +385,14 @@ ApplicationWindow { Layout.fillWidth: true Text { text: "Gantt Chart Execution Timeline" - color: "#ffffff" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" font.pixelSize: 16 font.bold: true } Item { Layout.fillWidth: true } Text { text: "Current Time: " + (guiController ? guiController.currentTime : 0) - color: "#aaaaaa" + color: (guiController && guiController.isDarkMode) ? "#aaaaaa" : "#555555" font.pixelSize: 14 } } @@ -317,22 +406,58 @@ ApplicationWindow { Row { id: ganttRow spacing: 2 - + Repeater { model: guiController ? guiController.ganttBlocksModel : null - Rectangle { + delegate: Rectangle { + id: ganttDelegate + width: model.duration * 40 height: 70 color: hoverHandlerGantt.hovered ? Qt.lighter(model.color, 1.2) : model.color radius: 4 - border.color: "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" border.width: 1 - scale: 0.0 - Component.onCompleted: scale = 1.0 + // ===== ИСПРАВЛЕНИЕ БАГА ===== + // Флаг "появился" — после первого Component.onCompleted + // становится true и больше никогда не сбрасывается. + // Благодаря этому переключение animationsEnabled + // НЕ пересчитывает opacity существующих блоков в 0. + property bool appeared: false + + opacity: appeared + ? 1.0 + : ((guiController && guiController.animationsEnabled) ? 0.0 : 1.0) + + Component.onCompleted: { + // Устанавливаем appeared = true. При включённых + // анимациях это вызовет пересчёт opacity 0.0 -> 1.0, + // и Behavior ниже проиграет fade-in. При выключенных + // анимациях opacity и так 1.0 — визуально ничего + // не меняется, но binding всё равно "замораживается" + // на значении 1.0, что и предотвращает исчезновение + // блоков при повторном включении анимаций. + appeared = true + } + + Behavior on opacity { + NumberAnimation { + duration: 250 + running: guiController ? guiController.animationsEnabled : true + } + } + // ===== КОНЕЦ ИСПРАВЛЕНИЯ ===== + + scale: hoverHandlerGantt.hovered ? 1.04 : 1.0 + Behavior on scale { - NumberAnimation { duration: 250; easing.type: Easing.OutBack } + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + running: guiController ? guiController.animationsEnabled : true + } } HoverHandler { @@ -368,8 +493,8 @@ ApplicationWindow { Rectangle { Layout.fillWidth: true implicitHeight: 160 - color: "#1e1e1e" - border.color: "#333333" + color: (guiController && guiController.isDarkMode) ? "#1e1e1e" : "#ffffff" + border.color: (guiController && guiController.isDarkMode) ? "#333333" : "#cccccc" radius: 5 clip: true @@ -380,7 +505,7 @@ ApplicationWindow { Text { text: "Process / Thread Status Table" - color: "#ffffff" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" font.pixelSize: 14 font.bold: true } @@ -388,7 +513,7 @@ ApplicationWindow { Rectangle { Layout.fillWidth: true height: 25 - color: "#2d2d2d" + color: (guiController && guiController.isDarkMode) ? "#2d2d2d" : "#e0e0e0" radius: 3 RowLayout { @@ -396,10 +521,10 @@ ApplicationWindow { anchors.leftMargin: 10 anchors.rightMargin: 10 - Text { text: "ID"; color: "#aaa"; font.bold: true; Layout.preferredWidth: 60 } - Text { text: "Priority"; color: "#aaa"; font.bold: true; Layout.preferredWidth: 80 } - Text { text: "State"; color: "#aaa"; font.bold: true; Layout.preferredWidth: 100 } - Text { text: "Remaining / Burst"; color: "#aaa"; font.bold: true; Layout.fillWidth: true } + Text { text: "ID"; color: (guiController && guiController.isDarkMode) ? "#aaa" : "#333"; font.bold: true; Layout.preferredWidth: 60 } + Text { text: "Priority"; color: (guiController && guiController.isDarkMode) ? "#aaa" : "#333"; font.bold: true; Layout.preferredWidth: 80 } + Text { text: "State"; color: (guiController && guiController.isDarkMode) ? "#aaa" : "#333"; font.bold: true; Layout.preferredWidth: 100 } + Text { text: "Remaining / Burst"; color: (guiController && guiController.isDarkMode) ? "#aaa" : "#333"; font.bold: true; Layout.fillWidth: true } } } @@ -413,9 +538,9 @@ ApplicationWindow { delegate: Rectangle { width: ListView.view.width height: 30 - color: "#252526" + color: (guiController && guiController.isDarkMode) ? "#252526" : "#f0f0f0" radius: 3 - border.color: "#383838" + border.color: (guiController && guiController.isDarkMode) ? "#383838" : "#d0d0d0" RowLayout { anchors.fill: parent @@ -434,7 +559,7 @@ ApplicationWindow { } Text { text: "T" + model.threadId - color: "#ffffff" + color: (guiController && guiController.isDarkMode) ? "#ffffff" : "#000000" font.bold: true anchors.verticalCenter: parent.verticalCenter } @@ -442,7 +567,7 @@ ApplicationWindow { Text { text: model.priority - color: "#cccccc" + color: (guiController && guiController.isDarkMode) ? "#cccccc" : "#333333" Layout.preferredWidth: 80 } @@ -468,7 +593,7 @@ ApplicationWindow { Text { text: model.remainingTime + " / " + model.burstTime + " ticks" - color: "#cccccc" + color: (guiController && guiController.isDarkMode) ? "#cccccc" : "#333333" Layout.fillWidth: true } }