1 /**
2  * File name: MidiKeyWidget.cpp
3  * Project: Geonkick (A kick synthesizer)
4  *
5  * Copyright (C) 2020 Iurie Nistor (http://iuriepage.wordpress.com)
6  *
7  * This file is part of Geonkick.
8  *
9  * GeonKick is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22  */
23 
24 #include "MidiKeyWidget.h"
25 
26 #include <RkEvent.h>
27 
MidiKeyWidget(GeonkickWidget * parent,PercussionModel * model,Rk::WindowFlags flag)28 MidiKeyWidget::MidiKeyWidget(GeonkickWidget *parent,
29                              PercussionModel *model,
30                              Rk::WindowFlags flag)
31         : GeonkickWidget(parent, flag)
32         , percussionModel{model}
33         , cellSize{32, 32}
34         , widgetPadding{8}
35         , midiRows{8}
36         , midiColumns{12}
37 {
38         setFixedSize(midiColumns * cellSize.width() + 2 * widgetPadding,
39                      midiRows * cellSize.height()  +  2 * widgetPadding);
40         setBackgroundColor({68, 68, 70, 240});
41 
42         RkImage img(size());
43         RkPainter painter(&img);
44         auto font = painter.font();
45         font.setSize(10);
46         painter.setFont(font);
47         auto pen = painter.pen();
48         pen.setColor({10, 10, 10, 230});
49         pen.setWidth(1);
50         painter.setPen(pen);
51         RkRect borderRect = rect();
52         borderRect.setSize({borderRect.width() - 1, borderRect.height() - 1});
53         painter.drawRect(borderRect);
54         pen.setColor({40, 40, 40, 230});
55         pen.setWidth(1);
56         painter.setPen(pen);
57         GeonkickTypes::MidiKey key = 21;
58         for (int row = 0; row < midiRows && key <= 109; row++) {
59                 for (int col = 0; col < midiColumns && key <= 109; col++)
60                         drawCell(painter, key++, row, col);
61         }
62         setBackgroundImage(img);
63         selectedCell = getCell(percussionModel->key());
64         RK_ACT_BIND(percussionModel, keyUpdated,
65                     RK_ACT_ARGS(PercussionModel::KeyIndex key),
66                     this,
67                     onUpdateKey(key));
68 }
69 
drawCell(RkPainter & painter,GeonkickTypes::MidiKey key,int row,int col)70 void MidiKeyWidget::drawCell(RkPainter &painter,
71                              GeonkickTypes::MidiKey key,
72                              int row, int col)
73 {
74         RkRect cellRect({widgetPadding + col * cellSize.width(),
75                          widgetPadding + row * cellSize.height()}, cellSize);
76         auto penBk = painter.pen();
77         painter.fillRect(cellRect, {60, 60, 60});
78         painter.setPen(penBk);
79         painter.drawRect(cellRect);
80         auto pen = penBk;
81         RkFont font;
82         if (col == 0 || row == 0) {
83                 font = painter.font();
84                 font.setWeight(RkFont::Weight::Bold);
85                 pen.setColor({240, 240, 240});
86         } else {
87                 pen.setColor({200, 200, 200});
88                 font = painter.font();
89                 font.setWeight(RkFont::Weight::Normal);
90         }
91         painter.setPen(pen);
92         painter.setFont(font);
93         painter.drawText(cellRect, midiKeyToNote(key));
94         painter.setPen(penBk);
95 }
96 
paintWidget(RkPaintEvent * event)97 void MidiKeyWidget::paintWidget([[maybe_unused]] RkPaintEvent *event)
98 {
99         RkPainter painter(this);
100         if (hoverCell.isValid()) {
101                 // Draw cell background.
102                 RkColor bkColor(80, 80, 80);
103                 RkRect innerRect({hoverCell.rect().left() + 1, hoverCell.rect().top() + 1},
104                                  RkSize(hoverCell.rect().width() - 1,
105                                         hoverCell.rect().height() - 1));
106                 painter.fillRect(innerRect, bkColor);
107 
108                 // Draw cell text.
109                 RkFont font = painter.font();
110                 font.setSize(10);
111                 if (hoverCell.column() == 0 || hoverCell.row() == 0)
112                         font.setWeight(RkFont::Weight::Bold);
113                 else
114                         font.setWeight(RkFont::Weight::Normal);
115                 auto pen = painter.pen();
116                 pen.setColor({230, 230, 230});
117                 painter.setPen(pen);
118                 painter.setFont(font);
119                 painter.drawText(hoverCell.rect(), midiKeyToNote(hoverCell.key()));
120         }
121 
122         if (selectedCell.isValid()) {
123                 // Draw cell background.
124                 RkColor bkColor;
125                 bkColor = RkColor(100, 100, 100);
126                 RkRect innerRect({selectedCell.rect().left() + 1, selectedCell.rect().top() + 1},
127                                  RkSize(selectedCell.rect().width() - 1,
128                                         selectedCell.rect().height() - 1));
129                 painter.fillRect(innerRect, bkColor);
130 
131                 // Draw cell text.
132                 RkFont font = painter.font();
133                 font.setSize(10);
134                 if (selectedCell.column() == 0 || selectedCell.row() == 0)
135                         font.setWeight(RkFont::Weight::Bold);
136                 else
137                         font.setWeight(RkFont::Weight::Normal);
138                 auto pen = painter.pen();
139                 pen.setColor({230, 230, 230});
140                 painter.setPen(pen);
141                 painter.setFont(font);
142                 painter.drawText(selectedCell.rect(), midiKeyToNote(selectedCell.key()));
143         }
144 }
145 
getCell(int x,int y) const146 KeyCell MidiKeyWidget::getCell(int x, int y) const
147 {
148         KeyCell cell;
149         int row = (y - widgetPadding) / cellSize.height();
150         if (row > midiRows - 1)
151                 return KeyCell();
152         int col = (x - widgetPadding) / cellSize.width();
153         if (col > midiColumns - 1)
154                 return KeyCell();
155         GeonkickTypes::MidiKey key = row * midiColumns + col + 21;
156         if (key < 21 || key > 109)
157                 return KeyCell();
158         cell.setColumn(col);
159         cell.setRow(row);
160         cell.setKey(key);
161         cell.setRect(RkRect({widgetPadding + col * cellSize.width(),
162                              widgetPadding + row * cellSize.height()},
163                         RkSize(cellSize.width(), cellSize.height())));
164         return cell;
165 }
166 
getCell(GeonkickTypes::MidiKey key) const167 KeyCell MidiKeyWidget::getCell(GeonkickTypes::MidiKey key) const
168 {
169         int row;
170         int col;
171         if (key < 21 || key > 108) {
172                 // Any cell.
173                 row = 7;
174                 col = 4;
175         } else {
176                 row = (key - 21) / midiColumns;
177                 col = (key - 21) % 12;
178         }
179 
180         KeyCell cell;
181         cell.setColumn(col);
182         cell.setRow(row);
183         cell.setKey(key);
184         cell.setRect(RkRect({widgetPadding + col * cellSize.width(),
185                              widgetPadding + row * cellSize.height()},
186                         RkSize(cellSize.width(), cellSize.height())));
187         return cell;
188 }
189 
mouseButtonPressEvent(RkMouseEvent * event)190 void MidiKeyWidget::mouseButtonPressEvent(RkMouseEvent *event)
191 {
192         if (event->button() == RkMouseEvent::ButtonType::Left) {
193 		if (auto cell = getCell(event->x(), event->y());
194 		    cell.isValid() && cell != selectedCell) {
195 			selectedCell = cell;
196 			percussionModel->setKey(selectedCell.key());
197 		}
198 	}
199 }
200 
mouseButtonReleaseEvent(RkMouseEvent * event)201 void MidiKeyWidget::mouseButtonReleaseEvent(RkMouseEvent *event)
202 {
203 }
204 
mouseMoveEvent(RkMouseEvent * event)205 void MidiKeyWidget::mouseMoveEvent(RkMouseEvent *event)
206 {
207         if (auto cell = getCell(event->x(), event->y()); cell != hoverCell) {
208                 hoverCell = cell;
209 		update();
210         }
211 }
212 
midiKeyToNote(GeonkickTypes::MidiKey key)213 RkString MidiKeyWidget::midiKeyToNote(GeonkickTypes::MidiKey key)
214 {
215         if (key < 21 || key > 108)
216                 return RkString("Any");
217 
218         constexpr std::array<const char*, 17> notes =
219                   {"C",  "C#", "D",
220                    "D#", "E",  "F",
221                    "F#", "G",  "G#",
222                    "A",  "A#", "B"};
223         return RkString(notes[(key - 21 + 9) % 12]) + std::to_string((key - 20 + 9) / 12);
224 }
225 
onUpdateKey(PercussionModel::KeyIndex key)226 void MidiKeyWidget::onUpdateKey(PercussionModel::KeyIndex key)
227 {
228         selectedCell = getCell(key);
229         update();
230 }
231 
closeEvent(RkCloseEvent * event)232 void MidiKeyWidget::closeEvent(RkCloseEvent *event)
233 {
234         action isAboutToClose();
235         RkWidget::closeEvent(event);
236 }
237 
hoverEvent(RkHoverEvent * event)238 void MidiKeyWidget::hoverEvent(RkHoverEvent *event)
239 {
240         if (!event->isHover()) {
241                 hoverCell = KeyCell();
242                 update();
243         }
244 }
245