Skip to content

Commit be63be0

Browse files
committed
Implement select, copy, and paste support for notes
Closes #59. This works across patterns (and probably even across songs?) and supports undo/redo. The shortcuts list has been updated, but this still needs to be added to one of the guides.
1 parent 55a5077 commit be63be0

File tree

5 files changed

+312
-38
lines changed

5 files changed

+312
-38
lines changed

gui/theme/project_theme.tres

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ NoteMap/colors/active_note_color = Color(1, 1, 0.752941, 1)
565565
NoteMap/colors/gutter_color = Color(0.427451, 0.521569, 0.552941, 1)
566566
NoteMap/colors/gutter_hover_color = Color(0.313726, 0.396078, 0.415686, 1)
567567
NoteMap/colors/note_cursor_color = Color(1, 1, 1, 1)
568+
NoteMap/colors/note_cursor_shadow_color = Color(0, 0, 0, 0.627451)
568569
NoteMap/colors/octave_bar_color = Color(0.737255, 0.811765, 1, 1)
569570
NoteMap/colors/octave_bar_shadow_color = Color(0, 0, 0, 0.784314)
570571
NoteMap/colors/playback_cursor_bevel_color = Color(0.427451, 0.521569, 0.552941, 1)
@@ -573,8 +574,13 @@ NoteMap/constants/active_note_bevel_width = 4
573574
NoteMap/constants/active_note_overlap_opacity = 40
574575
NoteMap/constants/border_cover_opacity = 88
575576
NoteMap/constants/border_width = 3
577+
NoteMap/constants/note_cursor_shadow_offset_x = 2
578+
NoteMap/constants/note_cursor_shadow_offset_y = 2
576579
NoteMap/constants/note_cursor_width = 3
577580
NoteMap/constants/note_height = 28
581+
NoteMap/constants/octave_bar_inset = 40
582+
NoteMap/constants/octave_bar_shadow_offset_x = 0
583+
NoteMap/constants/octave_bar_shadow_offset_y = 2
578584
NoteMap/constants/playback_cursor_width = 6
579585
NoteMap/icons/active_note_overlap = ExtResource("1_4kypt")
580586
OptionListPopup/colors/empty_font_color = Color(0.284934, 0.322112, 0.334212, 1)

gui/views/note_map/NoteMap.gd

Lines changed: 180 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ var _note_cursor_size: int = 1
5050

5151
var _note_drawing_mode: DrawingMode = DrawingMode.DRAWING_OFF
5252

53+
var _note_selecting: bool = false
54+
var _note_selecting_rect: Rect2 = Rect2(-1, -1, 0, 0)
55+
var _note_copied_buffer: PackedVector3Array = PackedVector3Array()
56+
5357
@onready var _gutter: NoteMapGutter = $NoteMapGutter
5458
@onready var _scrollbar: NoteMapScrollbar = $NoteMapScrollbar
5559
@onready var _overlay: NoteMapOverlay = $NoteMapOverlay
@@ -98,9 +102,15 @@ func _notification(what: int) -> void:
98102
if Engine.is_editor_hint():
99103
return
100104

101-
if what == NOTIFICATION_APPLICATION_FOCUS_OUT:
105+
if what == NOTIFICATION_DRAG_END:
106+
if _note_selecting:
107+
_stop_selecting_notes()
108+
109+
elif what == NOTIFICATION_APPLICATION_FOCUS_OUT:
102110
if _note_drawing_mode != DrawingMode.DRAWING_OFF:
103111
_stop_drawing_notes()
112+
if _note_selecting:
113+
_stop_selecting_notes()
104114

105115

106116
func _gui_input(event: InputEvent) -> void:
@@ -120,25 +130,46 @@ func _gui_input(event: InputEvent) -> void:
120130
_change_scroll_offset(-1)
121131

122132
elif mb.button_index == MOUSE_BUTTON_LEFT:
123-
_start_drawing_notes(DrawingMode.DRAWING_ADD)
133+
if mb.shift_pressed:
134+
_start_selecting_notes()
135+
else:
136+
_clear_note_selection()
137+
_start_drawing_notes(DrawingMode.DRAWING_ADD)
124138
elif mb.button_index == MOUSE_BUTTON_RIGHT:
139+
_clear_note_selection()
125140
_start_drawing_notes(DrawingMode.DRAWING_REMOVE)
126141

127-
if _note_drawing_mode != DrawingMode.DRAWING_OFF && not mb.pressed:
128-
if mb.button_index == MOUSE_BUTTON_LEFT || mb.button_index == MOUSE_BUTTON_RIGHT:
142+
if not mb.pressed:
143+
if _note_drawing_mode != DrawingMode.DRAWING_OFF && (mb.button_index == MOUSE_BUTTON_LEFT || mb.button_index == MOUSE_BUTTON_RIGHT):
129144
_stop_drawing_notes()
145+
elif _note_selecting && mb.button_index == MOUSE_BUTTON_LEFT:
146+
_stop_selecting_notes()
130147

131148

132149
func _shortcut_input(event: InputEvent) -> void:
150+
if Controller.is_song_editing_locked():
151+
return
152+
133153
if event.is_action_pressed("bosca_notemap_cursor_bigger", true, true):
134154
_resize_note_cursor(1)
135155
elif event.is_action_pressed("bosca_notemap_cursor_smaller", true, true):
136156
_resize_note_cursor(-1)
157+
elif event.is_action_pressed("ui_copy"):
158+
_copy_selected_notes()
159+
get_viewport().set_input_as_handled()
160+
elif event.is_action_pressed("ui_paste"):
161+
_paste_selected_notes()
162+
get_viewport().set_input_as_handled()
137163

138164

139165
func _physics_process(_delta: float) -> void:
140166
_process_note_cursor()
141167
_process_note_drawing()
168+
_process_note_selecting()
169+
170+
171+
func _update_processing_state() -> void:
172+
set_physics_process(_note_cursor_visible || _note_selecting)
142173

143174

144175
func _draw() -> void:
@@ -413,17 +444,20 @@ func _update_playback_cursor() -> void:
413444
# Grid layout and coordinates.
414445

415446
func _get_cell_at_cursor() -> Vector2i:
447+
return _get_cell_at_position(get_local_mouse_position())
448+
449+
450+
func _get_cell_at_position(at_position: Vector2) -> Vector2i:
416451
var available_rect: Rect2 = get_available_rect()
417452
var note_height := get_theme_constant("note_height", "NoteMap")
418453

419-
var mouse_position := get_local_mouse_position()
420-
if not available_rect.has_point(mouse_position):
454+
if not available_rect.has_point(at_position):
421455
return Vector2i(-1, -1)
422456

423-
var mouse_normalized := mouse_position - available_rect.position
457+
var position_normalized := at_position - available_rect.position
424458
var cell_indexed := Vector2i(0, 0)
425-
cell_indexed.x = clampi(floori(mouse_normalized.x / _note_width), 0, pattern_size - 1)
426-
cell_indexed.y = clampi(floori((available_rect.size.y - mouse_normalized.y) / note_height), 0, _note_rows.size() - 1)
459+
cell_indexed.x = clampi(floori(position_normalized.x / _note_width), 0, pattern_size - 1)
460+
cell_indexed.y = clampi(floori((available_rect.size.y - position_normalized.y) / note_height), 0, _note_rows.size() - 1)
427461
return cell_indexed
428462

429463

@@ -558,7 +592,8 @@ func _update_active_notes() -> void:
558592
var note := ActiveNote.new()
559593
note.note_value = note_value_normalized
560594
note.note_index = note_data.y
561-
note.position = _get_cell_position(Vector2i(note_data.y, row_index - _scroll_offset))
595+
note.cell_index = Vector2i(note_data.y, row_index - _scroll_offset)
596+
note.position = _get_cell_position(note.cell_index)
562597
note.length = note_data.z
563598
_active_notes.push_back(note)
564599

@@ -585,12 +620,12 @@ func _update_gutter_size() -> void:
585620
func _show_note_cursor() -> void:
586621
_note_cursor_visible = true
587622
_process_note_cursor()
588-
set_physics_process(true)
623+
_update_processing_state()
589624

590625

591626
func _hide_note_cursor() -> void:
592-
set_physics_process(false)
593627
_note_cursor_visible = false
628+
_update_processing_state()
594629
_process_note_cursor()
595630

596631

@@ -701,6 +736,136 @@ func _preview_note_at_cursor(row_index: int) -> void:
701736
Controller.preview_pattern_note(note_value, _note_cursor_size)
702737

703738

739+
# Note selecting.
740+
741+
func _start_selecting_notes() -> void:
742+
_note_selecting = true
743+
_update_processing_state()
744+
745+
_note_selecting_rect = Rect2()
746+
_note_selecting_rect.position = get_local_mouse_position()
747+
_process_note_selecting()
748+
749+
750+
func _stop_selecting_notes() -> void:
751+
_note_selecting = false
752+
_update_processing_state()
753+
_note_selecting_rect = Rect2(-1, -1, 0, 0)
754+
755+
_overlay.note_selecting_rect = _note_selecting_rect
756+
_overlay.queue_redraw()
757+
758+
759+
func _process_note_selecting() -> void:
760+
if not _note_selecting:
761+
return
762+
763+
_note_selecting_rect.end = get_local_mouse_position()
764+
765+
# Convert the pixel rect to a grid/cell index rect.
766+
var grid_rect := Rect2i()
767+
grid_rect.position = _get_cell_at_position(_note_selecting_rect.position)
768+
grid_rect.end = _get_cell_at_position(_note_selecting_rect.end)
769+
# Normalize the rect so we can check the points.
770+
var grid_rect_normalized := grid_rect.abs()
771+
grid_rect_normalized.size += Vector2i(1, 1) # Far edges must be inclusive.
772+
773+
for active_note in _active_notes:
774+
active_note.selected = grid_rect_normalized.has_point(active_note.cell_index)
775+
776+
_overlay.note_selecting_rect = _note_selecting_rect
777+
_overlay.queue_redraw()
778+
779+
780+
func _clear_note_selection() -> void:
781+
for active_note in _active_notes:
782+
active_note.selected = false
783+
784+
785+
func _copy_selected_notes() -> void:
786+
_note_copied_buffer.clear()
787+
788+
# Find the coordinates to zero out against.
789+
var top_left := Vector2(-1, -1)
790+
791+
# Extract all selected notes. We don't really care for their actual values, as
792+
# all the data is interpreted relatively.
793+
for active_note in _active_notes:
794+
if not active_note.selected:
795+
continue
796+
797+
var relative_data := Vector3(active_note.cell_index.y, active_note.cell_index.x, active_note.length)
798+
_note_copied_buffer.push_back(relative_data)
799+
800+
if top_left.x == -1 || top_left.x > relative_data.x:
801+
top_left.x = relative_data.x
802+
if top_left.y == -1 || top_left.y > relative_data.y:
803+
top_left.y = relative_data.y
804+
805+
# Zero out the data.
806+
for i in _note_copied_buffer.size():
807+
var relative_data := _note_copied_buffer[i]
808+
relative_data.x -= top_left.x
809+
relative_data.y -= top_left.y
810+
811+
_note_copied_buffer[i] = relative_data
812+
813+
Controller.update_status("SELECTED NOTES COPIED", Controller.StatusLevel.INFO)
814+
815+
816+
func _paste_selected_notes() -> void:
817+
if not Controller.current_song || not current_pattern:
818+
return
819+
if _note_copied_buffer.is_empty():
820+
return
821+
822+
var base_indexed := _get_cell_at_cursor()
823+
if base_indexed.x < 0 || base_indexed.y < 0:
824+
return
825+
826+
# First, prepare the copied notes for the placement. Some may not fit well and have
827+
# to be skipped. We override existing values with this operation, as this seems to
828+
# be the most expected behavior.
829+
830+
var notes_to_add: Array[Vector3i] = []
831+
var notes_to_remove: Array[Vector3i] = []
832+
833+
for copied_note: Vector3i in _note_copied_buffer:
834+
var note_indexed := base_indexed + Vector2i(copied_note.y, copied_note.x)
835+
var note_value_index := note_indexed.y + _scroll_offset
836+
if note_value_index >= _note_row_value_map.size():
837+
continue
838+
839+
var note_value: int = _note_row_value_map[note_value_index] + current_pattern.key
840+
var note_data := Vector3i(note_value, note_indexed.x, copied_note.z)
841+
842+
var existing_note := current_pattern.get_note(note_value, note_indexed.x, true)
843+
if current_pattern.is_note_valid(existing_note, Controller.current_song.pattern_size):
844+
notes_to_remove.push_back(existing_note)
845+
846+
notes_to_add.push_back(note_data)
847+
848+
# Generate the undo/redo action.
849+
850+
var pattern_state := Controller.state_manager.create_state_change(StateManager.StateChangeType.PATTERN, Controller.current_pattern_index)
851+
var state_context := pattern_state.get_context()
852+
state_context["removed"] = notes_to_remove
853+
state_context["added"] = notes_to_add
854+
855+
pattern_state.add_do_action(func() -> void:
856+
var reference_pattern := Controller.current_song.patterns[pattern_state.reference_id]
857+
reference_pattern.remove_notes(state_context.removed)
858+
reference_pattern.restore_notes(state_context.added)
859+
)
860+
pattern_state.add_undo_action(func() -> void:
861+
var reference_pattern := Controller.current_song.patterns[pattern_state.reference_id]
862+
reference_pattern.remove_notes(state_context.added)
863+
reference_pattern.restore_notes(state_context.removed)
864+
)
865+
866+
Controller.state_manager.commit_state_change(pattern_state)
867+
868+
704869
class NoteRow:
705870
var note_index: int = -1
706871
var label: String = ""
@@ -719,5 +884,8 @@ class OctaveRow:
719884
class ActiveNote:
720885
var note_value: int = -1
721886
var note_index: int = -1
887+
var cell_index: Vector2i = Vector2i(-1, -1)
722888
var position: Vector2 = Vector2.ZERO
723889
var length: int = 1
890+
891+
var selected: bool = false

0 commit comments

Comments
 (0)