- Tab selection in linear view

- Cut/Copy/Paste selected notes
- LN displayed correctly in the Linear View
- bunch of other small stuff
This commit is contained in:
Stepland 2019-03-27 20:37:30 +01:00
parent e6d4267385
commit 8d44098063
24 changed files with 577 additions and 50 deletions

View File

@ -34,7 +34,23 @@ set(SOURCE_FILES
Chart.cpp
EditorState.cpp
${imgui}
tinyfiledialogs.c Toolbox.cpp Toolbox.h LNMarker.cpp LNMarker.h History.h HistoryActions.cpp HistoryActions.h NotificationsQueue.cpp NotificationsQueue.h Notification.cpp Notification.h SoundEffect.cpp SoundEffect.h)
tinyfiledialogs.c
Toolbox.cpp
Toolbox.h
LNMarker.cpp
LNMarker.h
History.h
HistoryActions.cpp
HistoryActions.h
NotificationsQueue.cpp
NotificationsQueue.h
Notification.cpp
Notification.h
SoundEffect.cpp
SoundEffect.h
TimeSelection.h
NotesClipboard.cpp
NotesClipboard.h)
#set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${FEIS_SOURCE_DIR}/cmake")
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH})

View File

@ -54,3 +54,44 @@ bool Chart::is_colliding(const Note &note, int ticks_threshold) {
return false;
}
/*
* This function does not take long notes into account and just gives back
* anything with a timing value between the two arguments, inclusive
*/
std::set<Note> Chart::getNotesBetween(int start_timing, int end_timing) const {
std::set<Note> res = {};
auto lower_bound = Notes.lower_bound(Note(0,start_timing));
auto upper_bound = Notes.upper_bound(Note(15,end_timing));
for (auto& note_it = lower_bound; note_it != upper_bound; ++note_it) {
res.insert(*note_it);
}
return res;
}
/*
* Takes long notes into account, gives back any note that would be visible between
* the two arguments, LN tails included
*/
std::set<Note> Chart::getVisibleNotesBetween(int start_timing, int end_timing) const {
auto res = getNotesBetween(start_timing, end_timing);
auto note_it = Notes.upper_bound(Note(0,start_timing));
std::set<Note>::reverse_iterator rev_note_it(note_it);
for (; rev_note_it != Notes.rend(); ++rev_note_it) {
if (rev_note_it->getLength() != 0) {
int end_tick = rev_note_it->getTiming() + rev_note_it->getLength();
if (end_tick >= start_timing and end_tick <= end_timing) {
res.insert(*rev_note_it);
}
}
}
return res;
}

View File

@ -10,6 +10,9 @@
#include <vector>
#include "Note.h"
/*
* Holds the notes, the difficulty name and the level
*/
class Chart {
public:
@ -25,6 +28,9 @@ public:
int level;
std::set<Note> Notes;
std::set<Note> getNotesBetween(int start_timing, int end_timing) const;
std::set<Note> getVisibleNotesBetween(int start_timing, int end_timing) const;
bool is_colliding(const Note &note, int ticks_threshold);
bool operator==(const Chart &rhs) const;

View File

@ -325,9 +325,10 @@ void EditorState::displayPlayfield(Marker& marker, MarkerEndingState markerEndin
}
}
// Check for collisions then display them
if (chart) {
// Check for collisions then display them
int ticks_threshold = static_cast<int>((1.f/60.f)*fumen.BPM*getResolution());
std::array<bool, 16> collisions = {};
@ -348,6 +349,19 @@ void EditorState::displayPlayfield(Marker& marker, MarkerEndingState markerEndin
++ImGuiIndex;
}
}
// Display selected notes
for (auto const& note : visibleNotes) {
if (chart->selectedNotes.find(note) != chart->selectedNotes.end()) {
int x = note.getPos()%4;
int y = note.getPos()/4;
ImGui::SetCursorPos({x * squareSize, TitlebarHeight + y * squareSize});
ImGui::PushID(ImGuiIndex);
ImGui::Image(playfield.note_selected, {squareSize, squareSize});
ImGui::PopID();
++ImGuiIndex;
}
}
}
}
ImGui::End();
@ -550,7 +564,7 @@ void EditorState::displayLinearView() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize,0);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(2,2));
if (ImGui::Begin("Linear View", &showLinearView,ImGuiWindowFlags_NoScrollbar)) {
linearView.update(chart->ref, playbackPosition, getTicks(), fumen.BPM, getResolution(), ImGui::GetContentRegionMax());
linearView.update(chart->ref, chart->selectedNotes, chart->timeSelection, playbackPosition, getTicks(), fumen.BPM, getResolution(), ImGui::GetContentRegionMax());
ImGui::SetCursorPos({0,ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.f});
ImGui::Image(linearView.view.getTexture(),ImVec2(0,1),ImVec2(1,0));
}

View File

@ -14,29 +14,40 @@
#include "History.h"
#include "HistoryActions.h"
#include "Widgets.h"
#include "TimeSelection.h"
#include "NotesClipboard.h"
class ActionWithMessage;
class OpenChart;
/*
* The god class, holds everything there is to know about the currently open .memon file
*/
class EditorState {
public:
struct Chart_with_History {
explicit Chart_with_History(Chart& c);
Chart& ref;
std::set<Note> selectedNotes;
NotesClipboard notesClipboard;
SelectionState timeSelection;
History<std::shared_ptr<ActionWithMessage>> history;
};
explicit EditorState(Fumen& fumen);
Fumen fumen;
std::optional<Chart_with_History> chart;
Fumen fumen;
Widgets::Playfield playfield;
Widgets::DensityGraph densityGraph;
Widgets::LinearView linearView;
// the snap but divided by 4 because you can't set a snap to anything lower than 4ths
int snap = 1;
std::optional<sf::Music> music;

12
Fumen.h
View File

@ -15,6 +15,9 @@
#include "Note.h"
#include "Chart.h"
/*
* Difficulty name ordering : BSC > ADV > EXT > anything else in lexicographical order
*/
struct cmpDifName {
std::map<std::string,int> dif_names;
@ -24,6 +27,9 @@ struct cmpDifName {
bool operator()(const std::string& a, const std::string& b) const;
};
/*
* Represents a .memon file : several charts and some metadata
*/
class Fumen {
public:
@ -41,14 +47,8 @@ public:
void loadFromMemon(std::filesystem::path path);
void loadFromMemon_v0_1_0(nlohmann::json j);
void loadFromMemon_fallback(nlohmann::json j);
// TODO : implementer ça
//void loadFromMemo(std::string path);
//void loadFromEve(std::string path);
void saveAsMemon(std::filesystem::path path);
// TODO : implementer ça
//void saveAsMemo(std::string path);
//void saveAsEve(std::string path);
void autoLoadFromMemon() {loadFromMemon(path);};
void autoSaveAsMemon() {saveAsMemon(path);};

View File

@ -30,7 +30,7 @@ class History {
public:
/*
* we cannot undo the very first action, which in my case corresponds to opening a chart
* we cannot undo the very first action, which in F.E.I.S corresponds to opening a chart
*/
std::optional<T> get_previous() {
if (previous_actions.size() == 1) {

View File

@ -15,6 +15,9 @@
class EditorState;
/*
* Base class for history actions (stuff that is stored inside of History objects)
*/
class ActionWithMessage {
public:
explicit ActionWithMessage(std::string message = "") : message(std::move(message)) {};

View File

@ -14,8 +14,9 @@
#include <SFML/Graphics/RenderTexture.hpp>
/*
* This approach at displaying rotated textures is absolutely terrible, I should just dig a little bit into the internals
* of Dear ImGui and pass in custom vertices and UVs when I want to rotate a texture but I don't feel confident enough rn
* Stores every rotated variant of the long note marker
* This approach is absolutely terrible, I should just dig a little bit into the internals
* of Dear ImGui and pass in custom UVs when I want to rotate a texture but I don't feel confident enough rn
*/
class LNMarker {

View File

@ -40,12 +40,15 @@ namespace Markers {
};
}
/*
* Holds the textures associated with a given marker folder from the assets folder
*/
class Marker {
public:
Marker();
Marker(std::filesystem::path folder);
explicit Marker(std::filesystem::path folder);
std::optional<std::reference_wrapper<sf::Texture>> getSprite(MarkerEndingState state, float seconds);
const std::map<std::string, sf::Texture> &getTextures() const;

42
Note.h
View File

@ -1,18 +1,50 @@
//
// Created by Syméon on 17/08/2017.
//
// Side note :
//
// Les notes ne contiennent pas en elles mêmes d'information sur leur timing, c'est la structure Fumen qui
// les contient qui s'en occupe
// The tail position could be stored by a number between 0 and 5, if you look at it carefully
// every note has exactly 6 different possible LN tail positions,
// Doing this would allow any tuple of 4 numbers verifying :
//
// Pour l'instant je vois pas trop la necessité de changer la position par contre la longeur et la position
// de la queue je pense que ça peut être utile
// 0 <= position <= 15
// 0 <= timing
// 0 <= length
// 0 <= tail position <= 5
//
// To count as a valid note, this would also allow for a purely json-schema based chart validation. since it
// removes the need for a fancy consistency check between the note and its tail positions
#ifndef FEIS_NOTE_H
#define FEIS_NOTE_H
/*
* A Note has :
*
* - a Position, from 0 to 15 given this way :
*
* 0 1 2 3
* 4 5 6 7
* 8 9 10 11
* 12 13 14 15
*
* - a Timing value, just a positive integer
*
* - a Length, set to 0 if not a long note, else a positive integer in the same unit as the note's timing
*
* - a Tail position, currently given this way :
*
* 8
* 4
* 0
* 11 7 3 . 1 5 9
* 2
* 6
* 10
*
* with the . marking the note position
* gets ignored if the length is zero
*/
class Note {
public:

27
NotesClipboard.cpp Normal file
View File

@ -0,0 +1,27 @@
//
// Created by Syméon on 27/03/2019.
//
#include "NotesClipboard.h"
NotesClipboard::NotesClipboard(const std::set<Note> &notes) {
copy(notes);
}
void NotesClipboard::copy(const std::set<Note> &notes) {
contents.clear();
if (not notes.empty()) {
int timing_offset = notes.cbegin()->getTiming();
for (const auto &note : notes) {
contents.emplace(note.getPos(), note.getTiming()-timing_offset, note.getLength(), note.getTail_pos());
}
}
}
std::set<Note> NotesClipboard::paste(int tick_offset) {
std::set<Note> res = {};
for (auto note : contents) {
res.emplace(note.getPos(), note.getTiming()+tick_offset, note.getLength(), note.getTail_pos());
}
return res;
}

24
NotesClipboard.h Normal file
View File

@ -0,0 +1,24 @@
//
// Created by Syméon on 27/03/2019.
//
#ifndef FEIS_NOTESCLIPBOARD_H
#define FEIS_NOTESCLIPBOARD_H
#include <set>
#include "Note.h"
class NotesClipboard {
public:
explicit NotesClipboard(const std::set<Note> &notes = {});
void copy(const std::set<Note> &notes);
std::set<Note> paste(int tick_offset);
bool empty() {return contents.empty();};
private:
std::set<Note> contents;
};
#endif //FEIS_NOTESCLIPBOARD_H

View File

@ -9,6 +9,9 @@
#include <string>
#include "HistoryActions.h"
/*
* The display function should call ImGui primitives to display arbitrary stuff in the notifications queue
*/
class Notification {
public:
virtual void display() const = 0;
@ -16,7 +19,9 @@ public:
virtual ~Notification() = default;
};
/*
* Displays the string given to the constructor
*/
class TextNotification : public Notification {
public:
explicit TextNotification(const std::string &message);
@ -26,6 +31,9 @@ public:
const std::string message;
};
/*
* Displays "Undo" in orange followed by the message associated with the action passed to the constructor
*/
class UndoNotification : public Notification {
public:
explicit UndoNotification(const ActionWithMessage& awm) : message(awm.getMessage()) {};
@ -35,6 +43,9 @@ public:
const std::string message;
};
/*
* Displays "Redo" in blue followed by the message associated with the action passed to the constructor
*/
class RedoNotification : public Notification {
public:
explicit RedoNotification(const ActionWithMessage& awm) : message(awm.getMessage()) {};

View File

@ -9,6 +9,9 @@
#include <SFML/System.hpp>
#include "Notification.h"
/*
* Responsible for displaying the notifications with a fadeout effect
*/
class NotificationsQueue {
public:
explicit NotificationsQueue(int max_size = 10): max_size(max_size) {};

View File

@ -9,6 +9,9 @@
#include <iostream>
#include <SFML/Audio.hpp>
/*
* Holds an sf::Sound and can display some controls associated with it (volume and on/off toggle)
*/
class SoundEffect {
public:
explicit SoundEffect(std::string filename);

22
TimeSelection.h Normal file
View File

@ -0,0 +1,22 @@
//
// Created by Syméon on 27/03/2019.
//
#ifndef FEIS_TIMESELECTION_H
#define FEIS_TIMESELECTION_H
#include <variant>
struct TimeSelection {
explicit TimeSelection(unsigned int start = 0, unsigned int duration = 0) : start(start), duration(duration) {};
unsigned int start;
unsigned int duration;
};
typedef std::variant<std::monostate,unsigned int,TimeSelection> SelectionState;
#endif //FEIS_TIMESELECTION_H

View File

@ -185,3 +185,18 @@ Toolbox::displayIfHasValue(const std::optional<std::reference_wrapper<sf::Textur
++index;
}
}
void Toolbox::center(sf::Shape &s) {
sf::FloatRect bounds = s.getLocalBounds();
s.setOrigin(bounds.left + bounds.width/2.f, bounds.top + bounds.height/2.f);
}
bool Toolbox::editFillColor(const char* label, sf::Shape& s) {
sf::Color col = s.getFillColor();
if (ImGui::ColorEdit4(label, col)) {
s.setFillColor(col);
return true;
}
return false;
}

View File

@ -13,6 +13,9 @@
#include <SFML/Graphics/Texture.hpp>
#include <imgui-SFML.h>
/*
* I just dump things here where I'm unsure whether they deserve a special file for them or not
*/
namespace Toolbox {
struct CustomColors {
@ -42,8 +45,9 @@ namespace Toolbox {
int getNextDivisor(int number, int starting_point);
int getPreviousDivisor(int number, int starting_point);
std::string toOrdinal(int number);
void displayIfHasValue(const std::optional<std::reference_wrapper<sf::Texture>>& tex, ImVec2 cursorPosition, ImVec2 texSize, int& index);
void center(sf::Shape& s);
bool editFillColor(const char* label, sf::Shape& s);
}
template<typename T>

View File

@ -5,7 +5,7 @@
#include "Widgets.h"
#include "Toolbox.h"
Widgets::Ecran_attente::Ecran_attente() : gris_de_fond(sf::Color(38,38,38)) {
Widgets::BlankScreen::BlankScreen() : gris_de_fond(sf::Color(38,38,38)) {
if(!tex_FEIS_logo.loadFromFile("assets/textures/FEIS_logo.png"))
{
@ -17,7 +17,7 @@ Widgets::Ecran_attente::Ecran_attente() : gris_de_fond(sf::Color(38,38,38)) {
}
void Widgets::Ecran_attente::render(sf::RenderWindow& window) {
void Widgets::BlankScreen::render(sf::RenderWindow& window) {
// effacement de la fenêtre en noir
window.clear(gris_de_fond);
@ -40,6 +40,9 @@ Widgets::Playfield::Playfield() {
button_pressed.setTexture(base_texture);
button_pressed.setTextureRect({192,0,192,192});
note_selected.setTexture(base_texture);
note_selected.setTextureRect({384,0,192,192});
note_collision.setTexture(base_texture);
note_collision.setTextureRect({576,0,192,192});
}
@ -138,7 +141,10 @@ void Widgets::LinearView::setZoom(int newZoom) {
zoom = std::clamp(newZoom,-5,5);
}
void Widgets::LinearView::update(std::optional<Chart> chart, sf::Time playbackPosition, float ticksAtPlaybackPosition, float BPM, int resolution, ImVec2 size) {
void Widgets::LinearView::update(const std::optional<Chart> &chart, const std::set<Note> &selectedNotes,
const SelectionState &selectionState, const sf::Time &playbackPosition,
const float &ticksAtPlaybackPosition, const float &BPM, const int &resolution,
const ImVec2 &size) {
int x = std::max(140, static_cast<int>(size.x));
int y = std::max(140, static_cast<int>(size.y));
@ -154,10 +160,14 @@ void Widgets::LinearView::update(std::optional<Chart> chart, sf::Time playbackPo
AffineTransform<float> SecondsToTicks(playbackPosition.asSeconds()-(60.f/BPM)/timeFactor(), playbackPosition.asSeconds(), ticksAtPlaybackPosition-resolution/timeFactor(), ticksAtPlaybackPosition);
AffineTransform<float> PixelsToSeconds(-25.f, 75.f, playbackPosition.asSeconds()-(60.f/BPM)/timeFactor(), playbackPosition.asSeconds());
AffineTransform<float> PixelsToSecondsProprotional(0.f, 100.f, 0.f, (60.f/BPM)/timeFactor());
AffineTransform<float> PixelsToTicks(-25.f, 75.f, ticksAtPlaybackPosition-resolution/timeFactor(), ticksAtPlaybackPosition);
// Draw the beat lines and numbers
/*
* Draw the beat lines and numbers
*/
int next_beat_tick = ((1 + (static_cast<int>(PixelsToTicks.transform(0.f))+resolution)/resolution) * resolution) - resolution;
int next_beat = std::max(0,next_beat_tick/resolution);
next_beat_tick = next_beat*resolution;
@ -201,38 +211,101 @@ void Widgets::LinearView::update(std::optional<Chart> chart, sf::Time playbackPo
next_beat_line_y = PixelsToTicks.backwards_transform(static_cast<float>(next_beat_tick));
}
// Draw the notes;
/*
* Draw the notes
*/
// Size & center the shapes
float note_width = (static_cast<float>(x)-80.f)/16.f;
note_rect.setSize({note_width,6.f});
sf::FloatRect note_bounds = note_rect.getLocalBounds();
note_rect.setOrigin(note_bounds.left + note_bounds.width/2.f, note_bounds.top + note_bounds.height/2.f);
Toolbox::center(note_rect);
float collision_zone_size = 10.f*(60.f/BPM)*100.f*timeFactor();
note_selected.setSize({note_width+2.f,8.f});
Toolbox::center(note_selected);
float collision_zone_size = PixelsToSecondsProprotional.backwards_transform(1.f);
note_collision_zone.setSize({(static_cast<float>(x)-80.f)/16.f-2.f,collision_zone_size});
sf::FloatRect collision_zone_bounds = note_collision_zone.getLocalBounds();
note_collision_zone.setOrigin(collision_zone_bounds.left + collision_zone_bounds.width/2.f, collision_zone_bounds.top + collision_zone_bounds.height/2.f);
Toolbox::center(note_collision_zone);
long_note_collision_zone.setSize({(static_cast<float>(x)-80.f)/16.f-2.f,collision_zone_size});
Toolbox::center(long_note_collision_zone);
// Find the notes that need to be displayed
int lower_bound_ticks = std::max(0,static_cast<int>(SecondsToTicks.transform(PixelsToSeconds.transform(0.f)-0.5f)));
int upper_bound_ticks = std::max(0,static_cast<int>(SecondsToTicks.transform(PixelsToSeconds.transform(static_cast<float>(y))+0.5f)));
auto lower_note = chart->Notes.lower_bound(Note(0,lower_bound_ticks));
auto upper_note = chart->Notes.upper_bound(Note(15,upper_bound_ticks));
for (auto& note : chart->getVisibleNotesBetween(lower_bound_ticks,upper_bound_ticks)) {
if (lower_note != chart->Notes.end()) {
for (auto note = lower_note; note != chart->Notes.end() and note != upper_note; ++note) {
float note_x = 50.f+note_width*(note->getPos()+0.5f);
float note_y = PixelsToTicks.backwards_transform(static_cast<float>(note->getTiming()));
note_rect.setPosition(note_x,note_y);
note_collision_zone.setPosition(note_x,note_y);
view.draw(note_rect);
float note_x = 50.f+note_width*(note.getPos()+0.5f);
float note_y = PixelsToTicks.backwards_transform(static_cast<float>(note.getTiming()));
note_rect.setPosition(note_x,note_y);
note_selected.setPosition(note_x,note_y);
note_collision_zone.setPosition(note_x,note_y);
if (note.getLength() != 0) {
float tail_size = PixelsToSecondsProprotional.backwards_transform(SecondsToTicks.backwards_transform(note.getLength()));
float long_note_collision_size = collision_zone_size + tail_size;
long_note_collision_zone.setSize({(static_cast<float>(x)-80.f)/16.f-2.f,collision_zone_size});
Toolbox::center(long_note_collision_zone);
long_note_collision_zone.setSize({(static_cast<float>(x)-80.f)/16.f-2.f,long_note_collision_size});
long_note_collision_zone.setPosition(note_x,note_y);
view.draw(long_note_collision_zone);
float tail_width = .75f*(static_cast<float>(x)-80.f)/16.f;
long_note_rect.setSize({tail_width,tail_size});
sf::FloatRect long_note_bounds = long_note_rect.getLocalBounds();
long_note_rect.setOrigin(long_note_bounds.left + long_note_bounds.width/2.f, long_note_bounds.top);
long_note_rect.setPosition(note_x,note_y);
view.draw(long_note_rect);
} else {
view.draw(note_collision_zone);
}
view.draw(note_rect);
if (selectedNotes.find(note) != selectedNotes.end()) {
view.draw(note_selected);
}
}
// Draw the cursor
cursor.setSize(sf::Vector2f(static_cast<float>(x)-76.f,4.f));
/*
* Draw the cursor
*/
cursor.setSize({static_cast<float>(x)-76.f,4.f});
view.draw(cursor);
/*
* Draw the timeSelection
*/
selection.setSize({static_cast<float>(x)-80.f,0.f});
if (std::holds_alternative<unsigned int>(selectionState)) {
unsigned int ticks = std::get<unsigned int>(selectionState);
float selection_y = PixelsToTicks.backwards_transform(static_cast<float>(ticks));
if (selection_y > 0.f and selection_y < static_cast<float>(y)) {
selection.setPosition(50.f,selection_y);
view.draw(selection);
}
} else if (std::holds_alternative<TimeSelection>(selectionState)) {
const auto& ts = std::get<TimeSelection>(selectionState);
float selection_start_y = PixelsToTicks.backwards_transform(static_cast<float>(ts.start));
float selection_end_y = PixelsToTicks.backwards_transform(static_cast<float>(ts.start+ts.duration));
if (
(selection_start_y > 0.f and selection_start_y < static_cast<float>(y)) or
(selection_end_y > 0.f and selection_end_y < static_cast<float>(y))
) {
selection.setSize({static_cast<float>(x)-80.f,selection_end_y-selection_start_y});
selection.setPosition(50.f,selection_start_y);
view.draw(selection);
}
}
}
}
@ -243,10 +316,34 @@ Widgets::LinearView::LinearView() {
std::cerr << "Unable to load " << font_path;
throw std::runtime_error("Unable to load" + font_path);
}
cursor.setFillColor(sf::Color(66,150,250,200));
cursor.setOrigin(0.f,2.f);
cursor.setPosition({48.f,75.f});
note_rect.setFillColor(sf::Color(230,179,0,255));
selection.setFillColor(sf::Color(153,255,153,92));
selection.setOutlineColor(sf::Color(153,255,153,189));
selection.setOutlineThickness(1.f);
note_rect.setFillColor(sf::Color(255,213,0,255));
note_selected.setFillColor(sf::Color(255,255,255,200));
note_selected.setOutlineThickness(1.f);
note_collision_zone.setFillColor(sf::Color(230,179,0,127));
long_note_rect.setFillColor(sf::Color(255,90,0,223));
long_note_collision_zone.setFillColor(sf::Color(230,179,0,127));
}
void Widgets::LinearView::displaySettings() {
if (ImGui::Begin("Linear View Settings",&shouldDisplaySettings)) {
Toolbox::editFillColor("Cursor", cursor);
Toolbox::editFillColor("Note", note_rect);
if(Toolbox::editFillColor("Note Collision Zone", note_collision_zone)) {
long_note_collision_zone.setFillColor(note_collision_zone.getFillColor());
}
Toolbox::editFillColor("Long Note Tail", long_note_rect);
}
ImGui::End();
}

View File

@ -10,13 +10,23 @@
#include "Marker.h"
#include "LNMarker.h"
#include "Chart.h"
#include "TimeSelection.h"
/*
* I create a widget whenever I want some graphical thing to hold some data and not just reload it on every frame
* for instance :
* - BlankScreen holds the texture for the FEIS logo
* - Playfield holds the textures for the buttons
* - LinearView holds its font and textures
* - DensityGraph holds all the density info and some textures
*/
namespace Widgets {
class Ecran_attente {
class BlankScreen {
public:
Ecran_attente();
BlankScreen();
void render(sf::RenderWindow &window);
private:
@ -35,6 +45,7 @@ namespace Widgets {
sf::Texture base_texture;
sf::Sprite button;
sf::Sprite button_pressed;
sf::Sprite note_selected;
sf::Sprite note_collision;
LNMarker longNoteMarker;
@ -51,16 +62,33 @@ namespace Widgets {
sf::RenderTexture view;
sf::Font beat_number_font;
sf::RectangleShape cursor;
sf::RectangleShape selection;
sf::RectangleShape note_rect;
sf::RectangleShape long_note_rect;
sf::RectangleShape long_note_collision_zone;
sf::RectangleShape note_selected;
sf::RectangleShape note_collision_zone;
void update(std::optional<Chart> chart, sf::Time playbackPosition, float ticksAtPlaybackPosition, float BPM, int resolution, ImVec2 size);
void update(
const std::optional<Chart>& chart,
const std::set<Note>& selectedNotes,
const SelectionState& selectionState,
const sf::Time& playbackPosition,
const float& ticksAtPlaybackPosition,
const float& BPM,
const int& resolution,
const ImVec2& size
);
void setZoom(int zoom);
void zoom_in() {setZoom(zoom+1);};
void zoom_out() {setZoom(zoom-1);};
float timeFactor() {return std::pow(1.25f,static_cast<float>(zoom));};
bool shouldDisplaySettings;
void displaySettings();
private:
int zoom = 0;

View File

@ -559,6 +559,27 @@ bool ImageButton(const sf::Sprite& sprite, const sf::Vector2f& size,
return ::imageButtonImpl(*texturePtr, static_cast<sf::FloatRect>(sprite.getTextureRect()), size, framePadding, bgColor, tintColor);
}
/////////////// Custom-custom stuff for F.E.I.S
bool ColorEdit4(const char* label, sf::Color& col, ImGuiColorEditFlags flags) {
std::array<float, 4> array_col = {
static_cast<float>(col.r)/255.f,
static_cast<float>(col.g)/255.f,
static_cast<float>(col.b)/255.f,
static_cast<float>(col.a)/255.f
};
if (ImGui::ColorEdit4(label,array_col.data(),flags)) {
col.r = static_cast<sf::Uint8>(array_col[0]*255.f);
col.g = static_cast<sf::Uint8>(array_col[1]*255.f);
col.b = static_cast<sf::Uint8>(array_col[2]*255.f);
col.a = static_cast<sf::Uint8>(array_col[3]*255.f);
return true;
}
return false;
}
/////////////// Draw_list Overloads
void DrawLine(const sf::Vector2f& a, const sf::Vector2f& b, const sf::Color& color,

View File

@ -89,6 +89,10 @@ namespace SFML
const sf::Color& bgColor = sf::Color::Transparent,
const sf::Color& tintColor = sf::Color::White);
// Custom custom stuff for F.E.I.S
bool ColorEdit4(const char* label, sf::Color& col, ImGuiColorEditFlags flags = 0);
// Draw_list overloads. All positions are in relative coordinates (relative to top-left of the current window)
void DrawLine(const sf::Vector2f& a, const sf::Vector2f& b, const sf::Color& col, float thickness = 1.0f);
void DrawRect(const sf::FloatRect& rect, const sf::Color& color, float rounding = 0.0f, int rounding_corners = 0x0F, float thickness = 1.0f);

145
main.cpp
View File

@ -2,19 +2,21 @@
#include <imgui.h>
#include <imgui-SFML.h>
#include <imgui_stdlib.h>
#include <variant>
#include "Widgets.h"
#include "EditorState.h"
#include "tinyfiledialogs.h"
#include "Toolbox.h"
#include "NotificationsQueue.h"
#include "SoundEffect.h"
#include "TimeSelection.h"
int main(int argc, char** argv) {
// TODO : Selection system
// TODO : Long notes editing
// TODO : A small preference persistency system (marker , etc ...)
// TODO : Debug Log
// TODO : Rewrite the terrible LNMarker stuff
// Création de la fenêtre
sf::RenderWindow window(sf::VideoMode(800, 600), "FEIS");
@ -48,7 +50,7 @@ int main(int argc, char** argv) {
Marker& marker = defaultMarker;
MarkerEndingState markerEndingState = MarkerEndingState_MISS;
Widgets::Ecran_attente bg;
Widgets::BlankScreen bg;
std::optional<EditorState> editorState;
NotificationsQueue notificationsQueue;
ESHelper::NewChartDialog newChartDialog;
@ -69,6 +71,75 @@ int main(int argc, char** argv) {
break;
case sf::Event::KeyPressed:
switch (event.key.code) {
/*
* Selection related stuff
*/
// Discard, in that order : timeSelection, selected_notes
case sf::Keyboard::Escape:
if (editorState and editorState->chart) {
if (not std::holds_alternative<std::monostate>(editorState->chart->timeSelection)) {
editorState->chart->timeSelection.emplace<std::monostate>();
} else if (not editorState->chart->selectedNotes.empty()) {
editorState->chart->selectedNotes.clear();
}
}
break;
// Modify timeSelection
case sf::Keyboard::Tab:
if (editorState and editorState->chart) {
// if no timeSelection was previously made
if (std::holds_alternative<std::monostate>(editorState->chart->timeSelection)) {
// set the start of the timeSelection to the current time
editorState->chart->timeSelection = static_cast<unsigned int>(editorState->getTicks());
// if the start of the timeSelection is already set
} else if (std::holds_alternative<unsigned int>(editorState->chart->timeSelection)) {
auto current_tick = static_cast<int>(editorState->getTicks());
auto selection_start = static_cast<int>(std::get<unsigned int>(editorState->chart->timeSelection));
// if we are on the same tick as the timeSelection start we discard the timeSelection
if (current_tick == selection_start) {
editorState->chart->timeSelection.emplace<std::monostate>();
// else we create a full timeSelection while paying attention to the order
} else {
auto new_selection_start = static_cast<unsigned int>(std::min(current_tick, selection_start));
auto duration = static_cast<unsigned int>(std::abs(current_tick - selection_start));
editorState->chart->timeSelection.emplace<TimeSelection>(new_selection_start,duration);
editorState->chart->selectedNotes = editorState->chart->ref.getNotesBetween(new_selection_start, new_selection_start+duration);
}
// if a full timeSelection already exists
} else if (std::holds_alternative<TimeSelection>(editorState->chart->timeSelection)) {
// discard the current timeSelection and set the start of the timeSelection to the current time
editorState->chart->timeSelection = static_cast<unsigned int>(editorState->getTicks());
}
}
break;
// Delete selected notes from the chart and discard timeSelection
case sf::Keyboard::Delete:
if (editorState and editorState->chart) {
if (not editorState->chart->selectedNotes.empty()) {
editorState->chart->history.push(std::make_shared<ToggledNotes>(editorState->chart->selectedNotes,false));
notificationsQueue.push(std::make_shared<TextNotification>("Deleted selected notes"));
for (auto note : editorState->chart->selectedNotes) {
editorState->chart->ref.Notes.erase(note);
}
editorState->chart->selectedNotes.clear();
}
}
break;
/*
* Arrow keys
*/
case sf::Keyboard::Up:
if (event.key.shift) {
if (editorState) {
@ -144,6 +215,10 @@ int main(int argc, char** argv) {
}
}
break;
/*
* F keys
*/
case sf::Keyboard::F3:
if (beatTick.toggle()) {
notificationsQueue.push(std::make_shared<TextNotification>("Beat tick : on"));
@ -187,6 +262,24 @@ int main(int argc, char** argv) {
notificationsQueue.push(std::make_shared<TextNotification>("Zoom out"));
}
break;
/*
* Letter keys, in alphabetical order
*/
case sf::Keyboard::C:
if (event.key.control) {
if (editorState and editorState->chart and (not editorState->chart->selectedNotes.empty())) {
std::stringstream ss;
ss << "Copied " << editorState->chart->selectedNotes.size() << " note";
if (editorState->chart->selectedNotes.size() > 1) {
ss << "s";
}
notificationsQueue.push(std::make_shared<TextNotification>(ss.str()));
editorState->chart->notesClipboard.copy(editorState->chart->selectedNotes);
}
}
break;
case sf::Keyboard::O:
if (event.key.control) {
ESHelper::open(editorState);
@ -203,6 +296,48 @@ int main(int argc, char** argv) {
notificationsQueue.push(std::make_shared<TextNotification>("Saved file"));
}
break;
case sf::Keyboard::V:
if (event.key.control) {
if (editorState and editorState->chart and (not editorState->chart->notesClipboard.empty())) {
int tick_offset = static_cast<int>(editorState->getTicks());
std::set<Note> pasted_notes = editorState->chart->notesClipboard.paste(tick_offset);
std::stringstream ss;
ss << "Pasted " << pasted_notes.size() << " note";
if (pasted_notes.size() > 1) {
ss << "s";
}
notificationsQueue.push(std::make_shared<TextNotification>(ss.str()));
for (auto note : pasted_notes) {
editorState->chart->ref.Notes.insert(note);
}
editorState->chart->selectedNotes = pasted_notes;
editorState->chart->history.push(std::make_shared<ToggledNotes>(editorState->chart->selectedNotes,true));
}
}
break;
case sf::Keyboard::X:
if (event.key.control) {
if (editorState and editorState->chart and (not editorState->chart->selectedNotes.empty())) {
std::stringstream ss;
ss << "Cut " << editorState->chart->selectedNotes.size() << " note";
if (editorState->chart->selectedNotes.size() > 1) {
ss << "s";
}
notificationsQueue.push(std::make_shared<TextNotification>(ss.str()));
editorState->chart->notesClipboard.copy(editorState->chart->selectedNotes);
for (auto note : editorState->chart->selectedNotes) {
editorState->chart->ref.Notes.erase(note);
}
editorState->chart->history.push(std::make_shared<ToggledNotes>(editorState->chart->selectedNotes,false));
editorState->chart->selectedNotes.clear();
}
}
break;
case sf::Keyboard::Y:
if (event.key.control) {
if (editorState and editorState->chart) {
@ -315,6 +450,9 @@ int main(int argc, char** argv) {
if (editorState->showLinearView) {
editorState->displayLinearView();
}
if (editorState->linearView.shouldDisplaySettings) {
editorState->linearView.displaySettings();
}
if (editorState->showProperties) {
editorState->displayProperties();
}
@ -475,6 +613,9 @@ int main(int argc, char** argv) {
if (ImGui::MenuItem("Sound")) {
editorState->showSoundSettings = true;
}
if (ImGui::MenuItem("Linear View")) {
editorState->linearView.shouldDisplaySettings = true;
}
if (ImGui::BeginMenu("Marker")) {
int i = 0;
for (auto& tuple : markerPreviews) {