lots of UI stuff
parent
14f1f2d783
commit
d8af8f463b
38
notes
38
notes
|
@ -1,8 +1,7 @@
|
|||
pattern editor {
|
||||
iosevka works perfectly for tracker patterns
|
||||
C-4 01 vFF
|
||||
can freely embed it too (SIL OFL allows bundling)
|
||||
probably Iosevka Term (Light?)
|
||||
IMPORTANT LINKS {
|
||||
https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/
|
||||
lowpass filter https://www.embeddedrelated.com/showarticle/779.php
|
||||
|
||||
}
|
||||
|
||||
project data {
|
||||
|
@ -47,21 +46,28 @@ TODO {
|
|||
immediate frontburner {
|
||||
- allow swapping channels around
|
||||
|
||||
double click to rename channel
|
||||
^ QHeaderView is a QAbstractItemView as of qt5, so can set an item delegate
|
||||
- double click to rename channel - popup for now since QHeaderView doesn't actually implement editing via delegate...
|
||||
|
||||
create new Project and hook that up to the main window instead of just making an empty pattern
|
||||
( Project::new(), Pattern::new(Project*) )
|
||||
make and hook up UI for displaying all existing patterns by index, and for displaying the sequence
|
||||
- right click channel header:
|
||||
- rename
|
||||
- add
|
||||
- delete
|
||||
|
||||
toggle box for expand-to-name
|
||||
|
||||
- create new Project and hook that up to the main window instead of just making an empty pattern
|
||||
make and hook up UI for displaying all existing patterns by index...
|
||||
...and for displaying the sequence
|
||||
implement drag and drop for pattern swapping/rearrangement (std::rotate)
|
||||
add miscellaneous editables (artist, song title, project bpm; pattern name, length etc.)
|
||||
make splitter collapse update pattern editor
|
||||
|
||||
- make pattern editor detect ctrl and alt modifiers
|
||||
make everything relevant check if editing is locked
|
||||
|
||||
- figure out what library to use for messagepack (official msgpack-c)
|
||||
implement saving (probably a FileIO helper class)
|
||||
hook up new/open/save menu items
|
||||
- hook up new/open/save menu items
|
||||
}
|
||||
|
||||
? de-hardcode the "» " (probably just make it a static const variable somewhere?)
|
||||
|
@ -127,6 +133,7 @@ graph+node+port system {
|
|||
this way, we don't bother processing anything that doesn't actually contribute to output in any way
|
||||
|
||||
before playback starts, playback thread assembles an expanded queue (graph has a function that recursively expands queue given a ref to a pre-reserved vector)
|
||||
note: also preassemble a map of all unique channel names for note number purposes
|
||||
can use the same logic to count active nodes (make sure to include the graph i/o ports unless implementing those another way? actually yeah, explicit pull operation)
|
||||
... how will the worker threads tell when they've outpaced the queue, and how will they wait properly?
|
||||
something something ready flags (all input nodes and containing graph; main graph and command ports always have the bit set)
|
||||
|
@ -142,6 +149,15 @@ graph+node+port system {
|
|||
(most built-in gadgets can avoid this by nature of aligned power-of-two types (<=8bytes) inherently atomic on modern CPUs)
|
||||
}
|
||||
|
||||
on-the-wire command format {
|
||||
ushort noteId // for sending commands to the same note
|
||||
short note // note number >= 0, -1 for none, -2 note off, -3 hard cut
|
||||
unsigned char numParams * {
|
||||
unsigned char cmd
|
||||
unsigned char amount
|
||||
}
|
||||
}
|
||||
|
||||
keybinds {
|
||||
pattern editor {
|
||||
note column {
|
||||
|
|
|
@ -4,6 +4,7 @@ using Row = Pattern::Row;
|
|||
using Channel = Pattern::Channel;
|
||||
|
||||
Row Pattern::fallbackRow(-1337, -1337);
|
||||
Channel Pattern::fallbackChannel(0);
|
||||
std::array<unsigned char, 2> Row::fallbackParam {'.', 0};
|
||||
|
||||
|
||||
|
@ -30,6 +31,22 @@ void Pattern::setLength(int r) {
|
|||
}
|
||||
}
|
||||
|
||||
void Pattern::addChannel(int at) {
|
||||
if (at < 0 || at > static_cast<int>(channels.size())) at = static_cast<int>(channels.size());
|
||||
channels.insert(channels.begin() + at, rows);
|
||||
}
|
||||
|
||||
void Pattern::deleteChannel(int at) {
|
||||
if (at < 0 || at >= static_cast<int>(channels.size())) at = static_cast<int>(channels.size()) - 1;
|
||||
channels.erase(channels.begin() + at);
|
||||
}
|
||||
|
||||
Channel& Pattern::channel(int c) {
|
||||
auto cc = static_cast<size_t>(c);
|
||||
if (cc >= channels.size()) return fallbackChannel;
|
||||
return channels[cc];
|
||||
}
|
||||
|
||||
Row& Pattern::rowAt(int c, int r) {
|
||||
auto nc = this->channels.size();
|
||||
if (nc == 0 || rows == 0) return fallbackRow;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
@ -71,12 +72,14 @@ namespace Xybrid::Data {
|
|||
};
|
||||
private:
|
||||
static Row fallbackRow;
|
||||
static Channel fallbackChannel;
|
||||
|
||||
public:
|
||||
// raw pointer is fine for now, since a project will never be destroyed without explicitly orphaning patterns
|
||||
// (and probably deleting them since basically the only reason one would be kept alive is if it's open in the pattern editor,
|
||||
// which would then immediately update with a pattern from opening a new project, or the window would close)
|
||||
Project* project;
|
||||
size_t index; // index in project's pattern list
|
||||
|
||||
std::string name;
|
||||
|
||||
|
@ -88,7 +91,14 @@ namespace Xybrid::Data {
|
|||
Pattern(int rows, int channels);
|
||||
|
||||
void setLength(int rows);
|
||||
void addChannel(int at = -1);
|
||||
void deleteChannel(int at);
|
||||
|
||||
size_t numChannels() {
|
||||
return channels.size();
|
||||
}
|
||||
|
||||
Channel& channel(int channel);
|
||||
Row& rowAt(int channel, int row);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,2 +1,20 @@
|
|||
#include "project.h"
|
||||
using Xybrid::Data::Project;
|
||||
using Xybrid::Data::Pattern;
|
||||
|
||||
Project::~Project() {
|
||||
// orphan patterns so they're not pointing at a non-project
|
||||
for (auto& pat : patterns) pat->project = nullptr;
|
||||
}
|
||||
|
||||
void Project::updatePatternIndices() {
|
||||
for (size_t i = 0; i < patterns.size(); i++) patterns[i]->index = i;
|
||||
}
|
||||
|
||||
std::shared_ptr<Pattern> Project::newPattern() {
|
||||
auto pt = std::make_shared<Pattern>();
|
||||
pt->project = this;
|
||||
pt->index = patterns.size();
|
||||
patterns.push_back(pt);
|
||||
return pt;
|
||||
}
|
||||
|
|
|
@ -7,16 +7,26 @@
|
|||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "data/pattern.h"
|
||||
|
||||
namespace Xybrid {
|
||||
class UISocket;
|
||||
}
|
||||
|
||||
namespace Xybrid::Data {
|
||||
class Project {
|
||||
public:
|
||||
bool editingLocked = false;
|
||||
|
||||
UISocket* socket;
|
||||
|
||||
std::string title;
|
||||
std::string artist;
|
||||
|
||||
QString fileName;
|
||||
|
||||
size_t sampleRate = 48000; // global sr for rendering
|
||||
float tempo = 140.0;
|
||||
// default time signature
|
||||
|
@ -27,5 +37,12 @@ namespace Xybrid::Data {
|
|||
|
||||
//std::shared_ptr<Graph> mainGraph;
|
||||
// list of input nodes is just part of mainGraph
|
||||
|
||||
Project() = default;
|
||||
~Project();
|
||||
|
||||
void updatePatternIndices();
|
||||
|
||||
std::shared_ptr<Pattern> newPattern();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,9 +5,13 @@ using Xybrid::MainWindow;
|
|||
#include <QDebug>
|
||||
#include <QKeyEvent>
|
||||
#include <QTabWidget>
|
||||
#include <QFileDialog>
|
||||
|
||||
#include "ui/patterneditoritemdelegate.h"
|
||||
|
||||
using Xybrid::Data::Project;
|
||||
using Xybrid::Data::Pattern;
|
||||
|
||||
using Xybrid::UI::PatternEditorModel;
|
||||
using Xybrid::UI::PatternEditorItemDelegate;
|
||||
|
||||
|
@ -29,18 +33,12 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
//mb->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Maximum, QSizePolicy::Policy::Maximum));
|
||||
//mb->resize(mb->size().width(), t->tabBar()->size().height());
|
||||
|
||||
// prevent right pane of pattern view from being collapsed
|
||||
ui->patternViewSplitter->setCollapsible(1, false);
|
||||
|
||||
pattern.reset(new Xybrid::Data::Pattern(64, 8));
|
||||
pattern->channels[0].rows[0].port = 15;
|
||||
pattern->channels[0].rows[0].note = 64;
|
||||
pattern->channels[0].rows[0].addParam('v', 255);
|
||||
pattern->channels[1].rows[0].port = 1;
|
||||
pattern->channels[1].rows[0].note = 0;
|
||||
pattern->channels[5].name = "I have a name";
|
||||
socket.reset(new UISocket());
|
||||
|
||||
auto pe = t->findChild<Xybrid::UI::PatternEditorView*>("patternEditor");
|
||||
|
||||
pe->setPattern(&*pattern);
|
||||
menuFileNew();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
|
@ -61,3 +59,49 @@ bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MainWindow::menuFileNew() {
|
||||
auto hold = project; // keep alive until done
|
||||
project = std::make_shared<Project>();
|
||||
project->newPattern();
|
||||
|
||||
onNewProjectLoaded();
|
||||
}
|
||||
|
||||
void MainWindow::menuFileOpen() {
|
||||
auto fileName = QFileDialog::getOpenFileName(this, "Open project...");
|
||||
if (fileName.isEmpty()) return; // canceled
|
||||
}
|
||||
|
||||
void MainWindow::menuFileSave() {
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::menuFileSaveAs() {
|
||||
auto fileName = QFileDialog::getSaveFileName(this, "Save project as...");
|
||||
if (fileName.isEmpty()) return; // canceled
|
||||
}
|
||||
|
||||
void MainWindow::onNewProjectLoaded() {
|
||||
project->socket = socket.get();
|
||||
Pattern* pt = project->patterns.front().get();
|
||||
for (auto* p : project->sequence) { // find first non-spacer in sequence, else fall back to first pattern numerically
|
||||
if (p == nullptr) continue;
|
||||
pt = p;
|
||||
break;
|
||||
}
|
||||
selectPatternForEditing(pt);
|
||||
}
|
||||
|
||||
bool MainWindow::selectPatternForEditing(Pattern* pattern) {
|
||||
if (pattern->project != project.get()) return false; // wrong project
|
||||
if (project->patterns.size() <= pattern->index) return false; // invalid id
|
||||
auto sp = project->patterns[pattern->index];
|
||||
if (sp.get() != pattern) return false; // wrong index
|
||||
auto hold = editingPattern; // keep alive until done
|
||||
editingPattern = sp;
|
||||
|
||||
ui->patternEditor->setPattern(editingPattern.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
#include <QMainWindow>
|
||||
|
||||
#include "data/pattern.h"
|
||||
#include "uisocket.h"
|
||||
#include "data/project.h"
|
||||
#include "ui/patterneditormodel.h"
|
||||
|
||||
namespace Ui {
|
||||
|
@ -19,10 +20,21 @@ namespace Xybrid {
|
|||
|
||||
private:
|
||||
Ui::MainWindow* ui;
|
||||
std::unique_ptr<Data::Pattern> pattern; // temporary pattern for testing the editor
|
||||
std::unique_ptr<UISocket> socket;
|
||||
std::shared_ptr<Data::Project> project;
|
||||
std::shared_ptr<Data::Pattern> editingPattern; // temporary pattern for testing the editor
|
||||
|
||||
void onNewProjectLoaded();
|
||||
bool selectPatternForEditing(Data::Pattern*);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void menuFileNew();
|
||||
void menuFileOpen();
|
||||
void menuFileSave();
|
||||
void menuFileSaveAs();
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -59,43 +59,95 @@
|
|||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="Xybrid::UI::PatternEditorView" name="patternEditor">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Iosevka Term Light</family>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
<widget class="QSplitter" name="patternViewSplitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::AnyKeyPressed</set>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideNone</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="gridStyle">
|
||||
<enum>Qt::NoPen</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>24</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>5</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderDefaultSectionSize">
|
||||
<number>24</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderMinimumSectionSize">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QListView" name="listView">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>192</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QWidget" name="patternViewPane" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>480</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="Xybrid::UI::PatternEditorView" name="patternEditor">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Iosevka Term Light</family>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::AnyKeyPressed</set>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideNone</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="gridStyle">
|
||||
<enum>Qt::NoPen</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>24</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>5</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderDefaultSectionSize">
|
||||
<number>24</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderMinimumSectionSize">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -162,7 +214,7 @@
|
|||
</widget>
|
||||
<action name="actionOpen">
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
<string>&Open</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+O</string>
|
||||
|
@ -173,7 +225,7 @@
|
|||
</action>
|
||||
<action name="actionNew">
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
<string>&New</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+N</string>
|
||||
|
@ -184,7 +236,7 @@
|
|||
</action>
|
||||
<action name="actionSave">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
<string>&Save</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
|
@ -195,7 +247,7 @@
|
|||
</action>
|
||||
<action name="actionSave_As">
|
||||
<property name="text">
|
||||
<string>Save As...</string>
|
||||
<string>Sa&ve As...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+S</string>
|
||||
|
@ -215,11 +267,59 @@
|
|||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>actionNew</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuFileNew()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>459</x>
|
||||
<y>305</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionOpen</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>showMaximized()</slot>
|
||||
<slot>menuFileOpen()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>459</x>
|
||||
<y>305</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionSave</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuFileSave()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>459</x>
|
||||
<y>305</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionSave_As</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuFileSaveAs()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
|
@ -232,4 +332,10 @@
|
|||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<slots>
|
||||
<slot>menuFileNew()</slot>
|
||||
<slot>menuFileOpen()</slot>
|
||||
<slot>menuFileSave()</slot>
|
||||
<slot>menuFileSaveAs()</slot>
|
||||
</slots>
|
||||
</ui>
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
#include "channelheaderview.h"
|
||||
using Xybrid::UI::ChannelHeaderView;
|
||||
using Xybrid::UI::ChannelHeaderItemDelegate;
|
||||
|
||||
#include "ui/patterneditormodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QTextEdit>
|
||||
|
||||
ChannelHeaderView::ChannelHeaderView(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) {
|
||||
//
|
||||
}
|
||||
|
||||
QWidget* ChannelHeaderItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) const {
|
||||
qDebug() << QString("trying edit");
|
||||
auto e = new QTextEdit(parent);
|
||||
return e;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
namespace Xybrid::UI {
|
||||
class ChannelHeaderView : public QHeaderView {
|
||||
public:
|
||||
ChannelHeaderView(QWidget* parent = nullptr);
|
||||
};
|
||||
|
||||
class ChannelHeaderItemDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ChannelHeaderItemDelegate(QWidget *parent = nullptr) : QStyledItemDelegate(parent) {}
|
||||
|
||||
//bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override;
|
||||
|
||||
protected:
|
||||
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
//void setEditorData(QWidget *editor, const QModelIndex &index) const override;
|
||||
//void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ bool PatternEditorItemDelegate::eventFilter(QObject *obj, QEvent *event) {
|
|||
}
|
||||
|
||||
bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) {
|
||||
if (index.data().isNull()) return false; // no channels?
|
||||
auto type = event->type();
|
||||
if (type == QEvent::KeyPress) {
|
||||
auto k = static_cast<QKeyEvent*>(event)->key(); // grab key
|
||||
|
@ -186,6 +187,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
}//*/
|
||||
|
||||
QSize PatternEditorItemDelegate::sizeHint(const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) const {
|
||||
if (index.data().isNull()) return QSize(0, 0);
|
||||
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
int cc = index.column() % PatternEditorModel::colsPerChannel;
|
||||
std::string s = "FF";
|
||||
|
|
|
@ -65,14 +65,17 @@ PatternEditorModel::PatternEditorModel(QObject *parent)
|
|||
}
|
||||
|
||||
int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {
|
||||
//if (pattern->channels.size() == 0) return 1;
|
||||
return pattern->rows;
|
||||
}
|
||||
|
||||
int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
|
||||
if (pattern->channels.size() == 0) return 1;
|
||||
return colsPerChannel * static_cast<int>(pattern->channels.size());
|
||||
}
|
||||
|
||||
QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
|
||||
if (pattern->channels.size() == 0) return QVariant();
|
||||
if (role == Qt::DisplayRole) {
|
||||
int cc = index.column() % colsPerChannel;
|
||||
int ch = (index.column() - cc) / colsPerChannel;
|
||||
|
@ -102,19 +105,8 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
|
|||
}
|
||||
|
||||
QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
|
||||
if (orientation == Qt::Orientation::Horizontal) {
|
||||
int cc = section % colsPerChannel;
|
||||
int ch = (section - cc) / colsPerChannel;
|
||||
|
||||
if (cc == 0) {
|
||||
auto n = QString::fromStdString(pattern->channels.at(static_cast<size_t>(ch)).name);
|
||||
if (n.length() == 0) return QString("(Channel %1)").arg(ch);
|
||||
return n;
|
||||
}
|
||||
return QString(""); // those aren't even supposed to be there
|
||||
}
|
||||
if (role == Qt::ToolTipRole) return QString(""); // no tool tip for simple numbers
|
||||
if (role == Qt::DisplayRole) {
|
||||
if (orientation == Qt::Orientation::Horizontal) return QVariant(); // blank actual-header
|
||||
return QString::number(section);
|
||||
} else if (role == Qt::SizeHintRole) {
|
||||
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
|
@ -129,9 +121,11 @@ int PatternEditorModel::hdrColumnCount() const {
|
|||
}
|
||||
QVariant PatternEditorModel::hdrData(int section, Qt::Orientation, int role) const {
|
||||
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
|
||||
if (pattern->channels.size() == 0) return QString("(no channels)");
|
||||
if (static_cast<size_t>(section) >= pattern->channels.size()) return QString("");
|
||||
auto n = QString::fromStdString(pattern->channels.at(static_cast<size_t>(section)).name);
|
||||
if (n.length() == 0) return QString("(Channel %1)").arg(section);
|
||||
//if (n.length() == 0) return QString("(Channel %1)").arg(section);
|
||||
if (n.length() == 0) return QString("(ch%1)").arg(section);
|
||||
return n;
|
||||
} else if (role == Qt::SizeHintRole) {
|
||||
if (cellWidthBase <= 0) {
|
||||
|
@ -157,7 +151,7 @@ QVariant PatternEditorModel::hdrData(int section, Qt::Orientation, int role) con
|
|||
|
||||
void PatternEditorModel::setPattern(Pattern* pattern) {
|
||||
this->pattern = pattern;
|
||||
//updateColumnDisplay();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void PatternEditorModel::updateColumnDisplay() {
|
||||
|
@ -165,15 +159,28 @@ void PatternEditorModel::updateColumnDisplay() {
|
|||
qDebug() << QString("column display request #%1").arg(qi++);//*/
|
||||
if (pattern == nullptr) return;
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
for (size_t ch = 0; ch < pattern->channels.size(); ch++) {
|
||||
auto& c = pattern->channels.at(ch);
|
||||
size_t maxParams = 0;
|
||||
for (auto& r : c.rows) {
|
||||
if (r.numParams() > maxParams) maxParams = r.numParams();
|
||||
}
|
||||
int lastShown = 0;
|
||||
int chWidth = 0;
|
||||
for (size_t i = 0; i < PatternEditorModel::colsPerChannel; i++) {
|
||||
view->setColumnHidden(static_cast<int>(ch*PatternEditorModel::colsPerChannel+i), i >= 3+2*maxParams);
|
||||
auto col = static_cast<int>(ch*PatternEditorModel::colsPerChannel+i);
|
||||
if (i < 3+2*maxParams) {
|
||||
view->setColumnHidden(col, false);
|
||||
view->resizeColumnToContents(col);
|
||||
lastShown = col;
|
||||
chWidth += view->columnWidth(col);
|
||||
} else view->setColumnHidden(col, true);
|
||||
//view->setColumnWidth(col, 1);
|
||||
}
|
||||
int minWidth = std::min(100, fm.width(QString::fromStdString(c.name)) + 8);
|
||||
int lsw = view->columnWidth(lastShown);
|
||||
view->setColumnWidth(lastShown, std::max(lsw + 3, minWidth - (chWidth - lsw)));
|
||||
}
|
||||
view->updateHeader(true);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ namespace Xybrid::UI {
|
|||
}
|
||||
|
||||
void updateColumnDisplay();
|
||||
void refresh() { // implemented in header to allow inlining
|
||||
emit layoutChanged();
|
||||
updateColumnDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
class PatternEditorHeaderProxyModel : public QAbstractTableModel {
|
||||
|
|
|
@ -16,6 +16,10 @@ using Xybrid::Data::Pattern;
|
|||
|
||||
#include <QHeaderView>
|
||||
#include <QScrollBar>
|
||||
#include <QMenu>
|
||||
#include <QTextEdit>
|
||||
#include <QInputDialog>
|
||||
|
||||
|
||||
namespace {
|
||||
Pattern pt(1, 0);
|
||||
|
@ -28,16 +32,25 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
hdr->setStretchLastSection(true);
|
||||
hdr->setSectionsMovable(true);
|
||||
hdr->setFirstSectionMovable(true);
|
||||
hdr->setEditTriggers(EditTrigger::DoubleClicked);
|
||||
|
||||
// hook up scrolling to update header position
|
||||
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatternEditorView::updateHeaderOffset);
|
||||
// and header swap
|
||||
connect(hdr.get(), &QHeaderView::sectionMoved, this, &PatternEditorView::headerMoved);
|
||||
|
||||
horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
// hook up context menu
|
||||
hdr->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(hdr.get(), &QHeaderView::customContextMenuRequested, this, &PatternEditorView::headerContextMenu);
|
||||
|
||||
connect(hdr.get(), &QHeaderView::sectionDoubleClicked, this, &PatternEditorView::headerDoubleClicked);
|
||||
|
||||
horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);//ResizeToContents);
|
||||
verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
//horizontalHeader()->setSectionsMovable(true);
|
||||
//horizontalHeader()->moveSection(1, 3);
|
||||
verticalHeader()->setStretchLastSection(true);
|
||||
verticalHeader()->setSectionsClickable(false);
|
||||
setCornerButtonEnabled(false);
|
||||
//verticalHeader()->setDefaultAlignment(Qt::AlignTop);
|
||||
|
||||
mdl.reset(new PatternEditorModel(this));
|
||||
del.reset(new PatternEditorItemDelegate(this));
|
||||
|
@ -45,9 +58,6 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
mdl->setPattern(&pt);
|
||||
setModel(&*mdl);
|
||||
hdr->setModel(&*mdl->hprox);
|
||||
//horizontalHeader()->setModel(&*mdl);
|
||||
|
||||
|
||||
}
|
||||
|
||||
PatternEditorView::~PatternEditorView() {
|
||||
|
@ -72,8 +82,6 @@ void PatternEditorView::keyPressEvent(QKeyEvent *event) {
|
|||
|
||||
void PatternEditorView::setPattern(Xybrid::Data::Pattern *pattern) {
|
||||
mdl->setPattern(pattern);
|
||||
colUpdateNeeded = true;
|
||||
this->viewport()->update();
|
||||
}
|
||||
|
||||
void PatternEditorView::updateGeometries() {
|
||||
|
@ -125,3 +133,41 @@ void PatternEditorView::headerMoved(int logicalIndex, int oldVisualIndex, int ne
|
|||
// and make sure header follows
|
||||
emit hdr->model()->headerDataChanged(Qt::Horizontal, 0, static_cast<int>(chn.size()) - 1);
|
||||
}
|
||||
|
||||
void PatternEditorView::headerDoubleClicked(int section) {
|
||||
startRenameChannel(section);
|
||||
}
|
||||
|
||||
void PatternEditorView::headerContextMenu(QPoint pt) {
|
||||
int idx = hdr->logicalIndexAt(pt);
|
||||
|
||||
QMenu *menu=new QMenu(this);
|
||||
menu->addAction("Add Channel", [idx, this]() {
|
||||
mdl->getPattern().addChannel(idx);
|
||||
mdl->refresh();
|
||||
});
|
||||
if (idx < hdr->count() - 1) {
|
||||
menu->addAction("Delete Channel", [idx, this]() {
|
||||
mdl->getPattern().deleteChannel(idx);
|
||||
mdl->refresh();
|
||||
});
|
||||
menu->addAction("Rename Channel", [idx, this]() {
|
||||
startRenameChannel(idx);
|
||||
});
|
||||
}
|
||||
menu->popup(hdr->mapToGlobal(pt));
|
||||
|
||||
}
|
||||
|
||||
void PatternEditorView::startRenameChannel(int channel) {
|
||||
auto p = &mdl->getPattern();
|
||||
if (static_cast<size_t>(channel) >= p->numChannels()) return;
|
||||
auto c = &p->channel(channel);
|
||||
bool ok = false;
|
||||
auto capt = QString("Rename channel %1:").arg(channel);
|
||||
auto n = QInputDialog::getText(this, "Rename...", capt, QLineEdit::Normal, QString::fromStdString(c->name), &ok);
|
||||
if (!ok) return; // canceled
|
||||
if (p != &mdl->getPattern() || c != &p->channel(channel)) return; // abort if this somehow isn't the channel it was before
|
||||
c->name = n.toStdString(); // and set name
|
||||
mdl->updateColumnDisplay(); // update sizes and such
|
||||
}
|
||||
|
|
|
@ -24,16 +24,22 @@ namespace Xybrid::UI {
|
|||
public:
|
||||
PatternEditorView(QWidget* parent = nullptr);
|
||||
~PatternEditorView() override;
|
||||
|
||||
void updateGeometries() override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void keyboardSearch(const QString&) override {} // disable accidental search
|
||||
|
||||
void setPattern(Data::Pattern* pattern);
|
||||
|
||||
void updateGeometries() override;
|
||||
void updateHeader(bool full = false);
|
||||
|
||||
private:
|
||||
void updateHeaderOffset(int);
|
||||
void headerMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
|
||||
void headerDoubleClicked(int section);
|
||||
void headerContextMenu(QPoint pt);
|
||||
|
||||
void keyboardSearch(const QString&) override {} // disable accidental search
|
||||
void startRenameChannel(int channel);
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace Xybrid {
|
||||
class UISocket : public QObject {
|
||||
Q_OBJECT
|
||||
signals:
|
||||
//
|
||||
};
|
||||
}
|
|
@ -43,7 +43,8 @@ HEADERS += \
|
|||
ui/patterneditoritemdelegate.h \
|
||||
ui/patterneditorview.h \
|
||||
data/project.h \
|
||||
ui/channelheaderview.h
|
||||
ui/channelheaderview.h \
|
||||
uisocket.h
|
||||
|
||||
FORMS += \
|
||||
mainwindow.ui
|
||||
|
|
Loading…
Reference in New Issue