@@ -50,6 +50,10 @@ var _note_cursor_size: int = 1
50
50
51
51
var _note_drawing_mode : DrawingMode = DrawingMode .DRAWING_OFF
52
52
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
+
53
57
@onready var _gutter : NoteMapGutter = $ NoteMapGutter
54
58
@onready var _scrollbar : NoteMapScrollbar = $ NoteMapScrollbar
55
59
@onready var _overlay : NoteMapOverlay = $ NoteMapOverlay
@@ -98,9 +102,15 @@ func _notification(what: int) -> void:
98
102
if Engine .is_editor_hint ():
99
103
return
100
104
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 :
102
110
if _note_drawing_mode != DrawingMode .DRAWING_OFF :
103
111
_stop_drawing_notes ()
112
+ if _note_selecting :
113
+ _stop_selecting_notes ()
104
114
105
115
106
116
func _gui_input (event : InputEvent ) -> void :
@@ -120,25 +130,46 @@ func _gui_input(event: InputEvent) -> void:
120
130
_change_scroll_offset (- 1 )
121
131
122
132
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 )
124
138
elif mb .button_index == MOUSE_BUTTON_RIGHT :
139
+ _clear_note_selection ()
125
140
_start_drawing_notes (DrawingMode .DRAWING_REMOVE )
126
141
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 ) :
129
144
_stop_drawing_notes ()
145
+ elif _note_selecting && mb .button_index == MOUSE_BUTTON_LEFT :
146
+ _stop_selecting_notes ()
130
147
131
148
132
149
func _shortcut_input (event : InputEvent ) -> void :
150
+ if Controller .is_song_editing_locked ():
151
+ return
152
+
133
153
if event .is_action_pressed ("bosca_notemap_cursor_bigger" , true , true ):
134
154
_resize_note_cursor (1 )
135
155
elif event .is_action_pressed ("bosca_notemap_cursor_smaller" , true , true ):
136
156
_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 ()
137
163
138
164
139
165
func _physics_process (_delta : float ) -> void :
140
166
_process_note_cursor ()
141
167
_process_note_drawing ()
168
+ _process_note_selecting ()
169
+
170
+
171
+ func _update_processing_state () -> void :
172
+ set_physics_process (_note_cursor_visible || _note_selecting )
142
173
143
174
144
175
func _draw () -> void :
@@ -413,17 +444,20 @@ func _update_playback_cursor() -> void:
413
444
# Grid layout and coordinates.
414
445
415
446
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 :
416
451
var available_rect : Rect2 = get_available_rect ()
417
452
var note_height := get_theme_constant ("note_height" , "NoteMap" )
418
453
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 ):
421
455
return Vector2i (- 1 , - 1 )
422
456
423
- var mouse_normalized := mouse_position - available_rect .position
457
+ var position_normalized := at_position - available_rect .position
424
458
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 )
427
461
return cell_indexed
428
462
429
463
@@ -558,7 +592,8 @@ func _update_active_notes() -> void:
558
592
var note := ActiveNote .new ()
559
593
note .note_value = note_value_normalized
560
594
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 )
562
597
note .length = note_data .z
563
598
_active_notes .push_back (note )
564
599
@@ -585,12 +620,12 @@ func _update_gutter_size() -> void:
585
620
func _show_note_cursor () -> void :
586
621
_note_cursor_visible = true
587
622
_process_note_cursor ()
588
- set_physics_process ( true )
623
+ _update_processing_state ( )
589
624
590
625
591
626
func _hide_note_cursor () -> void :
592
- set_physics_process (false )
593
627
_note_cursor_visible = false
628
+ _update_processing_state ()
594
629
_process_note_cursor ()
595
630
596
631
@@ -701,6 +736,136 @@ func _preview_note_at_cursor(row_index: int) -> void:
701
736
Controller .preview_pattern_note (note_value , _note_cursor_size )
702
737
703
738
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
+
704
869
class NoteRow :
705
870
var note_index : int = - 1
706
871
var label : String = ""
@@ -719,5 +884,8 @@ class OctaveRow:
719
884
class ActiveNote :
720
885
var note_value : int = - 1
721
886
var note_index : int = - 1
887
+ var cell_index : Vector2i = Vector2i (- 1 , - 1 )
722
888
var position : Vector2 = Vector2 .ZERO
723
889
var length : int = 1
890
+
891
+ var selected : bool = false
0 commit comments