Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright notice, this
10 * list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28#include "KeysWidget.h"
29#include "AudioEngine.h"
30#include <LibGUI/Painter.h>
31
32KeysWidget::KeysWidget(AudioEngine& audio_engine)
33 : m_audio_engine(audio_engine)
34{
35 set_fill_with_background_color(true);
36}
37
38KeysWidget::~KeysWidget()
39{
40}
41
42int KeysWidget::mouse_note() const
43{
44 if (m_mouse_down && m_mouse_note + m_audio_engine.octave_base() < note_count)
45 return m_mouse_note; // Can be -1.
46 else
47 return -1;
48}
49
50void KeysWidget::set_key(int key, Switch switch_key)
51{
52 if (key == -1 || key + m_audio_engine.octave_base() >= note_count)
53 return;
54
55 if (switch_key == On) {
56 ++m_key_on[key];
57 } else {
58 if (m_key_on[key] >= 1)
59 --m_key_on[key];
60 }
61 ASSERT(m_key_on[key] <= 2);
62
63 m_audio_engine.set_note_current_octave(key, switch_key);
64}
65
66int KeysWidget::key_code_to_key(int key_code) const
67{
68 switch (key_code) {
69 case Key_A:
70 return 0;
71 case Key_W:
72 return 1;
73 case Key_S:
74 return 2;
75 case Key_E:
76 return 3;
77 case Key_D:
78 return 4;
79 case Key_F:
80 return 5;
81 case Key_T:
82 return 6;
83 case Key_G:
84 return 7;
85 case Key_Y:
86 return 8;
87 case Key_H:
88 return 9;
89 case Key_U:
90 return 10;
91 case Key_J:
92 return 11;
93 case Key_K:
94 return 12;
95 case Key_O:
96 return 13;
97 case Key_L:
98 return 14;
99 case Key_P:
100 return 15;
101 case Key_Semicolon:
102 return 16;
103 case Key_Apostrophe:
104 return 17;
105 case Key_RightBracket:
106 return 18;
107 case Key_Return:
108 return 19;
109 default:
110 return -1;
111 }
112}
113
114constexpr int white_key_width = 24;
115constexpr int black_key_width = 16;
116constexpr int black_key_x_offset = black_key_width / 2;
117constexpr int black_key_height = 60;
118
119constexpr char white_key_labels[] = {
120 'A',
121 'S',
122 'D',
123 'F',
124 'G',
125 'H',
126 'J',
127 'K',
128 'L',
129 ';',
130 '\'',
131 'r',
132};
133constexpr int white_key_labels_count = sizeof(white_key_labels) / sizeof(char);
134
135constexpr char black_key_labels[] = {
136 'W',
137 'E',
138 'T',
139 'Y',
140 'U',
141 'O',
142 'P',
143 ']',
144};
145constexpr int black_key_labels_count = sizeof(black_key_labels) / sizeof(char);
146
147constexpr int black_key_offsets[] = {
148 white_key_width,
149 white_key_width * 2,
150 white_key_width,
151 white_key_width,
152 white_key_width * 2,
153};
154
155constexpr int white_key_note_accumulator[] = {
156 2,
157 2,
158 1,
159 2,
160 2,
161 2,
162 1,
163};
164
165constexpr int black_key_note_accumulator[] = {
166 2,
167 3,
168 2,
169 2,
170 3,
171};
172
173void KeysWidget::paint_event(GUI::PaintEvent& event)
174{
175 GUI::Painter painter(*this);
176 painter.translate(frame_thickness(), frame_thickness());
177
178 int note = 0;
179 int x = 0;
180 int i = 0;
181 for (;;) {
182 Gfx::Rect rect(x, 0, white_key_width, frame_inner_rect().height());
183 painter.fill_rect(rect, m_key_on[note] ? note_pressed_color : Color::White);
184 painter.draw_rect(rect, Color::Black);
185 if (i < white_key_labels_count) {
186 rect.set_height(rect.height() * 1.5);
187 painter.draw_text(rect, StringView(&white_key_labels[i], 1), Gfx::TextAlignment::Center, Color::Black);
188 }
189
190 note += white_key_note_accumulator[i % white_keys_per_octave];
191 x += white_key_width;
192 ++i;
193
194 if (note + m_audio_engine.octave_base() >= note_count)
195 break;
196 if (x >= frame_inner_rect().width())
197 break;
198 }
199
200 note = 1;
201 x = white_key_width - black_key_x_offset;
202 i = 0;
203 for (;;) {
204 Gfx::Rect rect(x, 0, black_key_width, black_key_height);
205 painter.fill_rect(rect, m_key_on[note] ? note_pressed_color : Color::Black);
206 painter.draw_rect(rect, Color::Black);
207 if (i < black_key_labels_count) {
208 rect.set_height(rect.height() * 1.5);
209 painter.draw_text(rect, StringView(&black_key_labels[i], 1), Gfx::TextAlignment::Center, Color::White);
210 }
211
212 note += black_key_note_accumulator[i % black_keys_per_octave];
213 x += black_key_offsets[i % black_keys_per_octave];
214 ++i;
215
216 if (note + m_audio_engine.octave_base() >= note_count)
217 break;
218 if (x >= frame_inner_rect().width())
219 break;
220 }
221
222 GUI::Frame::paint_event(event);
223}
224
225constexpr int notes_per_white_key[] = {
226 1,
227 3,
228 5,
229 6,
230 8,
231 10,
232 12,
233};
234
235// Keep in mind that in any of these functions a note value can be out of
236// bounds. Bounds checking is done in set_key().
237
238static inline int note_from_white_keys(int white_keys)
239{
240 int octaves = white_keys / white_keys_per_octave;
241 int remainder = white_keys % white_keys_per_octave;
242 int notes_from_octaves = octaves * notes_per_octave;
243 int notes_from_remainder = notes_per_white_key[remainder];
244 int note = (notes_from_octaves + notes_from_remainder) - 1;
245 return note;
246}
247
248int KeysWidget::note_for_event_position(const Gfx::Point& a_point) const
249{
250 if (!frame_inner_rect().contains(a_point))
251 return -1;
252
253 auto point = a_point;
254 point.move_by(-frame_thickness(), -frame_thickness());
255
256 int white_keys = point.x() / white_key_width;
257 int note = note_from_white_keys(white_keys);
258
259 bool black_key_on_left = note != 0 && key_pattern[(note - 1) % notes_per_octave] == Black;
260 if (black_key_on_left) {
261 int black_key_x = (white_keys * white_key_width) - black_key_x_offset;
262 Gfx::Rect black_key(black_key_x, 0, black_key_width, black_key_height);
263 if (black_key.contains(point))
264 return note - 1;
265 }
266
267 bool black_key_on_right = key_pattern[(note + 1) % notes_per_octave] == Black;
268 if (black_key_on_right) {
269 int black_key_x = ((white_keys + 1) * white_key_width) - black_key_x_offset;
270 Gfx::Rect black_key(black_key_x, 0, black_key_width, black_key_height);
271 if (black_key.contains(point))
272 return note + 1;
273 }
274
275 return note;
276}
277
278void KeysWidget::mousedown_event(GUI::MouseEvent& event)
279{
280 if (event.button() != GUI::MouseButton::Left)
281 return;
282
283 m_mouse_down = true;
284
285 m_mouse_note = note_for_event_position(event.position());
286
287 set_key(m_mouse_note, On);
288 update();
289}
290
291void KeysWidget::mouseup_event(GUI::MouseEvent& event)
292{
293 if (event.button() != GUI::MouseButton::Left)
294 return;
295
296 m_mouse_down = false;
297
298 set_key(m_mouse_note, Off);
299 update();
300}
301
302void KeysWidget::mousemove_event(GUI::MouseEvent& event)
303{
304 if (!m_mouse_down)
305 return;
306
307 int new_mouse_note = note_for_event_position(event.position());
308
309 if (m_mouse_note == new_mouse_note)
310 return;
311
312 set_key(m_mouse_note, Off);
313 set_key(new_mouse_note, On);
314 update();
315
316 m_mouse_note = new_mouse_note;
317}