diff --git a/seminar11_events/01_select_move_delete/Makefile b/seminar11_events/01_select_move_delete/Makefile new file mode 100644 index 0000000..bf2128f --- /dev/null +++ b/seminar11_events/01_select_move_delete/Makefile @@ -0,0 +1,9 @@ +#path = ../../../3rdparty/SFML-2.5.1 +#select_move_delete: +# g++ ./select_move_delete.cpp -std=c++11 -o select_move_delete.exe -I $(path)/include -L $(path)/lib/ -lsfml-graphics -lsfml-window -lsfml-system +#select_move_delete: +# g++ ./select_move_delete.cpp -std=c++11 -o select_move_delete.exe -I $(path)/include -L $(path)/lib/ -lsfml-graphics -lsfml-window -lsfml-system +build: + g++ ./select_move_delete.cpp -std=c++11 -o select_move_delete -lsfml-graphics -lsfml-window -lsfml-system +build_debug: + g++ ./select_move_delete.cpp -std=c++11 -o select_move_delete -lsfml-graphics -lsfml-window -lsfml-system -D_DEBUG diff --git a/seminar11_events/01_select_move_delete/consolas.ttf b/seminar11_events/01_select_move_delete/consolas.ttf new file mode 100644 index 0000000..a79deb5 Binary files /dev/null and b/seminar11_events/01_select_move_delete/consolas.ttf differ diff --git a/seminar11_events/01_select_move_delete/context_menu.hpp b/seminar11_events/01_select_move_delete/context_menu.hpp new file mode 100644 index 0000000..bbccf76 --- /dev/null +++ b/seminar11_events/01_select_move_delete/context_menu.hpp @@ -0,0 +1,178 @@ +#pragma once +#include +#include + + +/* + Класс ContextMenu - контекстное меню + При нажатии правой кнопки мыши на экране появляется контекстное меню + + Публичные методы: + ContextMenu(sf::RenderWindow&, const sf::Font&) + Конструктор принимает окно для отрисовки и шрифт + + void addButton(const sf::String& name) + Добавить новый элемент в контекстное меню по имени name + + void draw() + Нарисовать контекстное меню в окне, которое было передано в конструктор + + int handleEvent(const sf::Event& event) + Обрабатывает событие event и возвращает целое число + Если это событие MousePressed и был выбран один из вариантов + контекстного меню, то вернёт номер этого варианта + Нумерация начинается с нуля + В ином случае вернёт -1 + +*/ + + +class ContextMenu +{ +private: + inline static const sf::Color kDefaultColor {sf::Color(190, 210, 190)}; + inline static const sf::Color kHoverColor {sf::Color(150, 170, 150)}; + inline static const sf::Color kTextColor {sf::Color::Black}; + inline static const int kButtonHeight = 20; + inline static const int kCharacterSize = 16; + inline static const float kMenuWidthMultiplier = 1.2; + + sf::RenderWindow& mRenderWindow; + sf::RectangleShape mShape; + sf::RectangleShape mHoverShape; + sf::Text mText; + std::vector mButtons; + + bool mIsOpened = false; + bool mIsUpdated = false; + int mHoverPosition = -1; + + int onMousePressed(const sf::Event& event) + { + if (event.mouseButton.button == sf::Mouse::Right) { + mIsOpened = true; + sf::Vector2f mousePosition = mRenderWindow.mapPixelToCoords({event.mouseButton.x, event.mouseButton.y}); + mShape.setPosition(mousePosition); + } + if (event.mouseButton.button == sf::Mouse::Left && mIsOpened) { + mIsOpened = false; + return mHoverPosition; + } + return -1; + } + + void onMouseMove(const sf::Event& event) + { + if (!mIsOpened) { + return; + } + sf::Vector2f mousePosition = mRenderWindow.mapPixelToCoords({event.mouseMove.x, event.mouseMove.y}); + if (mShape.getGlobalBounds().contains(mousePosition)) { + mHoverPosition = (mousePosition.y - mShape.getPosition().y) / kButtonHeight; + } + else { + mHoverPosition = -1; + } + } + +public: + + ContextMenu(sf::RenderWindow& window, const sf::Font& font) : mRenderWindow(window) + { + mText.setFont(font); + mText.setCharacterSize(kCharacterSize); + mText.setFillColor(kTextColor); + mShape.setFillColor(kDefaultColor); + mHoverShape.setFillColor(kHoverColor); + + mIsOpened = false; + mIsUpdated = false; + mHoverPosition = -1; + } + + void addButton(const sf::String& name) + { + mButtons.push_back(name); + mIsUpdated = false; + } + + void draw() + { + if (!mIsOpened) { + return; + } + // Если добавили новый вариант, то её текст может быть длиннее + // чем у других. Нужно расширить прямоугольники. + if (!mIsUpdated) { + int maxSizeX = 0; + for (int i = 0; i < mButtons.size(); i++) { + mText.setString(mButtons[i]); + if (mText.getLocalBounds().width > maxSizeX) { + maxSizeX = mText.getLocalBounds().width; + } + } + maxSizeX *= kMenuWidthMultiplier; + mShape.setSize({(float)maxSizeX, (float)(kButtonHeight * mButtons.size())}); + mHoverShape.setSize({(float)maxSizeX, (float)(kButtonHeight)}); + mIsUpdated = true; + } + + + mRenderWindow.draw(mShape); + if (mHoverPosition >= 0){ + mHoverShape.setPosition(mShape.getPosition().x, mShape.getPosition().y + mHoverPosition * kButtonHeight); + mRenderWindow.draw(mHoverShape); + } + for (int i = 0; i < mButtons.size(); i++) { + mText.setString(mButtons[i]); + mText.setPosition(mShape.getPosition().x, mShape.getPosition().y + i * kButtonHeight); + mRenderWindow.draw(mText); + } + } + + int handleEvent(const sf::Event& event) { + if (event.type == sf::Event::MouseMoved) { + onMouseMove(event); + } + else if (event.type == sf::Event::MouseButtonPressed) { + return onMousePressed(event); + } + return -1; + } + + +int main() +{ + sf::RenderWindow window(sf::VideoMode(500, 500), "Slider!"); + window.setFramerateLimit(60); + + SliderSFML slider1(100, 100); + SliderSFML slider2(100, 200); + SliderSFML slider3(100, 300); + + slider1.create(20, 450); + slider2.create(0, 200); + slider3.create(0, 100); + + slider1.setSliderValue(235); + + while (window.isOpen()) + { + sf::Event event; + while (window.pollEvent(event)) + { + if (event.type == sf::Event::Closed) + window.close(); + } + + window.clear(sf::Color(25,29,33)); + + slider1.draw(window); + slider2.draw(window); + slider3.draw(window); + + window.display(); + } + + return 0; +}}; diff --git a/seminar11_events/01_select_move_delete/select_move_delete.cpp b/seminar11_events/01_select_move_delete/select_move_delete.cpp new file mode 100644 index 0000000..07e029a --- /dev/null +++ b/seminar11_events/01_select_move_delete/select_move_delete.cpp @@ -0,0 +1,314 @@ +#include +#include +#include +#include +#include +#include "context_menu.hpp" + +using namespace std; + +void meow() {;} + +// Вспомогательные функции, вычисляет расстояние между двумя точками +float distance(sf::Vector2f start, sf::Vector2f finish) +{ + return sqrtf((start.x - finish.x)*(start.x - finish.x) + (start.y - finish.y)*(start.y - finish.y)); +} + +// Вспомогательные функции, рисует линию на холсте окна window +void drawLine(sf::RenderWindow& window, sf::Vector2f start, sf::Vector2f finish, sf::Color color = sf::Color::White) +{ + sf::Vertex line_vertices[2] = {sf::Vertex(start, color), sf::Vertex(finish, color)}; + window.draw(line_vertices, 2, sf::Lines); +} + +// Вспомагательный класс, описывет шарик +// position - положение шарика; radius - радиус +// is_chosen - говорит о том, выбран ли шарик или нет +struct Ball +{ + sf::Vector2f position; + float radius; + bool isChoosen; + sf::Color color; + + Ball(sf::Vector2f position, float radius, sf::Color color = sf::Color::White) : position(position), radius(radius), color(color) + { + isChoosen = false; + } + + // Метод, который рисует шарик на холстек окна window + void draw(sf::RenderWindow& window) const + { + // Тут рисуем белый кружочек + sf::CircleShape circle(radius); + circle.setFillColor(color); + circle.setOrigin({radius, radius}); + circle.setPosition(position); + window.draw(circle); + + // Если шарик выбран + if (isChoosen) { + // То рисуем "уголки" + const float fraction = 0.7; + drawLine(window, {position.x - radius, position.y + radius}, {position.x - radius, position.y + radius*fraction}); + drawLine(window, {position.x - radius, position.y + radius}, {position.x - fraction * radius, position.y + radius}); + + drawLine(window, {position.x + radius, position.y + radius}, {position.x + radius, position.y + radius*fraction}); + drawLine(window, {position.x + radius, position.y + radius}, {position.x + radius*fraction, position.y + radius}); + + drawLine(window, {position.x + radius, position.y - radius}, {position.x + radius*fraction, position.y - radius}); + drawLine(window, {position.x + radius, position.y - radius}, {position.x + radius, position.y - radius*fraction}); + + drawLine(window, {position.x - radius, position.y - radius}, {position.x - radius*fraction, position.y - radius}); + drawLine(window, {position.x - radius, position.y - radius}, {position.x - radius, position.y - radius*fraction}); + } + } + void setColor(sf::Uint8 r, sf::Uint8 g, sf::Uint8 b, sf::Uint8 a = 255) { + color.r = r; + color.g = g; + color.b = b; + color.a = a; + } +}; + +void deleteChoosen(list& balls) { + for (list::const_iterator it = balls.begin(); it != balls.end();) { + if (it->isChoosen) + it = balls.erase(it); + else + ++it; + } +#ifdef _DEBUG + for (list::iterator it = balls.begin(); it != balls.end(); ++it) + cout << it->position.x <<" " << it->position.y<< endl; + cout << "###" << endl; +#endif +} + +void copyBalls(list& balls, list& buffer) { + buffer.clear(); + for (list::iterator it = balls.begin(); it != balls.end(); ++it) + if (it->isChoosen) + buffer.push_back(*it); +} + +void pasteBalls(list& balls, list& buffer) { + for (list::iterator it = buffer.begin(); it != buffer.end(); ++it) + balls.push_back(*it); +} + +void recolorChoosen(list& balls) { + for (Ball& b : balls) + if (b.isChoosen) + b.setColor(rand() % 255, rand() % 255, rand() % 255); +} + +void resizeChoosen(list& balls, double scaling) { + for (list::iterator it = balls.begin(); it != balls.end(); ++it) { + if (it->isChoosen) + it->radius += it->radius * scaling; + } +} + +int main() +{ + sf::ContextSettings settings; + settings.antialiasingLevel = 8; + sf::RenderWindow window(sf::VideoMode(800, 600), "Select, Move, Delete!", sf::Style::Default, settings); + window.setFramerateLimit(60); + + + // Создаём связный список из шариков + // Связный список -- потому что нам нужно будет постоянно удалять и добавлять в этот список + std::list balls; + balls.push_back(Ball({200, 200}, 26)); + balls.push_back(Ball({400, 300}, 20)); + balls.push_back(Ball({500, 100}, 16)); + balls.push_back(Ball({200, 400}, 18)); + balls.push_back(Ball({350, 150}, 22)); + balls.push_back(Ball({750, 400}, 21)); + + list buffer{}; + + // Создаём прямоугольник выделения + // Обратите внимание на четвёртый параметр sf::Color(150, 150, 240, 50) + // Это alpha - прозрачность 0 = полностью прозрачный 255 = непрозрачный + sf::RectangleShape selectionRect; + selectionRect.setFillColor(sf::Color(150, 150, 240, 50)); + selectionRect.setOutlineColor(sf::Color(200, 200, 255)); + selectionRect.setOutlineThickness(1); + + // Специальная переменная, которая будет показывать, что мы находимся в режиме выделения + bool isSelecting = false; + // "Режим выделения" + bool isMovingMode = false; + // Необходимо, чтоб в режиме выделения считать смещение + Ball* selected_ball = nullptr; + + bool isContextMenu = false; + + sf::Vector2f mousePosition{}; + + sf::Font font; + if (!font.loadFromFile("consolas.ttf")) { + std::cout << "Can't load button font" << std::endl; + } + + std::vector contextMenuStrings {"Delete", "Create", "Random Color", "Increase", "Decrease", "Copy", "Cut", "Paste"}; + ContextMenu contextMenu(window, font); + for (const auto& el : contextMenuStrings) { + contextMenu.addButton(el); + } + + while (window.isOpen()) { + sf::Event event; + while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + window.close(); + } + int result = contextMenu.handleEvent(event); + switch(result) { + case 0: + deleteChoosen(balls); + break; + case 1: + mousePosition = window.mapPixelToCoords({event.mouseButton.x, event.mouseButton.y}); + balls.push_back(Ball(mousePosition, 5 + rand() % 40)); + break; + case 2: + recolorChoosen(balls); + break; + case 3: + resizeChoosen(balls, 0.25); + break; + case 4: + resizeChoosen(balls, -0.25); + break; + case 5: + copyBalls(balls, buffer); + break; + case 6: + copyBalls(balls, buffer); + deleteChoosen(balls); + break; + case 7: + pasteBalls(balls, buffer); + break; + } + if (event.type == sf::Event::MouseMoved) { + mousePosition = window.mapPixelToCoords({event.mouseMove.x, event.mouseMove.y}); + + // Если мы находимся в режиме выделения и не попали в уже выделенный шарик, то меняем прямоугольник выделения + if (isSelecting & !isMovingMode) { + selectionRect.setSize(mousePosition - selectionRect.getPosition()); + } + // Если в режиме перемещения, то двигаем все выделенные шарики + if (isMovingMode) { + sf::Vector2f direction = selected_ball->position - mousePosition; + for (Ball& b : balls) { + if (b.isChoosen) { + b.position -= direction; + } + } + } + } + + if (event.type == sf::Event::MouseButtonPressed) { + mousePosition = window.mapPixelToCoords({event.mouseButton.x, event.mouseButton.y}); + if (event.mouseButton.button == sf::Mouse::Left) { + for (Ball& b : balls) { + /* Если попали в какой-то шарик */ + if (distance(mousePosition, b.position) < b.radius) { + selected_ball = &b; + // Если попали в еще не выбранный шарик и не зажат левый Ctrl, то снимаем выделение со всех остальных + if(!b.isChoosen && !sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) { + for (Ball& ball : balls) { + ball.isChoosen = false; + } + } + b.isChoosen = true; // + isMovingMode = true; + break; + } + } + // ЛКМ + левый Alt - добавляем новый случайный шарик + if (sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt)) { + balls.push_back(Ball(mousePosition, 5 + rand() % 40)); + } + // Задаём новое положения прямоугольника выделения + if (!isMovingMode) { + isSelecting = true; + selectionRect.setPosition(mousePosition); + selectionRect.setSize({0, 0}); + } + } + if (event.mouseButton.button == sf::Mouse::Right) { + isContextMenu = true; + } + } + // При отпускании кнопки мыши выходим из режима выделения + if (event.type == sf::Event::MouseButtonReleased) { + // Если не зажат левый Ctrl и не в режиме перемещения шариков, то все выделения снимаются + if (!sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) && !isMovingMode && !isContextMenu) { + for (Ball& b : balls) { + b.isChoosen = false; + } + } + if (isSelecting) { + sf::Vector2f size = selectionRect.getSize(); + sf::Vector2f position = selectionRect.getPosition(); + for (Ball& b : balls) { + if ( ((b.position.x - b.radius > position.x) && (b.position.x + b.radius < position.x + size.x)) || + ((b.position.x + b.radius < position.x) && (b.position.x - b.radius > position.x + size.x)) + ) + if (((b.position.y - b.radius > position.y) && (b.position.y + b.radius < position.y + size.y)) || + ((b.position.y + b.radius < position.y) && (b.position.y - b.radius > position.y + size.y)) + ) + b.isChoosen = true; + } + } + isSelecting = false; + isMovingMode = false; + isContextMenu = false; + } + /*2check*/ + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Space)) + recolorChoosen(balls); + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Delete)) { + deleteChoosen(balls); + } + + /**/ + if (event.type == sf::Event::KeyPressed) + if (event.key.code == sf::Keyboard::C) + if (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) { + copyBalls(balls, buffer); + } + if (event.type == sf::Event::KeyPressed) + if (event.key.code == sf::Keyboard::V) + if (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) + pasteBalls(balls, buffer); + if (event.type == sf::Event::KeyPressed) + if (event.key.code == sf::Keyboard::X) + if (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) { + copyBalls(balls, buffer); + deleteChoosen(balls); + } + } + + window.clear(sf::Color::Black); + // Рисуем все шарики + for (Ball& b : balls) { + b.draw(window); + } + // Рисуем прямоугольник выделения + if (isSelecting) { + window.draw(selectionRect); + } + contextMenu.draw(); + window.display(); + } + + return 0; +}